scream-code 0.14.6 → 0.14.8

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.
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-D90kvbyJ.mjs";
7
7
  import { C as join$1, D as resolve$1, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize, x as dirname$2, y as KnowledgeStore } from "./src-BH9W5k24.mjs";
8
8
  import { t as require_base64_js } from "./base64-js-DzVmk6Nb.mjs";
9
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-C7gjr4Gk.mjs";
9
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-B0yHChBj.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
12
12
  import * as fs$1 from "node:fs/promises";
@@ -37,11 +37,11 @@ import { EventEmitter as EventEmitter$1 } from "node:events";
37
37
  import { StringDecoder } from "node:string_decoder";
38
38
  import Pi from "assert";
39
39
  import ro from "node:assert";
40
+ import { fileURLToPath, pathToFileURL } from "node:url";
40
41
  import * as posixPath from "node:path/posix";
41
42
  import * as win32Path from "node:path/win32";
42
43
  import { createServer } from "node:http";
43
44
  import * as z$1 from "zod/v4";
44
- import { fileURLToPath, pathToFileURL } from "node:url";
45
45
  import { parse as parse$1, stringify } from "smol-toml";
46
46
  import "zod/v3";
47
47
  import * as z4mini from "zod/v4-mini";
@@ -751,7 +751,7 @@ function isOrphanedToolCallMessage(lowerMessage) {
751
751
  */
752
752
  function isOrphanedToolCallError(error) {
753
753
  if (error instanceof APIOrphanedToolCallError) return true;
754
- return isOrphanedToolCallMessage(errorMessage$5(error).toLowerCase());
754
+ return isOrphanedToolCallMessage(errorMessage$6(error).toLowerCase());
755
755
  }
756
756
  /**
757
757
  * The API returned an empty response (no content, no tool calls).
@@ -829,7 +829,7 @@ function isRequestTooLargeError(error) {
829
829
  if (error instanceof APIStatusError) return error.statusCode === 413;
830
830
  return false;
831
831
  }
832
- function errorMessage$5(error) {
832
+ function errorMessage$6(error) {
833
833
  return error instanceof Error ? error.message : String(error);
834
834
  }
835
835
  //#endregion
@@ -1841,16 +1841,16 @@ const parseLogLevel$2 = (maybeLevel, sourceName, client) => {
1841
1841
  if (hasOwn$3(levelNumbers$2, maybeLevel)) return maybeLevel;
1842
1842
  loggerFor$2(client).warn(`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(Object.keys(levelNumbers$2))}`);
1843
1843
  };
1844
- function noop$3() {}
1844
+ function noop$4() {}
1845
1845
  function makeLogFn$2(fnLevel, logger, logLevel) {
1846
- if (!logger || levelNumbers$2[fnLevel] > levelNumbers$2[logLevel]) return noop$3;
1846
+ if (!logger || levelNumbers$2[fnLevel] > levelNumbers$2[logLevel]) return noop$4;
1847
1847
  else return logger[fnLevel].bind(logger);
1848
1848
  }
1849
1849
  const noopLogger$2 = {
1850
- error: noop$3,
1851
- warn: noop$3,
1852
- info: noop$3,
1853
- debug: noop$3
1850
+ error: noop$4,
1851
+ warn: noop$4,
1852
+ info: noop$4,
1853
+ debug: noop$4
1854
1854
  };
1855
1855
  let cachedLoggers$2 = /* @__PURE__ */ new WeakMap();
1856
1856
  function loggerFor$2(client) {
@@ -10411,7 +10411,7 @@ var require_gaxios = /* @__PURE__ */ __commonJSMin(((exports) => {
10411
10411
  }
10412
10412
  static async #getFetch() {
10413
10413
  const hasWindow = typeof window !== "undefined" && !!window;
10414
- this.#fetch ||= hasWindow ? window.fetch : (await import("./src-C8lOjRbK.mjs")).default;
10414
+ this.#fetch ||= hasWindow ? window.fetch : (await import("./src-C3oCiVNb.mjs")).default;
10415
10415
  return this.#fetch;
10416
10416
  }
10417
10417
  /**
@@ -31847,16 +31847,16 @@ const parseLogLevel$1 = (maybeLevel, sourceName, client) => {
31847
31847
  if (hasOwn$2(levelNumbers$1, maybeLevel)) return maybeLevel;
31848
31848
  loggerFor$1(client).warn(`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(Object.keys(levelNumbers$1))}`);
31849
31849
  };
31850
- function noop$2() {}
31850
+ function noop$3() {}
31851
31851
  function makeLogFn$1(fnLevel, logger, logLevel) {
31852
- if (!logger || levelNumbers$1[fnLevel] > levelNumbers$1[logLevel]) return noop$2;
31852
+ if (!logger || levelNumbers$1[fnLevel] > levelNumbers$1[logLevel]) return noop$3;
31853
31853
  else return logger[fnLevel].bind(logger);
31854
31854
  }
31855
31855
  const noopLogger$1 = {
31856
- error: noop$2,
31857
- warn: noop$2,
31858
- info: noop$2,
31859
- debug: noop$2
31856
+ error: noop$3,
31857
+ warn: noop$3,
31858
+ info: noop$3,
31859
+ debug: noop$3
31860
31860
  };
31861
31861
  let cachedLoggers$1 = /* @__PURE__ */ new WeakMap();
31862
31862
  function loggerFor$1(client) {
@@ -34836,7 +34836,7 @@ function normalizeScreamToolSchema(schema) {
34836
34836
  }
34837
34837
  function ensureScreamPropertyTypes(schema) {
34838
34838
  const normalized = cloneJsonValue(schema);
34839
- if (!isRecord$4(normalized)) throw new Error("JSON Schema root must normalize to an object.");
34839
+ if (!isRecord$5(normalized)) throw new Error("JSON Schema root must normalize to an object.");
34840
34840
  recurseSchema(normalized);
34841
34841
  return normalized;
34842
34842
  }
@@ -34897,7 +34897,7 @@ function resolveLocalJsonPointer(root, ref) {
34897
34897
  let current = root;
34898
34898
  for (const rawPart of ref.slice(2).split("/")) {
34899
34899
  const part = unescapeJsonPointerPart(rawPart);
34900
- if (isRecord$4(current)) {
34900
+ if (isRecord$5(current)) {
34901
34901
  if (!hasOwn$1(current, part)) return { found: false };
34902
34902
  current = current[part];
34903
34903
  } else if (Array.isArray(current)) {
@@ -34919,20 +34919,20 @@ function parseJsonPointerArrayIndex(part) {
34919
34919
  return Number(part);
34920
34920
  }
34921
34921
  function recurseSchema(node) {
34922
- if (!isRecord$4(node)) return;
34922
+ if (!isRecord$5(node)) return;
34923
34923
  visitChildSchemas(node, normalizeProperty);
34924
34924
  }
34925
34925
  function visitChildSchemas(node, visit) {
34926
34926
  for (const { key, kind } of CHILD_SCHEMA_SLOTS) {
34927
34927
  const value = node[key];
34928
34928
  if (kind === "single") {
34929
- if (isRecord$4(value)) visit(value);
34929
+ if (isRecord$5(value)) visit(value);
34930
34930
  } else if (kind === "array") {
34931
34931
  if (Array.isArray(value)) for (const item of value) visit(item);
34932
34932
  } else if (kind === "map") {
34933
- if (isRecord$4(value)) for (const item of Object.values(value)) visit(item);
34933
+ if (isRecord$5(value)) for (const item of Object.values(value)) visit(item);
34934
34934
  } else if (kind === "schema-or-array") {
34935
- if (isRecord$4(value)) visit(value);
34935
+ if (isRecord$5(value)) visit(value);
34936
34936
  else if (Array.isArray(value)) for (const item of value) visit(item);
34937
34937
  }
34938
34938
  }
@@ -34944,7 +34944,7 @@ function childSchemaKeysForParentType(parentType) {
34944
34944
  });
34945
34945
  }
34946
34946
  function normalizeProperty(node) {
34947
- if (!isRecord$4(node)) return;
34947
+ if (!isRecord$5(node)) return;
34948
34948
  if (!hasOwn$1(node, "type") && !hasAnyKey(node, TYPE_COMPLETION_SKIP_KEYS)) {
34949
34949
  const enumValues = node["enum"];
34950
34950
  if (Array.isArray(enumValues) && enumValues.length > 0) node["type"] = inferTypeFromValues(enumValues);
@@ -35008,14 +35008,14 @@ function hasAnyKey(obj, keys) {
35008
35008
  }
35009
35009
  function cloneJsonValue(value) {
35010
35010
  if (Array.isArray(value)) return value.map((item) => cloneJsonValue(item));
35011
- if (isRecord$4(value)) {
35011
+ if (isRecord$5(value)) {
35012
35012
  const cloned = {};
35013
35013
  for (const [key, child] of Object.entries(value)) cloned[key] = cloneJsonValue(child);
35014
35014
  return cloned;
35015
35015
  }
35016
35016
  return value;
35017
35017
  }
35018
- function isRecord$4(value) {
35018
+ function isRecord$5(value) {
35019
35019
  return typeof value === "object" && value !== null && !Array.isArray(value);
35020
35020
  }
35021
35021
  function hasOwn$1(obj, key) {
@@ -35785,16 +35785,16 @@ const parseLogLevel = (maybeLevel, sourceName, client) => {
35785
35785
  if (hasOwn(levelNumbers, maybeLevel)) return maybeLevel;
35786
35786
  loggerFor(client).warn(`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(Object.keys(levelNumbers))}`);
35787
35787
  };
35788
- function noop$1() {}
35788
+ function noop$2() {}
35789
35789
  function makeLogFn(fnLevel, logger, logLevel) {
35790
- if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) return noop$1;
35790
+ if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) return noop$2;
35791
35791
  else return logger[fnLevel].bind(logger);
35792
35792
  }
35793
35793
  const noopLogger = {
35794
- error: noop$1,
35795
- warn: noop$1,
35796
- info: noop$1,
35797
- debug: noop$1
35794
+ error: noop$2,
35795
+ warn: noop$2,
35796
+ info: noop$2,
35797
+ debug: noop$2
35798
35798
  };
35799
35799
  let cachedLoggers = /* @__PURE__ */ new WeakMap();
35800
35800
  function loggerFor(client) {
@@ -44510,8 +44510,8 @@ function reasoningEffortToThinkingEffort(reasoning) {
44510
44510
  case "minimal": return "low";
44511
44511
  case "medium": return "medium";
44512
44512
  case "high": return "high";
44513
- case "xhigh":
44514
- case "max": return "xhigh";
44513
+ case "xhigh": return "xhigh";
44514
+ case "max": return "max";
44515
44515
  case "none": return "off";
44516
44516
  default: return "off";
44517
44517
  }
@@ -44941,6 +44941,7 @@ var ScreamChatProvider = class {
44941
44941
  _baseUrl;
44942
44942
  _defaultHeaders;
44943
44943
  _generationKwargs;
44944
+ _forceThinking;
44944
44945
  _client;
44945
44946
  _clientFactory;
44946
44947
  _files;
@@ -44952,6 +44953,7 @@ var ScreamChatProvider = class {
44952
44953
  this._clientFactory = options.clientFactory;
44953
44954
  this._model = options.model;
44954
44955
  this._stream = options.stream ?? true;
44956
+ this._forceThinking = options.forceThinking;
44955
44957
  this._generationKwargs = { ...options.generationKwargs };
44956
44958
  this._client = this._apiKey === void 0 ? void 0 : new OpenAI({
44957
44959
  apiKey: this._apiKey,
@@ -45024,12 +45026,8 @@ var ScreamChatProvider = class {
45024
45026
  return UNKNOWN_CAPABILITY;
45025
45027
  }
45026
45028
  withThinking(effort) {
45027
- const thinking = { type: effort === "off" ? "disabled" : "enabled" };
45028
45029
  let reasoningEffort;
45029
45030
  switch (effort) {
45030
- case "off":
45031
- reasoningEffort = void 0;
45032
- break;
45033
45031
  case "low":
45034
45032
  reasoningEffort = "low";
45035
45033
  break;
@@ -45037,11 +45035,18 @@ var ScreamChatProvider = class {
45037
45035
  reasoningEffort = "medium";
45038
45036
  break;
45039
45037
  case "high":
45038
+ reasoningEffort = "high";
45039
+ break;
45040
45040
  case "xhigh":
45041
45041
  case "max":
45042
- reasoningEffort = "high";
45042
+ reasoningEffort = "max";
45043
+ break;
45044
+ case "off":
45045
+ reasoningEffort = void 0;
45043
45046
  break;
45044
45047
  }
45048
+ if (effort === "off" && this._forceThinking === true) return this._withGenerationKwargs({ reasoning_effort: "low" }).withExtraBody({ thinking: { type: "enabled" } });
45049
+ const thinking = { type: effort === "off" ? "disabled" : "enabled" };
45045
45050
  return this._withGenerationKwargs({ reasoning_effort: reasoningEffort }).withExtraBody({ thinking });
45046
45051
  }
45047
45052
  withGenerationKwargs(kwargs) {
@@ -45247,6 +45252,7 @@ var OpenAILegacyChatProvider = class {
45247
45252
  _reasoningKey;
45248
45253
  _reasoningEffort;
45249
45254
  _thinkingExplicitlyOff = false;
45255
+ _forceThinking;
45250
45256
  _generationKwargs;
45251
45257
  _toolMessageConversion;
45252
45258
  _client;
@@ -45262,6 +45268,7 @@ var OpenAILegacyChatProvider = class {
45262
45268
  const normalizedReasoningKey = options.reasoningKey?.trim();
45263
45269
  this._reasoningKey = normalizedReasoningKey !== void 0 && normalizedReasoningKey.length > 0 ? normalizedReasoningKey : void 0;
45264
45270
  this._reasoningEffort = void 0;
45271
+ this._forceThinking = options.forceThinking;
45265
45272
  this._generationKwargs = {};
45266
45273
  if (options.maxTokens !== void 0) this._generationKwargs.max_tokens = options.maxTokens;
45267
45274
  this._toolMessageConversion = options.toolMessageConversion ?? null;
@@ -45315,9 +45322,16 @@ var OpenAILegacyChatProvider = class {
45315
45322
  }
45316
45323
  }
45317
45324
  withThinking(effort) {
45318
- const reasoningEffort = thinkingEffortToReasoningEffort(effort);
45325
+ let mapped = thinkingEffortToReasoningEffort(effort);
45326
+ if (mapped === "xhigh") mapped = "max";
45327
+ if (effort === "off" && this._forceThinking === true) {
45328
+ const clone = this._clone();
45329
+ clone._reasoningEffort = "low";
45330
+ clone._thinkingExplicitlyOff = false;
45331
+ return clone;
45332
+ }
45319
45333
  const clone = this._clone();
45320
- clone._reasoningEffort = reasoningEffort;
45334
+ clone._reasoningEffort = mapped;
45321
45335
  clone._thinkingExplicitlyOff = effort === "off";
45322
45336
  return clone;
45323
45337
  }
@@ -47448,7 +47462,7 @@ function isAbortError$1(err) {
47448
47462
  if (err instanceof Error) return err.name === "AbortError";
47449
47463
  return false;
47450
47464
  }
47451
- function errorMessage$4(err) {
47465
+ function errorMessage$5(err) {
47452
47466
  if (err instanceof Error) return err.message;
47453
47467
  return String(err);
47454
47468
  }
@@ -52717,7 +52731,7 @@ var UpdateGoalTool = class {
52717
52731
  try {
52718
52732
  await goal.pauseGoal({ reason: "verifying" }, "system");
52719
52733
  } catch (error) {
52720
- return toolError(`Failed to pause goal for verification: ${errorMessage$3(error)}`, goal);
52734
+ return toolError(`Failed to pause goal for verification: ${errorMessage$4(error)}`, goal);
52721
52735
  }
52722
52736
  let rawGrade;
52723
52737
  try {
@@ -52725,7 +52739,7 @@ var UpdateGoalTool = class {
52725
52739
  } catch (error) {
52726
52740
  const resumeError = await resumeAfterGrading(goal);
52727
52741
  if (resumeError !== void 0) return resumeError;
52728
- const reason = `Goal verification could not be completed: ${errorMessage$3(error)}`;
52742
+ const reason = `Goal verification could not be completed: ${errorMessage$4(error)}`;
52729
52743
  this.appendGradingFeedback(reason);
52730
52744
  return {
52731
52745
  isError: true,
@@ -52753,7 +52767,7 @@ var UpdateGoalTool = class {
52753
52767
  name: GOAL_COMPLETION_REMINDER_NAME
52754
52768
  });
52755
52769
  } catch (error) {
52756
- return toolError(`Failed to mark verified goal complete: ${errorMessage$3(error)}`, goal);
52770
+ return toolError(`Failed to mark verified goal complete: ${errorMessage$4(error)}`, goal);
52757
52771
  }
52758
52772
  return {
52759
52773
  output: `Goal verified and marked complete.\n${grade.reason}`,
@@ -52788,7 +52802,7 @@ async function resumeAfterGrading(goal) {
52788
52802
  await goal.resumeGoal({}, "system");
52789
52803
  return;
52790
52804
  } catch (error) {
52791
- return toolError(`Failed to restore active goal after verification: ${errorMessage$3(error)}`, goal);
52805
+ return toolError(`Failed to restore active goal after verification: ${errorMessage$4(error)}`, goal);
52792
52806
  }
52793
52807
  }
52794
52808
  function toolError(message, goal) {
@@ -52797,7 +52811,7 @@ function toolError(message, goal) {
52797
52811
  output: `${message}. Current goal status: ${goal.getGoal().goal?.status ?? "missing"}.`
52798
52812
  };
52799
52813
  }
52800
- function errorMessage$3(error) {
52814
+ function errorMessage$4(error) {
52801
52815
  return error instanceof Error ? error.message : String(error);
52802
52816
  }
52803
52817
  //#endregion
@@ -59083,7 +59097,7 @@ function parseSkillText(options) {
59083
59097
  throw error;
59084
59098
  }
59085
59099
  const frontmatter = parsed.data ?? {};
59086
- if (!isRecord$3(frontmatter)) throw new SkillParseError(`Frontmatter in ${options.skillMdPath} must be a mapping at the top level`);
59100
+ if (!isRecord$4(frontmatter)) throw new SkillParseError(`Frontmatter in ${options.skillMdPath} must be a mapping at the top level`);
59087
59101
  const metadata = normalizeMetadata(frontmatter);
59088
59102
  if (!isSupportedSkillType(metadata.type)) throw new UnsupportedSkillTypeError(metadata.type ?? String(frontmatter["type"]));
59089
59103
  const name = nonEmptyString$1(metadata.name);
@@ -59196,7 +59210,7 @@ function tokenizeArgs(raw) {
59196
59210
  function nonEmptyString$1(value) {
59197
59211
  return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
59198
59212
  }
59199
- function isRecord$3(value) {
59213
+ function isRecord$4(value) {
59200
59214
  return typeof value === "object" && value !== null && !Array.isArray(value);
59201
59215
  }
59202
59216
  //#endregion
@@ -64662,7 +64676,7 @@ const MAX_SKILL_DESC_CHARS = 100;
64662
64676
  function buildSkillToolDescription(agent) {
64663
64677
  const skills = agent.skills?.registry.listInvocableSkills() ?? [];
64664
64678
  const lines = skills.slice(0, MAX_INLINE_SKILLS).map((s) => {
64665
- const desc = s.description.trim().replaceAll(/\n/g, " ").slice(0, MAX_SKILL_DESC_CHARS);
64679
+ const desc = s.description.trim().replaceAll("\n", " ").slice(0, MAX_SKILL_DESC_CHARS);
64666
64680
  return `- ${s.name}: ${desc}`;
64667
64681
  });
64668
64682
  if (skills.length > MAX_INLINE_SKILLS) lines.push(`…${String(skills.length - MAX_INLINE_SKILLS)} more (see skill registry)`);
@@ -64726,6 +64740,8 @@ var SkillTool = class SkillTool {
64726
64740
  if (skill.metadata.disableModelInvocation === true) return errorResult$1(`Skill "${args.skill}" can only be triggered by the user (model invocation is disabled).`);
64727
64741
  const skillArgs = args.args ?? "";
64728
64742
  if (!isInlineSkillType(skill.metadata.type)) return errorResult$1(`Skill "${skill.name}" is not an inline skill and cannot be invoked by the model in v1.`);
64743
+ const owningPluginId = skill.plugin?.id;
64744
+ if (owningPluginId !== void 0) this.agent.toolServices?.plugins?.recordUsage?.(owningPluginId, true);
64729
64745
  const origin = skillOrigin(skill, skillArgs, currentDepth);
64730
64746
  skills.recordActivation(origin);
64731
64747
  const skillContent = skills.registry.renderSkillPrompt(skill, skillArgs);
@@ -66196,6 +66212,7 @@ const ModelAliasSchema = z.object({
66196
66212
  displayName: z.string().optional(),
66197
66213
  reasoningKey: z.string().optional(),
66198
66214
  adaptiveThinking: z.boolean().optional(),
66215
+ forceThinking: z.boolean().optional(),
66199
66216
  thinkingLevels: z.array(ThinkingEffortSchema).optional()
66200
66217
  });
66201
66218
  const ThinkingConfigSchema = z.object({
@@ -66395,6 +66412,13 @@ const PLUGIN_NAME_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}$/;
66395
66412
  function normalizePluginId(name) {
66396
66413
  return name.toLowerCase();
66397
66414
  }
66415
+ function summarizePluginSync(report) {
66416
+ return {
66417
+ ok: report.ok,
66418
+ applied: report.applied,
66419
+ failed: report.failed
66420
+ };
66421
+ }
66398
66422
  //#endregion
66399
66423
  //#region ../../packages/agent-core/src/plugin/manifest.ts
66400
66424
  const SCREAM_PLUGIN_ROOT_PATH = "scream.plugin.json";
@@ -66507,11 +66531,11 @@ async function parseManifest(pluginRoot) {
66507
66531
  return {
66508
66532
  manifest: {
66509
66533
  name,
66510
- version: stringField$1(raw, "version"),
66511
- description: stringField$1(raw, "description"),
66512
- keywords: stringArrayField$1(raw, "keywords"),
66513
- homepage: stringField$1(raw, "homepage"),
66514
- license: stringField$1(raw, "license"),
66534
+ version: stringField$2(raw, "version"),
66535
+ description: stringField$2(raw, "description"),
66536
+ keywords: stringArrayField$2(raw, "keywords"),
66537
+ homepage: stringField$2(raw, "homepage"),
66538
+ license: stringField$2(raw, "license"),
66515
66539
  author: readAuthor(raw["author"]),
66516
66540
  skills,
66517
66541
  sessionStart: readSessionStart(raw["sessionStart"], diagnostics),
@@ -66578,7 +66602,7 @@ function readHooks(pluginRoot, raw, diagnostics) {
66578
66602
  });
66579
66603
  continue;
66580
66604
  }
66581
- const event = stringField$1(entry, "event");
66605
+ const event = stringField$2(entry, "event");
66582
66606
  if (event === void 0) {
66583
66607
  diagnostics.push({
66584
66608
  severity: "warn",
@@ -66586,7 +66610,7 @@ function readHooks(pluginRoot, raw, diagnostics) {
66586
66610
  });
66587
66611
  continue;
66588
66612
  }
66589
- let command = stringField$1(entry, "command");
66613
+ let command = stringField$2(entry, "command");
66590
66614
  if (command === void 0) {
66591
66615
  diagnostics.push({
66592
66616
  severity: "warn",
@@ -66599,7 +66623,7 @@ function readHooks(pluginRoot, raw, diagnostics) {
66599
66623
  const hook = {
66600
66624
  event,
66601
66625
  command,
66602
- ...stringField$1(entry, "matcher") !== void 0 ? { matcher: stringField$1(entry, "matcher") } : {},
66626
+ ...stringField$2(entry, "matcher") !== void 0 ? { matcher: stringField$2(entry, "matcher") } : {},
66603
66627
  ...timeout !== void 0 ? { timeout } : {}
66604
66628
  };
66605
66629
  out.push(hook);
@@ -66774,8 +66798,8 @@ async function normalizePluginMcpServer(input) {
66774
66798
  function readAuthor(raw) {
66775
66799
  if (typeof raw === "string") return { name: raw };
66776
66800
  if (!isObject(raw)) return void 0;
66777
- const name = stringField$1(raw, "name");
66778
- const email = stringField$1(raw, "email");
66801
+ const name = stringField$2(raw, "name");
66802
+ const email = stringField$2(raw, "email");
66779
66803
  if (name === void 0 && email === void 0) return void 0;
66780
66804
  return {
66781
66805
  name,
@@ -66785,21 +66809,21 @@ function readAuthor(raw) {
66785
66809
  function readInterface(raw) {
66786
66810
  if (!isObject(raw)) return void 0;
66787
66811
  const out = {
66788
- displayName: stringField$1(raw, "displayName"),
66789
- shortDescription: stringField$1(raw, "shortDescription"),
66790
- longDescription: stringField$1(raw, "longDescription"),
66791
- developerName: stringField$1(raw, "developerName"),
66792
- websiteURL: stringField$1(raw, "websiteURL")
66812
+ displayName: stringField$2(raw, "displayName"),
66813
+ shortDescription: stringField$2(raw, "shortDescription"),
66814
+ longDescription: stringField$2(raw, "longDescription"),
66815
+ developerName: stringField$2(raw, "developerName"),
66816
+ websiteURL: stringField$2(raw, "websiteURL")
66793
66817
  };
66794
66818
  return Object.values(out).some((value) => value !== void 0) ? out : void 0;
66795
66819
  }
66796
- function stringField$1(raw, key) {
66820
+ function stringField$2(raw, key) {
66797
66821
  const value = raw[key];
66798
66822
  if (typeof value !== "string") return void 0;
66799
66823
  const trimmed = value.trim();
66800
66824
  return trimmed.length === 0 ? void 0 : trimmed;
66801
66825
  }
66802
- function stringArrayField$1(raw, key) {
66826
+ function stringArrayField$2(raw, key) {
66803
66827
  const value = raw[key];
66804
66828
  if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) return;
66805
66829
  return value;
@@ -66945,6 +66969,24 @@ async function writeInstalled(screamHomeDir, data) {
66945
66969
  }
66946
66970
  //#endregion
66947
66971
  //#region ../../packages/agent-core/src/plugin/source.ts
66972
+ /** Compare dotted numeric versions (e.g. "1.2.0"); undefined when either side has no leading dotted-numeric segment. */
66973
+ function compareManifestVersions(a, b) {
66974
+ const parse = (value) => {
66975
+ if (typeof value !== "string") return void 0;
66976
+ const captured = value.trim().match(/^v?(\d+(?:\.\d+){0,3})/)?.[1];
66977
+ if (captured === void 0) return void 0;
66978
+ return captured.split(".").map(Number);
66979
+ };
66980
+ const left = parse(a);
66981
+ const right = parse(b);
66982
+ if (left === void 0 || right === void 0) return void 0;
66983
+ for (let index = 0; index < 3; index += 1) {
66984
+ const lhs = left[index] ?? 0;
66985
+ const rhs = right[index] ?? 0;
66986
+ if (lhs !== rhs) return lhs < rhs ? -1 : 1;
66987
+ }
66988
+ return 0;
66989
+ }
66948
66990
  const SHA_RE = /^[0-9a-f]{7,40}$/;
66949
66991
  const GITHUB_NAME_RE = /^[A-Za-z0-9._-]+$/;
66950
66992
  function resolveInstallSource(source) {
@@ -67036,6 +67078,198 @@ function decodeRefSegments(segments) {
67036
67078
  }).join("/");
67037
67079
  }
67038
67080
  //#endregion
67081
+ //#region ../../packages/agent-core/src/plugin/quarantine.ts
67082
+ function quarantinePath(screamHomeDir) {
67083
+ return path.join(screamHomeDir, "plugins", "quarantine.json");
67084
+ }
67085
+ /** Normalize a raw user-provided source string into its origin key. */
67086
+ function sourceKeyFromRawSource(source) {
67087
+ const trimmed = source.trim();
67088
+ const github = trimmed.match(/github\.com[/]([^/]+)[/]([^/#?\s]+)/);
67089
+ if (github !== null) return `github:${github[1]}/${github[2]}`;
67090
+ return trimmed;
67091
+ }
67092
+ /** Origin key for an installed record's persisted fields. */
67093
+ function sourceKeyFromFields(fields) {
67094
+ if (fields.github !== void 0) return `github:${fields.github.owner}/${fields.github.repo}`;
67095
+ return fields.originalSource ?? "";
67096
+ }
67097
+ async function readQuarantine(screamHomeDir) {
67098
+ try {
67099
+ const entries = JSON.parse(await readFile(quarantinePath(screamHomeDir), "utf8")).entries;
67100
+ return Array.isArray(entries) ? entries : [];
67101
+ } catch {
67102
+ return [];
67103
+ }
67104
+ }
67105
+ async function appendQuarantine(screamHomeDir, entry) {
67106
+ const entries = await readQuarantine(screamHomeDir);
67107
+ entries.push(entry);
67108
+ const file = {
67109
+ version: 1,
67110
+ entries: entries.slice(-200)
67111
+ };
67112
+ await mkdir(path.dirname(quarantinePath(screamHomeDir)), { recursive: true });
67113
+ await writeFile(quarantinePath(screamHomeDir), `${JSON.stringify(file, null, 2)}\n`, "utf8");
67114
+ }
67115
+ /** Mark the newest unacknowledged entry for this key; no-op when absent. */
67116
+ async function acknowledgeQuarantine(screamHomeDir, sourceKey) {
67117
+ const entries = await readQuarantine(screamHomeDir);
67118
+ const now = (/* @__PURE__ */ new Date()).toISOString();
67119
+ let changed = false;
67120
+ for (let index = 0; index < entries.length; index += 1) {
67121
+ const candidate = entries[index];
67122
+ if (candidate === void 0) continue;
67123
+ if (candidate.sourceKey === sourceKey && candidate.acknowledgedAt === void 0) {
67124
+ entries[index] = {
67125
+ ...candidate,
67126
+ acknowledgedAt: now
67127
+ };
67128
+ changed = true;
67129
+ }
67130
+ }
67131
+ if (!changed) return;
67132
+ const file = {
67133
+ version: 1,
67134
+ entries
67135
+ };
67136
+ await writeFile(quarantinePath(screamHomeDir), `${JSON.stringify(file, null, 2)}\n`, "utf8");
67137
+ }
67138
+ /** Newest unacknowledged entry for a normalized origin key, if any. */
67139
+ function matchQuarantine(entries, sourceKey) {
67140
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
67141
+ const candidate = entries[index];
67142
+ if (candidate === void 0) continue;
67143
+ if (candidate.sourceKey === sourceKey && candidate.acknowledgedAt === void 0) return candidate;
67144
+ }
67145
+ }
67146
+ //#endregion
67147
+ //#region ../../packages/agent-core/src/plugin/stats.ts
67148
+ const STATS_DEBOUNCE_MS = 5e3;
67149
+ const caches = /* @__PURE__ */ new Map();
67150
+ function statsPath(screamHomeDir) {
67151
+ return path.join(screamHomeDir, "plugins", "stats.json");
67152
+ }
67153
+ function cacheFor(screamHomeDir) {
67154
+ let cache = caches.get(screamHomeDir);
67155
+ if (cache === void 0) {
67156
+ cache = {
67157
+ loaded: false,
67158
+ base: /* @__PURE__ */ new Map(),
67159
+ deltas: /* @__PURE__ */ new Map(),
67160
+ timer: void 0
67161
+ };
67162
+ caches.set(screamHomeDir, cache);
67163
+ }
67164
+ return cache;
67165
+ }
67166
+ async function loadIntoCache(screamHomeDir) {
67167
+ const cache = cacheFor(screamHomeDir);
67168
+ if (cache.loaded) return cache.base;
67169
+ try {
67170
+ const byId = JSON.parse(await readFile(statsPath(screamHomeDir), "utf8")).byId ?? {};
67171
+ for (const [id, entry] of Object.entries(byId)) if (typeof entry.calls === "number" && typeof entry.okCalls === "number") cache.base.set(id, {
67172
+ calls: entry.calls,
67173
+ okCalls: entry.okCalls,
67174
+ lastUsedAt: entry.lastUsedAt
67175
+ });
67176
+ } catch {}
67177
+ cache.loaded = true;
67178
+ return cache.base;
67179
+ }
67180
+ function mergeEntry(entry, delta) {
67181
+ entry.calls += delta.calls;
67182
+ entry.okCalls += delta.okCalls;
67183
+ entry.lastUsedAt = delta.lastUsedAt;
67184
+ }
67185
+ /** Effective view = base ⊕ current deltas (deltas survive independently of load timing). */
67186
+ async function effectiveEntries(screamHomeDir) {
67187
+ const cache = cacheFor(screamHomeDir);
67188
+ const base = await loadIntoCache(screamHomeDir);
67189
+ if (cache.deltas.size === 0) return base;
67190
+ const merged = new Map(base);
67191
+ for (const [id, delta] of cache.deltas) {
67192
+ const entry = { ...merged.get(id) ?? {
67193
+ calls: 0,
67194
+ okCalls: 0,
67195
+ lastUsedAt: void 0
67196
+ } };
67197
+ mergeEntry(entry, delta);
67198
+ merged.set(id, entry);
67199
+ }
67200
+ return merged;
67201
+ }
67202
+ /** Force-write base⊕deltas for one home; safe to call repeatedly. */
67203
+ async function flushStats(screamHomeDir) {
67204
+ const cache = cacheFor(screamHomeDir);
67205
+ if (cache.timer !== void 0) {
67206
+ clearTimeout(cache.timer);
67207
+ cache.timer = void 0;
67208
+ }
67209
+ const base = await loadIntoCache(screamHomeDir);
67210
+ for (const [id, delta] of cache.deltas) {
67211
+ const entry = base.get(id) ?? {
67212
+ calls: 0,
67213
+ okCalls: 0,
67214
+ lastUsedAt: void 0
67215
+ };
67216
+ mergeEntry(entry, delta);
67217
+ base.set(id, entry);
67218
+ }
67219
+ cache.deltas.clear();
67220
+ const byId = {};
67221
+ for (const [id, entry] of base) byId[id] = { ...entry };
67222
+ await mkdir(path.dirname(statsPath(screamHomeDir)), { recursive: true });
67223
+ const file = {
67224
+ version: 1,
67225
+ byId
67226
+ };
67227
+ await writeFile(statsPath(screamHomeDir), `${JSON.stringify(file, null, 2)}\n`, "utf8");
67228
+ }
67229
+ /** Arm the debounced writer at most once per home (first-schedule semantics). */
67230
+ function ensureFlushScheduled(screamHomeDir) {
67231
+ const cache = cacheFor(screamHomeDir);
67232
+ if (cache.timer !== void 0) return;
67233
+ cache.timer = setTimeout(() => {
67234
+ cache.timer = void 0;
67235
+ flushStats(screamHomeDir).catch(() => {});
67236
+ }, STATS_DEBOUNCE_MS);
67237
+ }
67238
+ /** Synchronous counter bump into the delta layer; disk write is debounced. */
67239
+ function recordUsageInMemory(screamHomeDir, pluginId, ok) {
67240
+ const cache = cacheFor(screamHomeDir);
67241
+ const existing = cache.deltas.get(pluginId) ?? {
67242
+ calls: 0,
67243
+ okCalls: 0,
67244
+ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
67245
+ };
67246
+ cache.deltas.set(pluginId, {
67247
+ calls: existing.calls + 1,
67248
+ okCalls: existing.okCalls + (ok ? 1 : 0),
67249
+ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
67250
+ });
67251
+ loadIntoCache(screamHomeDir);
67252
+ ensureFlushScheduled(screamHomeDir);
67253
+ }
67254
+ /** Drop counters for a removed/uninstalled plugin id (base and deltas).
67255
+ * Best-effort: the in-memory delete always happens; a failed disk flush must
67256
+ * never fail the removal whose caller depends on it. */
67257
+ async function forgetStats(screamHomeDir, pluginId) {
67258
+ cacheFor(screamHomeDir).deltas.delete(pluginId);
67259
+ (await loadIntoCache(screamHomeDir)).delete(pluginId);
67260
+ try {
67261
+ await flushStats(screamHomeDir);
67262
+ } catch {}
67263
+ }
67264
+ /** Read-through snapshot used by tool surfaces (check/list views). */
67265
+ async function getUsage(screamHomeDir, pluginId) {
67266
+ return (await effectiveEntries(screamHomeDir)).get(pluginId) ?? {
67267
+ calls: 0,
67268
+ okCalls: 0,
67269
+ lastUsedAt: void 0
67270
+ };
67271
+ }
67272
+ //#endregion
67039
67273
  //#region ../../packages/agent-core/src/plugin/manager.ts
67040
67274
  const SCREAM_NODE_FALLBACK_SUBCOMMAND = "__plugin_run_node";
67041
67275
  var PluginManager = class {
@@ -67162,6 +67396,243 @@ var PluginManager = class {
67162
67396
  await this.persist();
67163
67397
  return record;
67164
67398
  }
67399
+ /**
67400
+ * Replace an already-installed plugin's files from `source`, keeping a
67401
+ * single-slot backup of the previous contents under
67402
+ * `plugins/backups/<id>/`. Unlike a plain reinstall this records the
67403
+ * version transition and refuses to run for unknown ids, so it can always
67404
+ * be paired with `rollback`.
67405
+ */
67406
+ async upgrade(source) {
67407
+ const resolved = resolveInstallSource(source);
67408
+ const isLocal = resolved.kind === "local-path";
67409
+ let stagedRoot;
67410
+ let originalSource;
67411
+ let sourceType;
67412
+ let parsed;
67413
+ let github;
67414
+ let remoteTmpRoot;
67415
+ if (resolved.kind === "local-path") {
67416
+ stagedRoot = await normalizeInstallRoot(resolved.path);
67417
+ originalSource = resolved.path;
67418
+ sourceType = "local-path";
67419
+ parsed = await parseManifest(stagedRoot);
67420
+ if (parsed.manifest === void 0) {
67421
+ const msg = parsed.diagnostics.find((d) => d.severity === "error")?.message ?? "no manifest";
67422
+ throw new Error(`Cannot upgrade plugin at ${stagedRoot}: ${msg}`);
67423
+ }
67424
+ } else {
67425
+ let zipUrl;
67426
+ const tmpDir = await mkdtemp(path.join(tmpdir(), "scream-plugin-upgrade-"));
67427
+ stagedRoot = tmpDir;
67428
+ remoteTmpRoot = tmpDir;
67429
+ try {
67430
+ if (resolved.kind === "github") {
67431
+ const githubResolution = await resolveGithubSource(resolved);
67432
+ zipUrl = githubResolution.tarballUrl;
67433
+ originalSource = source.trim();
67434
+ sourceType = "github";
67435
+ github = {
67436
+ owner: resolved.owner,
67437
+ repo: resolved.repo,
67438
+ ref: githubResolution.ref
67439
+ };
67440
+ } else {
67441
+ zipUrl = resolved.path;
67442
+ originalSource = resolved.path;
67443
+ sourceType = "zip-url";
67444
+ }
67445
+ stagedRoot = await extractZip(await downloadZip(zipUrl), tmpDir);
67446
+ parsed = await parseManifest(stagedRoot);
67447
+ if (parsed.manifest === void 0) {
67448
+ const msg = parsed.diagnostics.find((d) => d.severity === "error")?.message ?? "no manifest";
67449
+ throw new Error(`Cannot upgrade plugin from ${originalSource}: ${msg}`);
67450
+ }
67451
+ } catch (error) {
67452
+ await rm(tmpDir, {
67453
+ recursive: true,
67454
+ force: true
67455
+ });
67456
+ throw error;
67457
+ }
67458
+ }
67459
+ const disposeStaged = async () => {
67460
+ if (!isLocal) {
67461
+ await rm(stagedRoot, {
67462
+ recursive: true,
67463
+ force: true
67464
+ });
67465
+ if (remoteTmpRoot !== void 0 && remoteTmpRoot !== stagedRoot) await rm(remoteTmpRoot, {
67466
+ recursive: true,
67467
+ force: true
67468
+ });
67469
+ }
67470
+ };
67471
+ try {
67472
+ const id = normalizePluginId(parsed.manifest.name);
67473
+ const existing = this.records.get(id);
67474
+ if (existing === void 0) throw new Error(`Plugin "${id}" is not installed; upgrade requires an existing installation`);
67475
+ const oldVersion = existing.manifest?.version;
67476
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(/[:.]/g, "-");
67477
+ const backupsForId = path.join(this.screamHomeDir, "plugins", "backups", id);
67478
+ const backupPath = path.join(backupsForId, `${stamp}-${(oldVersion ?? "unknown").replaceAll(/[:.]/g, "-")}`);
67479
+ await mkdir(backupPath, { recursive: true });
67480
+ await cp(existing.root, backupPath, { recursive: true });
67481
+ try {
67482
+ await writeFile(path.join(backupPath, ".scream-rollback.json"), JSON.stringify({
67483
+ originalSource: existing.originalSource,
67484
+ source: existing.source,
67485
+ ...existing.github !== void 0 ? { github: existing.github } : {}
67486
+ }), "utf8");
67487
+ } catch {}
67488
+ await keepNewestBackupOnly(backupsForId);
67489
+ const comparison = compareManifestVersions(oldVersion, parsed.manifest.version);
67490
+ const warn = comparison === void 0 ? "unknown" : comparison === 0 ? "same" : comparison === 1 ? "downgrade" : void 0;
67491
+ const promotedRoot = await copyPluginToManagedRoot(this.screamHomeDir, id, stagedRoot);
67492
+ const promoted = await parseManifest(promotedRoot);
67493
+ const baseRecord = await recordFrom({
67494
+ id,
67495
+ root: promotedRoot,
67496
+ enabled: existing.enabled,
67497
+ installedAt: existing.installedAt,
67498
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
67499
+ originalSource,
67500
+ source: sourceType,
67501
+ capabilities: existing.capabilities,
67502
+ github,
67503
+ parsed: promoted
67504
+ });
67505
+ const note = `upgraded ${oldVersion ?? "unknown"} -> ${promoted.manifest?.version ?? "unknown"}`;
67506
+ const record = {
67507
+ ...baseRecord,
67508
+ diagnostics: [...baseRecord.diagnostics, {
67509
+ severity: "info",
67510
+ message: note
67511
+ }]
67512
+ };
67513
+ this.records.set(id, record);
67514
+ await this.persist();
67515
+ return {
67516
+ record,
67517
+ from: oldVersion,
67518
+ to: promoted.manifest?.version,
67519
+ ...warn !== void 0 ? { warn } : {},
67520
+ backupPath
67521
+ };
67522
+ } finally {
67523
+ await disposeStaged();
67524
+ }
67525
+ }
67526
+ /**
67527
+ * Restore a plugin's files from its newest backup (created by `upgrade`),
67528
+ * then reload the table. The backup manifest is validated BEFORE anything
67529
+ * under plugins/managed is touched.
67530
+ */
67531
+ async rollback(id) {
67532
+ const key = normalizePluginId(id);
67533
+ const existing = this.records.get(key);
67534
+ if (existing === void 0) throw new Error(`Plugin "${id}" is not installed`);
67535
+ const backupsForId = path.join(this.screamHomeDir, "plugins", "backups", key);
67536
+ let newest;
67537
+ try {
67538
+ newest = (await readdir(backupsForId)).toSorted().at(-1);
67539
+ } catch {
67540
+ newest = void 0;
67541
+ }
67542
+ if (newest === void 0) throw new Error(`No backup available for "${key}"`);
67543
+ const backupRoot = path.join(backupsForId, newest);
67544
+ if ((await parseManifest(backupRoot)).manifest === void 0) throw new Error(`Backup at ${backupRoot} has no readable manifest`);
67545
+ let preUpgrade = {};
67546
+ try {
67547
+ preUpgrade = JSON.parse(await readFile(path.join(backupRoot, ".scream-rollback.json"), "utf8"));
67548
+ } catch {}
67549
+ const promotedRoot = await copyPluginToManagedRoot(this.screamHomeDir, key, backupRoot);
67550
+ await rm(path.join(promotedRoot, ".scream-rollback.json"), {
67551
+ recursive: true,
67552
+ force: true
67553
+ });
67554
+ const promoted = await parseManifest(promotedRoot);
67555
+ const baseRecord = await recordFrom({
67556
+ id: key,
67557
+ root: promotedRoot,
67558
+ enabled: existing.enabled,
67559
+ installedAt: existing.installedAt,
67560
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
67561
+ originalSource: preUpgrade.originalSource ?? existing.originalSource,
67562
+ source: preUpgrade.source ?? existing.source,
67563
+ capabilities: existing.capabilities,
67564
+ github: preUpgrade.github ?? existing.github,
67565
+ parsed: promoted
67566
+ });
67567
+ const record = {
67568
+ ...baseRecord,
67569
+ diagnostics: [...baseRecord.diagnostics, {
67570
+ severity: "info",
67571
+ message: `rolled back to backup ${newest}`
67572
+ }]
67573
+ };
67574
+ this.records.set(key, record);
67575
+ await this.persist();
67576
+ return {
67577
+ record,
67578
+ restoredFrom: backupRoot
67579
+ };
67580
+ }
67581
+ /**
67582
+ * Immune memory: remember the ORIGIN of a circuit-tripped plugin so future
67583
+ * sessions can warn before installing from the same repository again.
67584
+ * Best-effort bookkeeping; unknown/local-less records are silently skipped.
67585
+ */
67586
+ async appendQuarantine(pluginId, reason) {
67587
+ const key = normalizePluginId(pluginId);
67588
+ const record = this.records.get(key);
67589
+ if (record === void 0) return void 0;
67590
+ const sourceKey = sourceKeyFromFields({
67591
+ github: record.github,
67592
+ originalSource: record.originalSource
67593
+ });
67594
+ if (sourceKey === "") return void 0;
67595
+ const entry = {
67596
+ at: (/* @__PURE__ */ new Date()).toISOString(),
67597
+ pluginId: key,
67598
+ ...record.manifest?.name !== void 0 ? { name: record.manifest.name } : {},
67599
+ sourceKey,
67600
+ reason
67601
+ };
67602
+ await appendQuarantine(this.screamHomeDir, entry);
67603
+ return entry;
67604
+ }
67605
+ /** Newest unacknowledged quarantine hit for a raw source string, if any.
67606
+ * Both the verbatim and the realpath-resolved forms are tried, because
67607
+ * temp-dir fixtures and user paths can differ by symlinks (macOS /var).
67608
+ */
67609
+ async matchQuarantineForSource(source) {
67610
+ const entries = await readQuarantine(this.screamHomeDir);
67611
+ for (const key of await this.quarantineKeysFor(source)) {
67612
+ const hit = matchQuarantine(entries, key);
67613
+ if (hit !== void 0) return hit;
67614
+ }
67615
+ }
67616
+ /** Clear the pending warning for a source once an approved install landed. */
67617
+ async acknowledgeQuarantineForSource(source) {
67618
+ for (const key of await this.quarantineKeysFor(source)) await acknowledgeQuarantine(this.screamHomeDir, key);
67619
+ }
67620
+ /** Advisory usage counters (see plugin/stats.ts); unknown ids are ignored. */
67621
+ recordUsage(pluginId, ok) {
67622
+ const key = normalizePluginId(pluginId);
67623
+ if (!this.records.has(key)) return;
67624
+ recordUsageInMemory(this.screamHomeDir, key, ok);
67625
+ }
67626
+ async getUsage(pluginId) {
67627
+ return getUsage(this.screamHomeDir, normalizePluginId(pluginId));
67628
+ }
67629
+ async quarantineKeysFor(source) {
67630
+ const keys = [sourceKeyFromRawSource(source)];
67631
+ try {
67632
+ keys.push(sourceKeyFromRawSource(await realpath(source)));
67633
+ } catch {}
67634
+ return [...new Set(keys.filter((key) => key !== ""))];
67635
+ }
67165
67636
  async setEnabled(id, enabled) {
67166
67637
  const key = normalizePluginId(id);
67167
67638
  const current = this.records.get(key);
@@ -67195,9 +67666,37 @@ var PluginManager = class {
67195
67666
  });
67196
67667
  await this.persist();
67197
67668
  }
67669
+ /**
67670
+ * Flag a plugin as failed at runtime (typically its code entry point threw
67671
+ * while activating). The record keeps the reason as an error diagnostic so
67672
+ * `/plugin info` and listings show why the plugin is not usable.
67673
+ */
67674
+ async markError(id, message) {
67675
+ const key = normalizePluginId(id);
67676
+ const current = this.records.get(key);
67677
+ if (current === void 0) {
67678
+ log.warn("plugin markError for an unknown plugin", {
67679
+ pluginId: id,
67680
+ message
67681
+ });
67682
+ return;
67683
+ }
67684
+ const diagnostic = {
67685
+ severity: "error",
67686
+ message
67687
+ };
67688
+ this.records.set(key, {
67689
+ ...current,
67690
+ state: "error",
67691
+ diagnostics: [...current.diagnostics, diagnostic],
67692
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
67693
+ });
67694
+ await this.persist();
67695
+ }
67198
67696
  async remove(id) {
67199
67697
  const key = normalizePluginId(id);
67200
67698
  if (!this.records.delete(key)) throw new Error(`Plugin "${id}" is not installed`);
67699
+ await forgetStats(this.screamHomeDir, key);
67201
67700
  await this.persist();
67202
67701
  }
67203
67702
  async reload() {
@@ -67286,6 +67785,9 @@ var PluginManager = class {
67286
67785
  version: 1,
67287
67786
  plugins: installed
67288
67787
  });
67788
+ try {
67789
+ await flushStats(this.screamHomeDir);
67790
+ } catch {}
67289
67791
  }
67290
67792
  async materialize(entry) {
67291
67793
  const parsed = await parseManifest(entry.root);
@@ -67336,6 +67838,25 @@ async function copyPluginToManagedRoot(screamHomeDir, id, sourceRoot) {
67336
67838
  }
67337
67839
  return realpath(managedRoot);
67338
67840
  }
67841
+ /**
67842
+ * Keep only the newest backup slot per plugin id: upgrade creates a fresh
67843
+ * timestamped directory each run, so older siblings are removed here to keep
67844
+ * backups bounded. Names sort chronologically because they embed an
67845
+ * ISO-derived stamp.
67846
+ */
67847
+ async function keepNewestBackupOnly(backupsForId) {
67848
+ let names;
67849
+ try {
67850
+ names = await readdir(backupsForId);
67851
+ } catch {
67852
+ return;
67853
+ }
67854
+ const ordered = names.toSorted();
67855
+ for (const name of ordered.slice(0, Math.max(0, ordered.length - 1))) await rm(path.join(backupsForId, name), {
67856
+ recursive: true,
67857
+ force: true
67858
+ });
67859
+ }
67339
67860
  async function recordFrom(input) {
67340
67861
  const { parsed } = input;
67341
67862
  const hasError = parsed.diagnostics.some((d) => d.severity === "error");
@@ -67440,7 +67961,26 @@ function withMcpServerEnabled(config, enabled) {
67440
67961
  };
67441
67962
  }
67442
67963
  function pluginMcpRuntimeName(pluginId, serverName) {
67443
- return `plugin-${pluginId}:${serverName}`;
67964
+ return `${PLUGIN_MCP_RUNTIME_PREFIX}${pluginId}:${serverName}`;
67965
+ }
67966
+ /** Runtime-name prefix that marks an MCP server as contributed by a plugin. */
67967
+ const PLUGIN_MCP_RUNTIME_PREFIX = "plugin-";
67968
+ /**
67969
+ * True when an MCP runtime name belongs to a plugin. Anything else is
67970
+ * user-configured and must never be touched by the plugin hot-apply pass.
67971
+ */
67972
+ function isPluginMcpRuntimeName(name) {
67973
+ return name.startsWith(PLUGIN_MCP_RUNTIME_PREFIX);
67974
+ }
67975
+ /**
67976
+ * The plugin id embedded in a plugin MCP runtime name, or `undefined` for a
67977
+ * user-configured server or a name that is not well-formed.
67978
+ */
67979
+ function pluginIdFromMcpRuntimeName(name) {
67980
+ if (!isPluginMcpRuntimeName(name)) return void 0;
67981
+ const separator = name.indexOf(":");
67982
+ if (separator <= 7) return void 0;
67983
+ return name.slice(7, separator);
67444
67984
  }
67445
67985
  function withPluginMcpRuntime(config, pluginRoot, screamHomeDir) {
67446
67986
  if (config.transport === "http") return config;
@@ -67475,7 +68015,7 @@ function isSafeRelativePath(filePath) {
67475
68015
  return !normalized.startsWith("/");
67476
68016
  }
67477
68017
  async function writePluginSkillPackage(options) {
67478
- const { jian, screamHomeDir, package: pkg } = options;
68018
+ const { jian, screamHomeDir, manager, package: pkg } = options;
67479
68019
  const name = sanitizeSkillName(pkg.name);
67480
68020
  const targetDir = join$1(screamHomeDir, "plugins", "managed", name);
67481
68021
  if (await pathExists$1(jian, targetDir)) throw new ScreamError(ErrorCodes.SKILL_ALREADY_EXISTS, `Skill "${name}" already exists at ${targetDir}. Choose a different name or delete the existing skill first.`);
@@ -67512,14 +68052,21 @@ async function writePluginSkillPackage(options) {
67512
68052
  if (error instanceof ScreamError) throw error;
67513
68053
  throw new ScreamError(ErrorCodes.SKILL_INSTALL_FAILED, `Failed to write skill files to ${targetDir}: ${error instanceof Error ? error.message : String(error)}`);
67514
68054
  }
68055
+ let registered;
67515
68056
  try {
67516
- const manager = new PluginManager({ screamHomeDir });
67517
- await manager.load();
67518
- await manager.registerGenerated(targetDir);
68057
+ if (manager !== void 0) registered = await manager.registerGenerated(targetDir);
68058
+ else {
68059
+ const owned = new PluginManager({ screamHomeDir });
68060
+ await owned.load();
68061
+ registered = await owned.registerGenerated(targetDir);
68062
+ }
67519
68063
  } catch (error) {
67520
68064
  throw new ScreamError(ErrorCodes.SKILL_INSTALL_FAILED, `Failed to register skill in plugin center: ${error instanceof Error ? error.message : String(error)}`);
67521
68065
  }
67522
- return { targetDir };
68066
+ return {
68067
+ targetDir,
68068
+ pluginId: registered.id
68069
+ };
67523
68070
  }
67524
68071
  async function pathExists$1(jian, filePath) {
67525
68072
  try {
@@ -67569,11 +68116,19 @@ var MakeSkillApplyTool = class {
67569
68116
  content: args.content,
67570
68117
  files: args.files ?? []
67571
68118
  };
67572
- return { output: `Skill installed to ${(await writePluginSkillPackage({
68119
+ const result = await writePluginSkillPackage({
67573
68120
  jian: this.agent.jian,
67574
68121
  screamHomeDir,
68122
+ manager: this.agent.toolServices?.plugins,
67575
68123
  package: pkg
67576
- })).targetDir}\n\nYou can manage it in the Skill Center with /plugin, and invoke it with /${sanitizeSkillName(args.name)} in a new session.` };
68124
+ });
68125
+ const sync = this.agent.toolServices?.pluginSync;
68126
+ let hotApplied = false;
68127
+ if (sync !== void 0) try {
68128
+ const report = await sync([result.pluginId], { skipMcpAdd: true });
68129
+ hotApplied = report.ok || report.applied.length > 0;
68130
+ } catch {}
68131
+ return { output: `Skill installed to ${result.targetDir}\n\nYou can manage it in the Skill Center with /plugin, and invoke it with /${sanitizeSkillName(args.name)}` + (hotApplied ? " right away — it was hot-applied to this session." : " in a new session.") };
67577
68132
  } catch (error) {
67578
68133
  if (error instanceof ScreamError) return {
67579
68134
  isError: true,
@@ -74686,6 +75241,796 @@ function truncateUtf8$1(input, maxBytes) {
74686
75241
  return `${input.slice(0, low)}${suffix}`;
74687
75242
  }
74688
75243
  //#endregion
75244
+ //#region ../../packages/agent-core/src/plugin/marketplace.ts
75245
+ /**
75246
+ * Read-only plugin marketplace catalog.
75247
+ *
75248
+ * This module mirrors the shape the TUI consumes so a main agent can browse the
75249
+ * same catalog from inside a session. It intentionally does NOT download or
75250
+ * install anything: it only reads a JSON document and returns display fields.
75251
+ * Nothing here feeds the permission chain — a marketplace `tier` is a label, and
75252
+ * `install` always goes through its own approval gate regardless of it.
75253
+ */
75254
+ /** Env var that overrides the catalog location. Shared name with the TUI copy. */
75255
+ const SCREAM_CODE_PLUGIN_MARKETPLACE_URL_ENV$1 = "SCREAM_CODE_PLUGIN_MARKETPLACE_URL";
75256
+ /** Network budget for a catalog read. The catalog is small; fail fast. */
75257
+ const MARKETPLACE_FETCH_TIMEOUT_MS = 15e3;
75258
+ const PLUGIN_MARKETPLACE_TIERS$1 = ["official", "curated"];
75259
+ /**
75260
+ * Load and parse the marketplace catalog.
75261
+ *
75262
+ * Throws on an unreadable or malformed catalog; callers surface the failure as a
75263
+ * tool error with an actionable `next`.
75264
+ */
75265
+ async function loadPluginMarketplace$1(options = {}) {
75266
+ const location = resolveMarketplaceLocation$1(options.source ?? process.env["SCREAM_CODE_PLUGIN_MARKETPLACE_URL"] ?? "https://raw.githubusercontent.com/LIUTod/scream-code/main/plugins/marketplace.json", options.workDir);
75267
+ return parsePluginMarketplace$1(await readMarketplaceText$1(location, options.fetchImpl ?? fetch), location);
75268
+ }
75269
+ /**
75270
+ * Case-insensitive substring filter over the searchable fields.
75271
+ *
75272
+ * An empty/omitted query returns every entry, so callers can pass the raw
75273
+ * argument straight through.
75274
+ */
75275
+ function filterMarketplaceEntries(entries, query) {
75276
+ const needle = query?.trim().toLowerCase();
75277
+ if (needle === void 0 || needle.length === 0) return entries;
75278
+ return entries.filter((entry) => {
75279
+ return [
75280
+ entry.id,
75281
+ entry.displayName,
75282
+ entry.description ?? "",
75283
+ ...entry.keywords ?? []
75284
+ ].join(" ").toLowerCase().includes(needle);
75285
+ });
75286
+ }
75287
+ function resolveMarketplaceLocation$1(source, workDir) {
75288
+ const trimmed = source.trim();
75289
+ if (trimmed.length === 0) throw new Error(`Marketplace source is empty (${SCREAM_CODE_PLUGIN_MARKETPLACE_URL_ENV$1} must not be blank)`);
75290
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return {
75291
+ raw: trimmed,
75292
+ kind: "remote",
75293
+ resolved: trimmed
75294
+ };
75295
+ if (trimmed.startsWith("file://")) return {
75296
+ raw: trimmed,
75297
+ kind: "local",
75298
+ resolved: fileURLToPath(trimmed)
75299
+ };
75300
+ return {
75301
+ raw: trimmed,
75302
+ kind: "local",
75303
+ resolved: resolveLocalPath$1(trimmed, workDir)
75304
+ };
75305
+ }
75306
+ async function readMarketplaceText$1(location, fetchImpl) {
75307
+ if (location.kind === "local") try {
75308
+ return await readFile(location.resolved, "utf8");
75309
+ } catch (error) {
75310
+ throw new Error(`Cannot read the marketplace file at ${location.resolved}: ${messageOf(error)}`, { cause: error });
75311
+ }
75312
+ const response = await fetchImpl(location.resolved, { signal: AbortSignal.timeout(MARKETPLACE_FETCH_TIMEOUT_MS) });
75313
+ if (!response.ok) throw new Error(`The marketplace at ${location.resolved} returned HTTP ${String(response.status)}`);
75314
+ return response.text();
75315
+ }
75316
+ /**
75317
+ * Parse the catalog document.
75318
+ *
75319
+ * The array key is tolerated as either `entries` or `plugins` because both
75320
+ * shapes are in the wild. Per-entry fields are optional except `source`: without
75321
+ * one there is nothing to install. `id` falls back to the manifest name so a
75322
+ * hand-written catalog without ids still browses.
75323
+ */
75324
+ function parsePluginMarketplace$1(raw, location) {
75325
+ let parsed;
75326
+ try {
75327
+ parsed = JSON.parse(raw);
75328
+ } catch (error) {
75329
+ throw new Error(`The marketplace at ${location.resolved} is not valid JSON: ${messageOf(error)}`, { cause: error });
75330
+ }
75331
+ if (!isRecord$3(parsed)) throw new TypeError(`The marketplace at ${location.resolved} must be a JSON object.`);
75332
+ const rawEntries = rawEntriesOf(parsed);
75333
+ if (rawEntries === void 0) throw new TypeError(`The marketplace at ${location.resolved} must contain an "entries" (or "plugins") array.`);
75334
+ const entries = [];
75335
+ rawEntries.forEach((value, index) => {
75336
+ const entry = parseMarketplaceEntry$1(value, index, location);
75337
+ if (entry !== void 0) entries.push(entry);
75338
+ });
75339
+ return {
75340
+ source: location.resolved,
75341
+ version: stringField$1(parsed, "version"),
75342
+ entries
75343
+ };
75344
+ }
75345
+ function rawEntriesOf(parsed) {
75346
+ for (const key of ["entries", "plugins"]) {
75347
+ const value = parsed[key];
75348
+ if (Array.isArray(value)) return value;
75349
+ }
75350
+ }
75351
+ function parseMarketplaceEntry$1(value, index, location) {
75352
+ if (!isRecord$3(value)) return void 0;
75353
+ const source = stringField$1(value, "source") ?? stringField$1(value, "url") ?? stringField$1(value, "downloadUrl");
75354
+ if (source === void 0) return;
75355
+ const id = stringField$1(value, "id") ?? stringField$1(value, "name") ?? `entry-${String(index + 1)}`;
75356
+ return {
75357
+ id,
75358
+ displayName: stringField$1(value, "displayName") ?? stringField$1(value, "name") ?? id,
75359
+ source: resolveEntrySource$1(source, location),
75360
+ tier: parseMarketplaceTier$1(value),
75361
+ version: stringField$1(value, "version"),
75362
+ description: stringField$1(value, "description") ?? stringField$1(value, "shortDescription"),
75363
+ homepage: stringField$1(value, "homepage") ?? stringField$1(value, "websiteURL"),
75364
+ keywords: stringArrayField$1(value, "keywords") ?? stringArrayField$1(value, "tags")
75365
+ };
75366
+ }
75367
+ function parseMarketplaceTier$1(value) {
75368
+ const raw = value["tier"];
75369
+ if (typeof raw !== "string") return void 0;
75370
+ const tier = raw.trim();
75371
+ return PLUGIN_MARKETPLACE_TIERS$1.includes(tier) ? tier : void 0;
75372
+ }
75373
+ function resolveEntrySource$1(source, location) {
75374
+ const trimmed = source.trim();
75375
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("~/") || trimmed === "~" || isAbsolute(trimmed)) return trimmed;
75376
+ if (trimmed.startsWith("file://")) return fileURLToPath(trimmed);
75377
+ if (location.kind === "remote") return new URL(trimmed, location.resolved).toString();
75378
+ return resolve(dirname$1(location.resolved), trimmed);
75379
+ }
75380
+ function resolveLocalPath$1(input, workDir) {
75381
+ if (input === "~") return homedir();
75382
+ if (input.startsWith("~/")) return join(homedir(), input.slice(2));
75383
+ return isAbsolute(input) ? input : resolve(workDir ?? process.cwd(), input);
75384
+ }
75385
+ function stringField$1(value, field) {
75386
+ const raw = value[field];
75387
+ if (typeof raw !== "string") return void 0;
75388
+ const trimmed = raw.trim();
75389
+ return trimmed.length > 0 ? trimmed : void 0;
75390
+ }
75391
+ function stringArrayField$1(value, field) {
75392
+ const raw = value[field];
75393
+ if (!Array.isArray(raw)) return void 0;
75394
+ const out = raw.filter((item) => typeof item === "string").map((item) => item.trim()).filter((item) => item.length > 0);
75395
+ return out.length > 0 ? out : void 0;
75396
+ }
75397
+ function isRecord$3(value) {
75398
+ return typeof value === "object" && value !== null && !Array.isArray(value);
75399
+ }
75400
+ function messageOf(error) {
75401
+ return error instanceof Error ? error.message : String(error);
75402
+ }
75403
+ //#endregion
75404
+ //#region ../../packages/agent-core/src/tools/builtin/plugin/manage-plugin.md
75405
+ var manage_plugin_default = "Manage this product's own plugins. Plugins are the vehicle for add-on capabilities: each one can contribute skills, MCP servers, shell hooks, and — only after an explicit activate — code that runs in this process.\n\nUse this tool in two situations: (a) a task needs a capability you do not currently have — exhaust your existing tools first and add a plugin only when the gap is real; (b) the user asks you to find, install, configure, inspect, or clean up a capability (e.g. \"install something that can render mermaid\", \"what plugins do you have\"). In both cases, remove or disable additions again when the work is done unless the capability is broadly reusable.\n\nChoose the lightest vehicle that solves the problem, in this order: a skill (prompt instructions — safest, no code, no process), an MCP server (an external process), a code plugin (runs in this process — the most powerful, needs its own approved `activate`).\n\n## Workflow\n\n1. `{\"action\":\"list\"}` — see what is already installed before adding anything. Do not grow a second plugin that duplicates an existing capability; enable, disable, or extend the one you already have.\n2. `{\"action\":\"marketplace\",\"query\":\"pdf\"}` — browse the catalog for a matching capability. Pass `source` to read a different catalog. `tier` is a display label only; judge an entry by its description and source. Entries whose origin previously produced a tripped plugin carry `quarantined: true`.\n3. `{\"action\":\"precheck\",\"source\":\"https://github.com/owner/repo\"}` — free read-only origin check before installing anything external that you have not vetted in this session: it reports past incidents recorded by the plugin center's immune memory (`quarantined:true` + the reason). Surface such a warning to the user BEFORE asking for the install approval.\n4. If nothing in the catalog fits and precheck is clean, you may search the web (`WebSearch` / `FetchURL`) and evaluate a repository yourself: read its README and manifest, prefer a pinned branch/tag/commit URL over a moving default, and check that it does something you cannot already do.\n5. To **make a capability yourself**: if the user invoked `/make-skill`, use the `MakeSkillPlan` → `MakeSkillApply` pair (it produces a valid skill package and registers it into this table automatically). Otherwise build a plugin directory by hand (a `scream.plugin.json` manifest plus your SKILL.md under `skills/`) and register it with `register_generated` — that route lands in this table, hot-applies the same turn, and stays manageable here (disable/remove/circuit). Writing a plain SKILL.md into the user skills directory is also a legitimate skill, but it does NOT appear in this table and cannot be managed with this tool.\n6. `{\"action\":\"install\",\"source\":\"https://github.com/owner/repo/tree/v1.2.0\"}` — install from a local path, a GitHub URL, or a zip URL. **Installing never executes code.** The plugin's files are copied and its record is registered; a manifest `entryPoint` stays dormant until step 8.\n7. `{\"action\":\"install\",\"source\":\"...\",\"upgrade\":true}` — same-name refresh: backs up the current files (single slot under `plugins/backups/<id>/`), swaps in the new ones, and reloads; `from`/`to` versions and any downgrade warning are reported. If activation then fails, `rollback` restores the previous files in one approved step.\n8. `{\"action\":\"enable\",\"id\":\"...\"}` / `{\"action\":\"disable\",\"id\":\"...\"}` / `{\"action\":\"set_mcp_enabled\",\"id\":\"...\",\"server\":\"...\",\"enabled\":false}` — turn contributed capabilities on and off.\n9. `{\"action\":\"activate\",\"id\":\"...\"}` — the only action that runs a plugin's code entry point in this process. Skip it for skill-only and MCP-only plugins; they need no activation.\n10. `{\"action\":\"info\",\"id\":\"...\"}` / `{\"action\":\"check\"}` — inspect one record in full, or the health of the table. Each entry also carries `usage: {calls, okCalls, okRate, lastUsedAt}` from this machine: zero calls since install marks it as a cleanup candidate; high usage with a high okRate is worth keeping when you must choose.\n11. `{\"action\":\"reset\",\"id\":\"...\"}` — recover a circuit-tripped or manually disabled plugin: clears its failure ledger, re-derives the record from disk, enables it, and hot-applies its capabilities. It never runs code — a code plugin still needs its own approved `activate`.\n12. `{\"action\":\"rollback\",\"id\":\"...\"}` — restore the pre-upgrade backup taken by step 7 in one approved step.\n13. `{\"action\":\"reload\"}` — re-read the plugin table from disk and get an added/removed/error summary.\n14. `{\"action\":\"remove\",\"id\":\"...\"}` (or `disable` to keep the record) — retreat once the capability is no longer wanted.\n\n## Circuit breaker (limbs that fail get pulled, the loop never dies)\n\nPlugin-owned capabilities are counted per plugin: every failed tool call (in-process plugin tools and plugin MCP servers) and every faulty event handler. A success clears the streak; three consecutive failures trip the breaker, and the failing result arrives with a `[circuit]` advisory: the plugin was marked with the reason, disabled persistently, its code deactivated, and its tools, MCP servers, skills, and hooks pulled from every live session. Nothing retries automatically — read the state with `check` (each plugin carries `circuit: {failures, tripped}`), then `reset` to give it another chance or `remove` to let it go. User-configured MCP servers and built-in tools are never charged to anyone's breaker.\n\nEvery trip ALSO writes the origin into the plugin center's immune memory (quarantine ledger): future `precheck`/`install`/`marketplace` calls for the same repository will surface that history before anything is executed again, even across sessions — and even if the broken plugin was removed afterwards.\n\nChanges are hot-applied to the running session wherever the host supports it: mutating results carry a `sync` report (`{ok, applied[], failed[]}`) naming exactly what landed — `skills.inject`, `mcp.add`/`mcp.remove`, tool teardown. An `install` deliberately reports no `mcp.add`: installing never starts a plugin's MCP process; that waits for an approved `enable`/`activate`/`reload`. Treat `sync.failed` as advisory (the mutation itself already succeeded) and re-check with `info`/`check` rather than re-running the mutation. Reload before judging whether a change landed.\n\n## Registration of your own output\n\n`{\"action\":\"register_generated\",\"source\":\"/abs/path/to/generated/plugin\"}` registers a plugin directory you produced locally, without a copy step. It refuses any manifest that declares an `entryPoint`: this action is normally auto-approved and must never become a route to running code. For a code plugin, use `install` and then `activate`.\n\n## Approval contract\n\n- Read-only actions (`list`, `info`, `check`, `marketplace`) run without prompting.\n- Every mutating action (`install`, `register_generated`, `enable`, `disable`, `set_mcp_enabled`, `activate`, `deactivate`, `remove`, `reset`, `reload`) requires the user's approval. State what you are about to do and why in one line, then call the tool — never describe a plugin change you have not made.\n- The approval prompt for `install` and `register_generated` shows the full source string, and a session grant applies to that exact source only.\n- `activate` is the step that executes third-party code; treat it as its own decision, made after you have read what the plugin does.\n- A rejected approval is an answer, not a retry cue. Report the refusal and continue without the capability.\n\n## Output\n\nEvery action returns a JSON string. Success is a compact result object; failure is\n`{\"error\":{\"code\",\"message\",\"next\"}}` with `isError: true`. Follow `next` rather than\nguessing at a repair — it names the call that resolves the problem.\n";
75406
+ //#endregion
75407
+ //#region ../../packages/agent-core/src/tools/builtin/plugin/manage-plugin.ts
75408
+ /** Every action this tool understands. */
75409
+ const MANAGE_PLUGIN_ACTIONS = [
75410
+ "list",
75411
+ "info",
75412
+ "check",
75413
+ "marketplace",
75414
+ "precheck",
75415
+ "install",
75416
+ "register_generated",
75417
+ "enable",
75418
+ "disable",
75419
+ "set_mcp_enabled",
75420
+ "activate",
75421
+ "deactivate",
75422
+ "remove",
75423
+ "reset",
75424
+ "rollback",
75425
+ "reload"
75426
+ ];
75427
+ /**
75428
+ * Actions that only read plugin state. The permission policy auto-approves
75429
+ * exactly this set; nothing here may write state or run plugin code.
75430
+ */
75431
+ const MANAGE_PLUGIN_READ_ONLY_ACTIONS = [
75432
+ "list",
75433
+ "info",
75434
+ "check",
75435
+ "marketplace",
75436
+ "precheck"
75437
+ ];
75438
+ const ManagePluginInputSchema = z.object({
75439
+ action: z.enum(MANAGE_PLUGIN_ACTIONS).describe("Which plugin operation to run."),
75440
+ id: z.string().optional().describe("Plugin id. Required for info, check, enable, disable, set_mcp_enabled, activate, deactivate, remove, reset."),
75441
+ source: z.string().optional().describe("install: absolute path / GitHub URL / zip URL. register_generated: the generated plugin directory. marketplace: optional catalog URL or local JSON path."),
75442
+ upgrade: z.boolean().optional().describe("install onto an existing plugin id: back up its current files, replace with the incoming ones, and reload. Without it, installing over an existing id silently overwrites."),
75443
+ server: z.string().optional().describe("MCP server name, for set_mcp_enabled."),
75444
+ enabled: z.boolean().optional().describe("Target state, for set_mcp_enabled."),
75445
+ query: z.string().optional().describe("Case-insensitive substring filter, for marketplace.")
75446
+ });
75447
+ /**
75448
+ * The two actions that bring third-party code in. Their approval identity
75449
+ * includes the source, so a "approve for session" grant cannot be replayed
75450
+ * against a different URL later in the same session.
75451
+ */
75452
+ const SOURCE_SCOPED_ACTIONS = new Set(["install", "register_generated"]);
75453
+ /**
75454
+ * Actions that cannot be answered without naming a plugin. `check` is not here:
75455
+ * without an id it reports the health of the whole table.
75456
+ */
75457
+ const NEEDS_ID = new Set([
75458
+ "info",
75459
+ "enable",
75460
+ "disable",
75461
+ "set_mcp_enabled",
75462
+ "activate",
75463
+ "deactivate",
75464
+ "remove",
75465
+ "reset",
75466
+ "rollback"
75467
+ ]);
75468
+ function isKnownAction(action) {
75469
+ return MANAGE_PLUGIN_ACTIONS.includes(action);
75470
+ }
75471
+ function isReadOnlyAction(action) {
75472
+ return MANAGE_PLUGIN_READ_ONLY_ACTIONS.includes(action);
75473
+ }
75474
+ function isSourceScoped(action) {
75475
+ return SOURCE_SCOPED_ACTIONS.has(action);
75476
+ }
75477
+ function fail$1(code, message, next) {
75478
+ return { error: {
75479
+ code,
75480
+ message,
75481
+ next
75482
+ } };
75483
+ }
75484
+ function errorMessage$3(error) {
75485
+ return error instanceof Error ? error.message : String(error);
75486
+ }
75487
+ function result(payload, isError = false) {
75488
+ return isError ? {
75489
+ output: JSON.stringify(payload),
75490
+ isError: true
75491
+ } : { output: JSON.stringify(payload) };
75492
+ }
75493
+ /**
75494
+ * Same payload as {@link result} but typed as the error variant, which is what
75495
+ * `resolveExecution` may return to reject a call before it reaches approval.
75496
+ */
75497
+ function preflightError(envelope) {
75498
+ return {
75499
+ output: JSON.stringify(envelope),
75500
+ isError: true
75501
+ };
75502
+ }
75503
+ var ManagePluginTool = class {
75504
+ agent;
75505
+ name = "ManagePlugin";
75506
+ description = manage_plugin_default;
75507
+ parameters = toInputJsonSchema(ManagePluginInputSchema);
75508
+ constructor(agent) {
75509
+ this.agent = agent;
75510
+ }
75511
+ resolveExecution(args) {
75512
+ if (!isKnownAction(args.action)) return preflightError(fail$1("unknown_action", `Unknown ManagePlugin action "${String(args.action)}".`, `Pick one of: ${MANAGE_PLUGIN_ACTIONS.join(", ")}.`));
75513
+ const missing = missingRequiredArgs(args);
75514
+ if (missing !== void 0) return preflightError(missing);
75515
+ const subject = ruleSubject(args);
75516
+ return {
75517
+ accesses: isReadOnlyAction(args.action) ? ToolAccesses.none() : ToolAccesses.all(),
75518
+ description: approvalDescription(args),
75519
+ approvalRule: literalRulePattern(this.name, subject),
75520
+ matchesRule: (ruleArgs) => matchesGlobRuleSubject(ruleArgs, subject),
75521
+ execute: () => this.run(args)
75522
+ };
75523
+ }
75524
+ /**
75525
+ * Single funnel for every action. Contract: JSON in `output`, failures as
75526
+ * `isError` with `{error:{code,message,next}}`, and no exception ever
75527
+ * escapes — a plugin-center bug must not surface as a tool crash.
75528
+ */
75529
+ async run(args) {
75530
+ const manager = this.agent.toolServices?.plugins;
75531
+ if (manager === void 0) return result(fail$1("plugin_center_unavailable", "The shared plugin manager is not available in this runtime.", "Report this so the host can expose its PluginManager, or use the /plugin command."), true);
75532
+ try {
75533
+ switch (args.action) {
75534
+ case "list": return result({
75535
+ action: "list",
75536
+ count: manager.list().length,
75537
+ plugins: manager.list().map(listView),
75538
+ note: "Enabled plugins contribute skills and MCP servers; a code plugin only runs after a separate activate."
75539
+ });
75540
+ case "info": return this.info(manager, args.id);
75541
+ case "check": return await this.check(manager, args.id);
75542
+ case "precheck": return await this.precheck(args.source);
75543
+ case "marketplace": return await this.marketplace(args.source, args.query);
75544
+ case "install": return await this.install(manager, args.source, args.upgrade === true);
75545
+ case "register_generated": return await this.registerGenerated(manager, args.source);
75546
+ case "enable":
75547
+ case "disable": {
75548
+ const record = manager.get(args.id);
75549
+ if (record === void 0) return result(notFound(args.id, manager), true);
75550
+ const enabled = args.action === "enable";
75551
+ await manager.setEnabled(record.id, enabled);
75552
+ const sync = await this.sync([record.id]);
75553
+ return result({
75554
+ action: args.action,
75555
+ id: record.id,
75556
+ enabled,
75557
+ record: listView(manager.get(record.id)),
75558
+ ...sync !== void 0 ? { sync } : {},
75559
+ note: enabled ? "Enabled; skills and MCP servers hot-applied when sync reports them. Code still needs a separate activate." : "Disabled; its skills, MCP servers, and plugin tools were pulled from live sessions."
75560
+ });
75561
+ }
75562
+ case "set_mcp_enabled": {
75563
+ const record = manager.get(args.id);
75564
+ if (record === void 0) return result(notFound(args.id, manager), true);
75565
+ await manager.setMcpServerEnabled(record.id, args.server, args.enabled);
75566
+ const info = manager.info(record.id);
75567
+ const sync = await this.sync([record.id]);
75568
+ return result({
75569
+ action: args.action,
75570
+ id: record.id,
75571
+ server: args.server,
75572
+ enabled: args.enabled,
75573
+ mcpServers: info?.mcpServers ?? [],
75574
+ ...sync !== void 0 ? { sync } : {},
75575
+ note: "MCP server state changed; live sessions were reconciled when sync reports mcp.add/mcp.remove."
75576
+ });
75577
+ }
75578
+ case "activate": return await this.activate(manager, args.id);
75579
+ case "deactivate": return await this.deactivate(manager, args.id);
75580
+ case "remove": return await this.remove(manager, args.id);
75581
+ case "reset": return await this.reset(manager, args.id);
75582
+ case "rollback": return await this.rollbackPlugin(manager, args.id);
75583
+ case "reload": {
75584
+ const summary = await manager.reload();
75585
+ const sync = await this.sync();
75586
+ return result({
75587
+ action: "reload",
75588
+ ...summary,
75589
+ ...sync !== void 0 ? { sync } : {}
75590
+ });
75591
+ }
75592
+ default: return result(fail$1("unknown_action", `Unknown ManagePlugin action "${String(args.action)}".`, `Pick one of: ${MANAGE_PLUGIN_ACTIONS.join(", ")}.`), true);
75593
+ }
75594
+ } catch (error) {
75595
+ return result(fail$1("plugin_operation_failed", errorMessage$3(error), nextForError(args, error)), true);
75596
+ }
75597
+ }
75598
+ info(manager, id) {
75599
+ const info = manager.info(id);
75600
+ if (info === void 0) return result(notFound(id, manager), true);
75601
+ return result({
75602
+ action: "info",
75603
+ plugin: projectInfo(info),
75604
+ active: this.runtime?.isActive(info.id) === true
75605
+ });
75606
+ }
75607
+ /**
75608
+ * Health view built from the diagnostics each record already carries. There
75609
+ * is no persistent reload log, so none is invented: `reload` is what returns
75610
+ * a fresh summary after re-reading the table from disk.
75611
+ */
75612
+ async check(manager, id) {
75613
+ if (id !== void 0 && manager.get(id) === void 0) return result(notFound(id, manager), true);
75614
+ const records = id === void 0 ? manager.list() : [manager.get(id)];
75615
+ const plugins = await Promise.all(records.map(async (record) => ({
75616
+ ...checkView(record, this.runtime?.isActive(record.id) === true),
75617
+ circuit: this.circuitView(record.id),
75618
+ usage: await this.usageView(record.id)
75619
+ })));
75620
+ const unhealthy = plugins.filter((plugin) => plugin.errors.length > 0 || plugin.state === "error");
75621
+ return result({
75622
+ action: "check",
75623
+ checked: plugins.length,
75624
+ healthy: unhealthy.length === 0,
75625
+ unhealthy: unhealthy.map((plugin) => plugin.id),
75626
+ plugins,
75627
+ note: "Diagnostics are the ones stored on each record from the last load or reload. Run action:\"reload\" to re-read the plugin table from disk and get a fresh summary."
75628
+ });
75629
+ }
75630
+ async marketplace(source, query) {
75631
+ try {
75632
+ const catalog = await loadPluginMarketplace$1({
75633
+ source,
75634
+ workDir: this.agent.config.cwd
75635
+ });
75636
+ const filtered = filterMarketplaceEntries(catalog.entries, query);
75637
+ const quarantinedIds = /* @__PURE__ */ new Set();
75638
+ for (const entry of filtered) if (await this.matchQuarantine(entry.source) !== void 0) quarantinedIds.add(entry.id);
75639
+ const entries = filtered.map((entry) => ({
75640
+ ...entry,
75641
+ ...quarantinedIds.has(entry.id) ? { quarantined: true } : {}
75642
+ }));
75643
+ return result({
75644
+ action: "marketplace",
75645
+ catalog: catalog.source,
75646
+ version: catalog.version,
75647
+ query: query ?? null,
75648
+ count: entries.length,
75649
+ entries,
75650
+ note: "Catalog metadata only. \"tier\" is a display label and never skips the approval prompt; nothing was downloaded or installed here."
75651
+ });
75652
+ } catch (error) {
75653
+ return result(fail$1("marketplace_unavailable", `Could not load the plugin marketplace: ${errorMessage$3(error)}`, "Check the URL or file path, or pass one explicitly: {action:\"marketplace\", source:\"<https url or local json path>\"}."), true);
75654
+ }
75655
+ }
75656
+ /**
75657
+ * Install only lands files and registers the record. Nothing is imported or
75658
+ * run: activation stays a separate, separately-approved action.
75659
+ */
75660
+ async precheck(source) {
75661
+ const entry = await this.agent.toolServices?.plugins?.matchQuarantineForSource?.(source) ?? void 0;
75662
+ return result({
75663
+ action: "precheck",
75664
+ source,
75665
+ quarantined: entry !== void 0,
75666
+ ...entry !== void 0 ? { entry: {
75667
+ at: entry.at,
75668
+ pluginId: entry.pluginId,
75669
+ reason: entry.reason
75670
+ } } : {},
75671
+ note: entry === void 0 ? "No unacknowledged incident on record for this origin." : "This origin previously produced a circuit-tripped plugin; proceed only with explicit user consent."
75672
+ });
75673
+ }
75674
+ /** Quarantine lookup helper tolerant of hosts without the ledger. */
75675
+ async matchQuarantine(source) {
75676
+ return await this.agent.toolServices?.plugins?.matchQuarantineForSource?.(source) ?? void 0;
75677
+ }
75678
+ /** Acknowledge the quarantine warning after an approved install landed. */
75679
+ async acknowledgeQuarantine(source) {
75680
+ try {
75681
+ await this.agent.toolServices?.plugins?.acknowledgeQuarantineForSource?.(source);
75682
+ } catch {}
75683
+ }
75684
+ async install(manager, source, upgrade = false) {
75685
+ const quarantineHit = await this.agent.toolServices?.plugins?.matchQuarantineForSource?.(source) ?? void 0;
75686
+ const quarantineNote = quarantineHit !== void 0 ? { quarantine: {
75687
+ at: quarantineHit.at,
75688
+ reason: quarantineHit.reason
75689
+ } } : {};
75690
+ if (upgrade) {
75691
+ const upgraded = await manager.upgrade(source);
75692
+ const sync = await this.sync([upgraded.record.id], { skipMcpAdd: true });
75693
+ await this.acknowledgeQuarantine(source);
75694
+ return result({
75695
+ action: "install",
75696
+ id: upgraded.record.id,
75697
+ installed: true,
75698
+ upgraded: true,
75699
+ from: upgraded.from,
75700
+ to: upgraded.to,
75701
+ ...upgraded.warn !== void 0 ? { warn: upgraded.warn } : {},
75702
+ backupPath: upgraded.backupPath,
75703
+ ...quarantineNote,
75704
+ codeExecuted: false,
75705
+ hasCodeEntryPoint: upgraded.record.manifest?.entryPoint !== void 0,
75706
+ record: listView(upgraded.record),
75707
+ ...sync !== void 0 ? { sync } : {},
75708
+ message: "files replaced from backup-safe upgrade; previous contents restorable via {action:\"rollback\"}; skills live now, MCP servers start after an approved enable/activate" + (quarantineHit !== void 0 ? ` \u26A0 origin previously tripped (${quarantineHit.reason})` : "")
75709
+ });
75710
+ }
75711
+ const record = await manager.install(source);
75712
+ const sync = await this.sync([record.id], { skipMcpAdd: true });
75713
+ await this.acknowledgeQuarantine(source);
75714
+ return result({
75715
+ action: "install",
75716
+ id: record.id,
75717
+ installed: true,
75718
+ activated: false,
75719
+ codeExecuted: false,
75720
+ hasCodeEntryPoint: record.manifest?.entryPoint !== void 0,
75721
+ record: listView(record),
75722
+ ...quarantineNote,
75723
+ ...sync !== void 0 ? { sync } : {},
75724
+ message: "installed, code not executed yet — its skills are live now, MCP servers start after an approved activate/enable; run activate to enable its code tools" + (quarantineHit !== void 0 ? ` \u26A0 origin previously tripped (${quarantineHit.reason})` : "")
75725
+ });
75726
+ }
75727
+ /**
75728
+ * Register a locally generated plugin directory. A manifest with an
75729
+ * `entryPoint` is refused: the generated-skill flow is auto-approved, so it
75730
+ * must never become a channel for running unapproved code.
75731
+ */
75732
+ async registerGenerated(manager, source) {
75733
+ const parsed = await parseManifest(source);
75734
+ if (parsed.manifest === void 0) return result(fail$1("manifest_invalid", `Cannot register "${source}" as a generated plugin: ${parsed.diagnostics.find((diagnostic) => diagnostic.severity === "error")?.message ?? "no plugin manifest found"}`, "Write a valid plugin manifest in that directory first (see the MakeSkillPlan / MakeSkillApply flow)."), true);
75735
+ if (parsed.manifest.entryPoint !== void 0) return result(fail$1("code_entry_point_not_allowed", `Refusing to register "${parsed.manifest.name}": its manifest declares an entryPoint, which would run code through a path that is normally auto-approved.`, "Install it from its source instead, then activate it as a separate approved step: {action:\"install\", source:\"<path or url>\"} followed by {action:\"activate\", id:\"<plugin id>\"}."), true);
75736
+ const record = await manager.registerGenerated(source);
75737
+ const sync = await this.sync([record.id], { skipMcpAdd: true });
75738
+ return result({
75739
+ action: "register_generated",
75740
+ id: record.id,
75741
+ registered: true,
75742
+ enabled: record.enabled,
75743
+ hasCodeEntryPoint: false,
75744
+ record: listView(record),
75745
+ ...sync !== void 0 ? { sync } : {},
75746
+ note: "Registered in place; its skills are hot-applied to this session when sync reports skills.inject."
75747
+ });
75748
+ }
75749
+ async activate(manager, id) {
75750
+ const record = manager.get(id);
75751
+ if (record === void 0) return result(notFound(id, manager), true);
75752
+ const runtime = this.runtime;
75753
+ if (runtime === void 0) return result(fail$1("extension_runtime_unavailable", "This runtime has no extension manager, so plugin code cannot be activated.", "The plugin stays installed; use the /plugin command or report this so the host can expose an ExtensionRuntime."), true);
75754
+ if (runtime.isActive(record.id)) return result({
75755
+ action: "activate",
75756
+ id: record.id,
75757
+ active: true,
75758
+ alreadyActive: true,
75759
+ note: "Nothing ran again — the plugin is already live in this process."
75760
+ });
75761
+ const [extension] = runtime.discover([record]);
75762
+ if (extension === void 0) return result(fail$1("no_code_entry_point", `Plugin "${record.id}" has no manifest entryPoint, so there is no code to activate.`, `Enable it instead: {action:"enable", id:"${record.id}"}.`), true);
75763
+ try {
75764
+ await runtime.activate(this.agent, extension);
75765
+ } catch (error) {
75766
+ await manager.markError(record.id, errorMessage$3(error)).catch(() => void 0);
75767
+ const upgradedRecently = record.diagnostics.some((d) => d.message.startsWith("upgraded "));
75768
+ return result(fail$1("activation_failed", `Failed to activate "${record.id}": ${errorMessage$3(error)}`, `Inspect {action:"info", id:"${record.id}"} for the stored diagnostics; fix it, or ${upgradedRecently ? `{action:"rollback", id:"${record.id}"} to restore the pre-upgrade files, or ` : ""}{action:"remove", id:"${record.id}"}.`), true);
75769
+ }
75770
+ const sync = await this.sync([record.id]);
75771
+ return result({
75772
+ action: "activate",
75773
+ id: record.id,
75774
+ active: true,
75775
+ entryPoint: extension.entryPoint,
75776
+ enabled: record.enabled,
75777
+ ...sync !== void 0 ? { sync } : {},
75778
+ ...record.enabled ? {} : { warning: "The plugin record is disabled; enable it so its capabilities survive the next reload." }
75779
+ });
75780
+ }
75781
+ async deactivate(manager, id) {
75782
+ const record = manager.get(id);
75783
+ if (record === void 0) return result(notFound(id, manager), true);
75784
+ const runtime = this.runtime;
75785
+ if (runtime === void 0) return result(fail$1("extension_runtime_unavailable", "This runtime has no extension manager to deactivate from.", "Use the /plugin command or report this so the host can expose an ExtensionRuntime."), true);
75786
+ const wasActive = runtime.isActive(record.id);
75787
+ await runtime.deactivate(record.id);
75788
+ const toolsUnregistered = this.agent.services?.tools?.unregisterToolsByOwner?.(record.id) ?? 0;
75789
+ return result({
75790
+ action: "deactivate",
75791
+ id: record.id,
75792
+ active: false,
75793
+ wasActive,
75794
+ toolsUnregistered,
75795
+ ...wasActive ? {} : { note: "The plugin was not active; nothing to unwind." }
75796
+ });
75797
+ }
75798
+ async remove(manager, id) {
75799
+ const record = manager.get(id);
75800
+ if (record === void 0) return result(notFound(id, manager), true);
75801
+ const runtime = this.runtime;
75802
+ const wasActive = runtime?.isActive(record.id) === true;
75803
+ if (wasActive && runtime !== void 0) await runtime.deactivate(record.id);
75804
+ await manager.remove(record.id);
75805
+ const sync = await this.sync([record.id]);
75806
+ return result({
75807
+ action: "remove",
75808
+ id: record.id,
75809
+ removed: true,
75810
+ deactivatedFirst: wasActive,
75811
+ ...sync !== void 0 ? { sync } : {},
75812
+ note: "The plugin record is gone; files on disk were not deleted."
75813
+ });
75814
+ }
75815
+ get runtime() {
75816
+ return this.agent.toolServices?.extensionRuntime;
75817
+ }
75818
+ /** Live circuit-breaker state for one plugin, read from the agent ledger. */
75819
+ circuitView(id) {
75820
+ const tools = this.agent.services?.tools;
75821
+ return {
75822
+ failures: tools?.getCircuitFailures?.(id) ?? 0,
75823
+ tripped: tools?.isCircuitTripped?.(id) ?? false
75824
+ };
75825
+ }
75826
+ /** Advisory usage counters from the host sidecar, formatted for the model. */
75827
+ async usageView(id) {
75828
+ const usage = await this.agent.toolServices?.plugins?.getUsage?.(id) ?? {
75829
+ calls: 0,
75830
+ okCalls: 0,
75831
+ lastUsedAt: void 0
75832
+ };
75833
+ const okRate = usage.calls === 0 ? null : Math.round(usage.okCalls / usage.calls * 100);
75834
+ return {
75835
+ calls: usage.calls,
75836
+ okCalls: usage.okCalls,
75837
+ okRate,
75838
+ lastUsedAt: usage.lastUsedAt
75839
+ };
75840
+ }
75841
+ /**
75842
+ * Recover a circuit-tripped (or otherwise disabled) plugin: clear the
75843
+ * failure ledger, re-derive the record state from disk, enable it, and hot
75844
+ * -apply its capabilities. A code entry point still needs a separate,
75845
+ * approved `activate` — reset never runs plugin code.
75846
+ */
75847
+ async reset(manager, id) {
75848
+ const record = manager.get(id);
75849
+ if (record === void 0) return result(notFound(id, manager), true);
75850
+ const recordId = record.id;
75851
+ this.agent.services?.tools?.resetCircuit?.(recordId);
75852
+ await manager.reload();
75853
+ const after = manager.get(recordId);
75854
+ if (after === void 0) return result(fail$1("plugin_not_found", `Plugin "${recordId}" disappeared when reset re-read the table.`, "call action:'list' to see what is installed now"), true);
75855
+ await manager.setEnabled(recordId, true);
75856
+ const sync = await this.sync([recordId]);
75857
+ return result({
75858
+ action: "reset",
75859
+ id: recordId,
75860
+ enabled: true,
75861
+ state: after.state,
75862
+ circuit: { cleared: true },
75863
+ ...sync !== void 0 ? { sync } : {},
75864
+ note: after.state === "error" ? "Circuit cleared, but the record still reports errors after re-reading the manifest — see {action:\"info\"}." : "Circuit cleared and capabilities hot-applied. Code entry points still need a separate approved activate."
75865
+ });
75866
+ }
75867
+ /**
75868
+ * Restore a plugin's files from the single-slot backup written by
75869
+ * `upgrade` (install with upgrade:true). Reads-only until success; any
75870
+ * failure is reported as an ordinary tool error.
75871
+ */
75872
+ async rollbackPlugin(manager, id) {
75873
+ const record = manager.get(id);
75874
+ if (record === void 0) return result(notFound(id, manager), true);
75875
+ try {
75876
+ const restored = await manager.rollback(record.id);
75877
+ const sync = await this.sync([record.id]);
75878
+ return result({
75879
+ action: "rollback",
75880
+ id: record.id,
75881
+ rolledBack: true,
75882
+ restoredFrom: restored.restoredFrom,
75883
+ state: restored.record.state,
75884
+ version: restored.record.manifest?.version,
75885
+ ...sync !== void 0 ? { sync } : {},
75886
+ note: "Previous files restored and the table reloaded; code entry points still need a separate approved activate."
75887
+ });
75888
+ } catch (error) {
75889
+ return result(fail$1("rollback_unavailable", error instanceof Error ? error.message : String(error), "call action:'info' to confirm the plugin is installed"), true);
75890
+ }
75891
+ }
75892
+ /**
75893
+ * Hot-apply a successful mutation into live sessions through the host-owned
75894
+ * sync entry point. Advisory by design: the plugin change is already durable
75895
+ * when this runs, so the report is surfaced but never flips the action's
75896
+ * outcome, and a missing entry point (older hosts) degrades to `undefined`.
75897
+ */
75898
+ async sync(changedIds, options) {
75899
+ const pluginSync = this.agent.toolServices?.pluginSync;
75900
+ if (pluginSync === void 0) return void 0;
75901
+ try {
75902
+ return summarizePluginSync(await pluginSync(changedIds, options));
75903
+ } catch (error) {
75904
+ return {
75905
+ ok: false,
75906
+ applied: [],
75907
+ failed: [{
75908
+ step: "mcp.add",
75909
+ message: error instanceof Error ? error.message : String(error)
75910
+ }]
75911
+ };
75912
+ }
75913
+ }
75914
+ };
75915
+ /**
75916
+ * The string that identifies what is being approved. For the source-scoped
75917
+ * actions the source is part of the identity, so the recorded session rule
75918
+ * reads `ManagePlugin(install <source>)` and only replays for that exact source.
75919
+ */
75920
+ function ruleSubject(args) {
75921
+ if (isSourceScoped(args.action) && args.source !== void 0) return `${args.action} ${args.source}`;
75922
+ return args.action;
75923
+ }
75924
+ /** Human-readable approval line. Source-scoped actions must show the full source. */
75925
+ function approvalDescription(args) {
75926
+ switch (args.action) {
75927
+ case "list": return "Listing installed plugins";
75928
+ case "info": return `Reading plugin "${args.id}"`;
75929
+ case "check": return args.id === void 0 ? "Checking all plugins" : `Checking plugin "${args.id}"`;
75930
+ case "marketplace": return `Browsing the plugin marketplace${args.source === void 0 ? "" : ` at ${args.source}`}`;
75931
+ case "precheck": return `Prechecking install origin "${args.source}"`;
75932
+ case "install": return `Install plugin from source: ${args.source} (downloads code; it is not executed until a separate activate)`;
75933
+ case "register_generated": return `Register generated plugin from directory: ${args.source}`;
75934
+ case "enable": return `Enable plugin "${args.id}"`;
75935
+ case "disable": return `Disable plugin "${args.id}"`;
75936
+ case "set_mcp_enabled": return `${args.enabled ? "Enable" : "Disable"} MCP server "${args.server}" of plugin "${args.id}"`;
75937
+ case "activate": return `Activate plugin "${args.id}" — this runs its code entry point in this process`;
75938
+ case "deactivate": return `Deactivate plugin "${args.id}"`;
75939
+ case "remove": return `Remove plugin "${args.id}" from the plugin center`;
75940
+ case "reset": return `Reset plugin "${args.id}" — clear its circuit breaker and re-enable it`;
75941
+ case "rollback": return `Roll back plugin "${args.id}" to its pre-upgrade backup`;
75942
+ case "reload": return "Reload the plugin table from disk";
75943
+ default: return "Managing plugins";
75944
+ }
75945
+ }
75946
+ function missingRequiredArgs(args) {
75947
+ if (NEEDS_ID.has(args.action) && args.id === void 0) return fail$1("missing_id", `Action "${args.action}" needs a plugin id.`, "call action:'list' to see installed plugin ids");
75948
+ if (isSourceScoped(args.action) && (args.source === void 0 || args.source.trim().length === 0)) return fail$1("missing_source", `Action "${args.action}" needs a source.`, "pass source:\"<absolute path | GitHub url | zip url>\", or browse first with action:\"marketplace\"");
75949
+ if (args.action === "set_mcp_enabled") {
75950
+ if (args.server === void 0) return fail$1("missing_server", "Action \"set_mcp_enabled\" needs the MCP server name.", `call {action:"info", id:"${String(args.id)}"} and read its mcpServers list`);
75951
+ if (typeof args.enabled !== "boolean") return fail$1("missing_enabled", "Action \"set_mcp_enabled\" needs enabled:true or enabled:false.", "pass the desired boolean state");
75952
+ }
75953
+ }
75954
+ function notFound(id, manager) {
75955
+ return fail$1("plugin_not_found", `Plugin "${id}" is not installed.`, `call action:'list' to see installed plugin ids (${String(manager.list().length)} installed)`);
75956
+ }
75957
+ /** Turn a manager-thrown message into the next step the model can act on. */
75958
+ function nextForError(args, error) {
75959
+ const message = errorMessage$3(error);
75960
+ if (/already exists/i.test(message)) return `use {action:"enable", id:"${args.id ?? args.source ?? ""}"} or install under a different name`;
75961
+ if (/not installed/i.test(message)) return "call action:'list' to see installed plugin ids";
75962
+ if (/does not declare MCP server/i.test(message)) return `call {action:"info", id:"${String(args.id)}"} and pick one of its listed servers`;
75963
+ if (/absolute path|does not exist|not a directory/i.test(message)) return "pass an absolute directory path, a https GitHub url, or a https zip url";
75964
+ return "inspect {action:\"list\"} and retry; if a record is broken, {action:\"remove\", id:\"<id>\"} it";
75965
+ }
75966
+ function listView(record) {
75967
+ return {
75968
+ id: record.id,
75969
+ name: record.manifest?.name ?? record.id,
75970
+ displayName: record.manifest?.interface?.displayName ?? record.id,
75971
+ version: record.manifest?.version,
75972
+ state: record.state,
75973
+ enabled: record.enabled,
75974
+ source: record.source,
75975
+ originalSource: record.originalSource,
75976
+ path: record.root,
75977
+ keywords: record.manifest?.keywords ?? [],
75978
+ skillCount: record.skillCount,
75979
+ mcpServerCount: Object.keys(record.manifest?.mcpServers ?? {}).length,
75980
+ hasCodeEntryPoint: record.manifest?.entryPoint !== void 0,
75981
+ hasErrors: record.diagnostics.some((diagnostic) => diagnostic.severity === "error")
75982
+ };
75983
+ }
75984
+ function checkView(record, active) {
75985
+ return {
75986
+ id: record.id,
75987
+ state: record.state,
75988
+ enabled: record.enabled,
75989
+ active,
75990
+ hasCodeEntryPoint: record.manifest?.entryPoint !== void 0,
75991
+ errors: record.diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message),
75992
+ warnings: record.diagnostics.filter((diagnostic) => diagnostic.severity === "warn").map((diagnostic) => diagnostic.message)
75993
+ };
75994
+ }
75995
+ /**
75996
+ * Full detail for one plugin, minus the raw manifest blob: `mcpServers`,
75997
+ * `skillRoots` and `diagnostics` are already projected by the manager.
75998
+ */
75999
+ function projectInfo(info) {
76000
+ return {
76001
+ id: info.id,
76002
+ name: info.manifest?.name ?? info.displayName,
76003
+ displayName: info.displayName,
76004
+ version: info.version,
76005
+ description: info.manifest?.description,
76006
+ state: info.state,
76007
+ enabled: info.enabled,
76008
+ source: info.source,
76009
+ originalSource: info.originalSource,
76010
+ path: info.root,
76011
+ keywords: info.manifest?.keywords ?? [],
76012
+ skillCount: info.skillCount,
76013
+ skills: info.skills,
76014
+ skillRoots: info.manifest?.skills ?? [],
76015
+ skillInstructions: info.manifest?.skillInstructions,
76016
+ mcpServerCount: info.mcpServerCount,
76017
+ enabledMcpServerCount: info.enabledMcpServerCount,
76018
+ mcpServers: info.mcpServers,
76019
+ hasCodeEntryPoint: info.manifest?.entryPoint !== void 0,
76020
+ entryPoint: info.manifest?.entryPoint,
76021
+ hooks: (info.manifest?.hooks ?? []).map((hook) => hook.event),
76022
+ hasErrors: info.hasErrors,
76023
+ diagnostics: info.diagnostics,
76024
+ installedAt: info.installedAt,
76025
+ updatedAt: info.updatedAt,
76026
+ manifestKind: info.manifestKind,
76027
+ manifestPath: info.manifestPath,
76028
+ shadowedManifestPath: info.shadowedManifestPath,
76029
+ github: info.github,
76030
+ homepage: info.manifest?.homepage
76031
+ };
76032
+ }
76033
+ //#endregion
74689
76034
  //#region ../../packages/agent-core/src/tools/builtin/python/python.ts
74690
76035
  const PY_DONE_MARKER = "__SCREAM_PY_DONE__";
74691
76036
  /** Head/tail output cap for python tool results — mirrors the generic tool
@@ -76615,19 +77960,23 @@ var EventSubscriptionBus = class {
76615
77960
  this.wildcard.clear();
76616
77961
  }
76617
77962
  /**
76618
- * Deliver an event to matching handlers. A handler that throws is isolated
76619
- * so a subscriber bug can never break the agent's event loop.
77963
+ * Deliver an event to matching handlers. A handler that throws synchronously
77964
+ * is caught here, and a handler that returns a rejected promise has that
77965
+ * rejection swallowed, so a subscriber bug can never break the agent's event
77966
+ * loop nor crash the process through `unhandledRejection`.
76620
77967
  */
76621
77968
  dispatch(event) {
76622
77969
  const typed = this.byType.get(event.type);
76623
77970
  if (typed !== void 0) for (const handler of typed) try {
76624
- handler(event);
77971
+ Promise.resolve(handler(event)).catch(noop$1);
76625
77972
  } catch {}
76626
77973
  if (this.wildcard.size > 0) for (const handler of this.wildcard) try {
76627
- handler(event);
77974
+ Promise.resolve(handler(event)).catch(noop$1);
76628
77975
  } catch {}
76629
77976
  }
76630
77977
  };
77978
+ /** Rejection sink for async subscribers: their failures are deliberately dropped. */
77979
+ function noop$1() {}
76631
77980
  //#endregion
76632
77981
  //#region ../../packages/agent-core/src/agent/context/projector.ts
76633
77982
  /** Synthetic error text used when a tool result is missing and must be
@@ -81652,6 +83001,46 @@ var GitCwdWriteApprovePermissionPolicy = class {
81652
83001
  }
81653
83002
  };
81654
83003
  //#endregion
83004
+ //#region ../../packages/agent-core/src/agent/permission/policies/manage-plugin-read-only-approve.ts
83005
+ const READ_ONLY_ACTIONS = new Set(MANAGE_PLUGIN_READ_ONLY_ACTIONS);
83006
+ /**
83007
+ * Auto-approves the read-only faces of `ManagePlugin`.
83008
+ *
83009
+ * Listing and inspecting the plugin table has no side effects, so gating it
83010
+ * would only train the model to skip the "look before you add" step the tool
83011
+ * exists to enforce. Anything that writes state or runs plugin code decides
83012
+ * nothing here and falls through the chain to the ordinary approval prompt.
83013
+ *
83014
+ * The action is read off the invocation arguments. A missing or unparseable
83015
+ * payload decides nothing on purpose, so the fail-open direction is always
83016
+ * "ask the user".
83017
+ */
83018
+ var ManagePluginReadOnlyApprovePermissionPolicy = class {
83019
+ name = "manage-plugin-read-only-approve";
83020
+ evaluate(context) {
83021
+ if (context.toolCall.name !== "ManagePlugin") return;
83022
+ const action = readAction(context);
83023
+ if (action === void 0 || !READ_ONLY_ACTIONS.has(action)) return;
83024
+ return {
83025
+ kind: "approve",
83026
+ reason: { manage_plugin_action: action }
83027
+ };
83028
+ }
83029
+ };
83030
+ /** Read `action` from the resolved args, falling back to the raw tool-call JSON. */
83031
+ function readAction(context) {
83032
+ const fromArgs = context.args?.action;
83033
+ if (typeof fromArgs === "string") return fromArgs;
83034
+ const rawArguments = context.toolCall.arguments;
83035
+ if (typeof rawArguments !== "string") return void 0;
83036
+ try {
83037
+ const raw = JSON.parse(rawArguments);
83038
+ return typeof raw?.action === "string" ? raw.action : void 0;
83039
+ } catch {
83040
+ return;
83041
+ }
83042
+ }
83043
+ //#endregion
81655
83044
  //#region ../../packages/agent-core/src/agent/permission/policies/plan-mode-guard-deny.ts
81656
83045
  var PlanModeGuardDenyPermissionPolicy = class {
81657
83046
  agent;
@@ -81924,6 +83313,7 @@ function createPermissionDecisionPolicies(agent) {
81924
83313
  new YoloModeApprovePermissionPolicy(agent),
81925
83314
  new WolfPackModeApprovePermissionPolicy(agent),
81926
83315
  new DefaultToolApprovePermissionPolicy(),
83316
+ new ManagePluginReadOnlyApprovePermissionPolicy(),
81927
83317
  new GitCwdWriteApprovePermissionPolicy(agent),
81928
83318
  new FallbackAskPermissionPolicy()
81929
83319
  ];
@@ -83243,6 +84633,43 @@ function compareWireVersions(a, b) {
83243
84633
  }
83244
84634
  //#endregion
83245
84635
  //#region ../../packages/agent-core/src/agent/records/persistence.ts
84636
+ /**
84637
+ * Record types whose state is fully captured by a `context.snapshot` record.
84638
+ * Canonical definition — records/index.ts imports it for the restore
84639
+ * fast-path. It also drives the parse-skipping fast path in `read()` below:
84640
+ * serialized lines of these types that predate the last snapshot are never
84641
+ * JSON.parse'd.
84642
+ */
84643
+ const SNAPSHOT_FOLDED_CONTEXT_TYPES = new Set([
84644
+ "context.append_message",
84645
+ "context.append_loop_event",
84646
+ "context.apply_compaction",
84647
+ "micro_compaction.apply",
84648
+ "full_compaction.complete"
84649
+ ]);
84650
+ /**
84651
+ * Serialized-line prefix of a context.snapshot record. Persisted records
84652
+ * always serialize "type" as the first key (locked by test), so a startsWith
84653
+ * probe is exact — a false positive would require an entire line to start
84654
+ * with this JSON prefix, which no other record (or any message content, whose
84655
+ * line starts with its own record "type") can produce.
84656
+ *
84657
+ * Probes run on raw UTF-8 bytes (see SNAPSHOT_RECORD_LINE_PREFIX_BYTES): all
84658
+ * characters in these prefixes are ASCII, so byte-level matching is exact.
84659
+ */
84660
+ const SNAPSHOT_RECORD_LINE_PREFIX = "{\"type\":\"context.snapshot\"";
84661
+ /** Line prefixes of the folded record types, derived from the set above. */
84662
+ const SNAPSHOT_FOLDED_LINE_PREFIXES = [...SNAPSHOT_FOLDED_CONTEXT_TYPES].map((type) => `{"type":"${type}"`);
84663
+ /** UTF-8 bytes of {@link SNAPSHOT_RECORD_LINE_PREFIX}. */
84664
+ const SNAPSHOT_RECORD_LINE_PREFIX_BYTES = Buffer.from(SNAPSHOT_RECORD_LINE_PREFIX, "utf8");
84665
+ /** UTF-8 bytes of each entry of {@link SNAPSHOT_FOLDED_LINE_PREFIXES}. */
84666
+ const SNAPSHOT_FOLDED_LINE_PREFIX_BYTES = SNAPSHOT_FOLDED_LINE_PREFIXES.map((prefix) => Buffer.from(prefix, "utf8"));
84667
+ function startsWithPrefix(data, prefix) {
84668
+ return data.length >= prefix.length && data.subarray(0, prefix.length).equals(prefix);
84669
+ }
84670
+ function startsWithAnyPrefix(data, prefixes) {
84671
+ return prefixes.some((prefix) => startsWithPrefix(data, prefix));
84672
+ }
83246
84673
  var FileSystemAgentRecordPersistence = class {
83247
84674
  filePath;
83248
84675
  options;
@@ -83257,29 +84684,53 @@ var FileSystemAgentRecordPersistence = class {
83257
84684
  }
83258
84685
  async *read() {
83259
84686
  await this.flush();
83260
- let line = "";
84687
+ const lines = [];
84688
+ let lastSnapshotLineNumber = -1;
83261
84689
  let lineNumber = 0;
83262
- const stream = createReadStream(this.filePath, { encoding: "utf8" });
84690
+ let openChunks = [];
84691
+ const stream = createReadStream(this.filePath);
83263
84692
  try {
83264
84693
  for await (const chunk of stream) {
83265
- line += chunk;
83266
- let newlineIndex = line.indexOf("\n");
83267
- while (newlineIndex !== -1) {
83268
- const rawLine = line.slice(0, newlineIndex);
83269
- line = line.slice(newlineIndex + 1);
84694
+ let searchFrom = 0;
84695
+ for (;;) {
84696
+ const newlineIndex = chunk.indexOf(10, searchFrom);
84697
+ if (newlineIndex === -1) break;
84698
+ const parts = openChunks.length > 0 ? [...openChunks, chunk.subarray(searchFrom, newlineIndex)] : [chunk.subarray(searchFrom, newlineIndex)];
84699
+ let lineData = Buffer.concat(parts);
84700
+ if (lineData.length > 0 && lineData.at(-1) === 13) lineData = lineData.subarray(0, lineData.length - 1);
83270
84701
  lineNumber++;
83271
- const record = parseRecordLine(rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine, lineNumber, this.filePath, false);
83272
- if (record !== void 0) yield record;
83273
- newlineIndex = line.indexOf("\n");
84702
+ if (startsWithPrefix(lineData, SNAPSHOT_RECORD_LINE_PREFIX_BYTES)) lastSnapshotLineNumber = lineNumber;
84703
+ lines.push({
84704
+ data: lineData,
84705
+ lineNumber,
84706
+ allowTruncated: false
84707
+ });
84708
+ openChunks = [];
84709
+ searchFrom = newlineIndex + 1;
83274
84710
  }
84711
+ if (searchFrom < chunk.length) openChunks.push(chunk.subarray(searchFrom));
83275
84712
  }
83276
84713
  } catch (error) {
83277
84714
  if (error.code === "ENOENT") return;
83278
84715
  throw error;
83279
84716
  }
83280
- if (line.length > 0) {
84717
+ if (openChunks.length > 0) {
83281
84718
  lineNumber++;
83282
- const record = parseRecordLine(line, lineNumber, this.filePath, true);
84719
+ lines.push({
84720
+ data: Buffer.concat(openChunks),
84721
+ lineNumber,
84722
+ allowTruncated: true
84723
+ });
84724
+ }
84725
+ let skipFoldedBeforeSnapshot = false;
84726
+ if (lines.length > 0) try {
84727
+ skipFoldedBeforeSnapshot = JSON.parse(lines[0].data.toString("utf8")).protocol_version === "1.4";
84728
+ } catch {
84729
+ skipFoldedBeforeSnapshot = false;
84730
+ }
84731
+ for (const entry of lines) {
84732
+ if (skipFoldedBeforeSnapshot && entry.lineNumber < lastSnapshotLineNumber && startsWithAnyPrefix(entry.data, SNAPSHOT_FOLDED_LINE_PREFIX_BYTES)) continue;
84733
+ const record = parseRecordLine(entry.data.toString("utf8"), entry.lineNumber, this.filePath, entry.allowTruncated);
83283
84734
  if (record !== void 0) yield record;
83284
84735
  }
83285
84736
  }
@@ -83603,14 +85054,10 @@ function asMediaContainer(value) {
83603
85054
  * pushing onto `compactedHistory` — a debug trail rendered from the pre-fold
83604
85055
  * history. Snapshot replay skips that pre-fold history, so the trail would be
83605
85056
  * re-rendered as empty; the snapshot carries the trail itself instead.
85057
+ *
85058
+ * The canonical definition lives in persistence.ts (it also drives the
85059
+ * parse-skipping fast path during read); imported from there.
83606
85060
  */
83607
- const SNAPSHOT_FOLDED_CONTEXT_TYPES = new Set([
83608
- "context.append_message",
83609
- "context.append_loop_event",
83610
- "context.apply_compaction",
83611
- "micro_compaction.apply",
83612
- "full_compaction.complete"
83613
- ]);
83614
85061
  function isSnapshotFoldedContextRecord(type) {
83615
85062
  return SNAPSHOT_FOLDED_CONTEXT_TYPES.has(type);
83616
85063
  }
@@ -83809,16 +85256,37 @@ var AgentRecords = class {
83809
85256
  await this.persistence?.flush();
83810
85257
  }
83811
85258
  };
83812
- //#endregion
83813
- //#region ../../packages/agent-core/src/agent/replay/index.ts
85259
+ /** Mirrors the TUI's isReplayUserTurnRecord predicate (message-replay.ts):
85260
+ * a replay record starts a user turn when it is a user-role message from the
85261
+ * user (or a user-slash skill activation). */
85262
+ function isUserTurnStartRecord(record) {
85263
+ if (record.type !== "message") return false;
85264
+ const { message } = record;
85265
+ if (message.role !== "user") return false;
85266
+ switch (message.origin?.kind) {
85267
+ case void 0:
85268
+ case "user": return true;
85269
+ case "skill_activation": return message.origin.trigger === "user-slash";
85270
+ default: return false;
85271
+ }
85272
+ }
83814
85273
  var ReplayBuilder = class {
83815
85274
  agent;
83816
85275
  records = [];
85276
+ /** Indices (into `records`) of user-turn-start records, oldest first. */
85277
+ userTurnStarts = [];
83817
85278
  constructor(agent) {
83818
85279
  this.agent = agent;
83819
85280
  }
83820
85281
  push(record) {
83821
- if (this.agent.records.restoring) this.records.push(record);
85282
+ if (!this.agent.records.restoring) return;
85283
+ this.records.push(record);
85284
+ if (!isUserTurnStartRecord(record)) return;
85285
+ this.userTurnStarts.push(this.records.length - 1);
85286
+ if (this.userTurnStarts.length <= 10) return;
85287
+ const dropBefore = this.userTurnStarts[this.userTurnStarts.length - 10];
85288
+ this.records.splice(0, dropBefore);
85289
+ this.userTurnStarts = this.userTurnStarts.slice(-10).map((i) => i - dropBefore);
83822
85290
  }
83823
85291
  buildResult() {
83824
85292
  return this.records;
@@ -91661,7 +93129,7 @@ function parseToolCallArguments(raw) {
91661
93129
  } catch {
91662
93130
  return {
91663
93131
  success: false,
91664
- error: errorMessage$4(error)
93132
+ error: errorMessage$5(error)
91665
93133
  };
91666
93134
  }
91667
93135
  }
@@ -91755,7 +93223,7 @@ async function prepareToolCall(step, call) {
91755
93223
  toolCallId: call.toolCall.id,
91756
93224
  error
91757
93225
  });
91758
- return settleError(effectiveArgs, error instanceof PathSecurityError ? error.message : `Tool "${call.toolName}" failed to resolve execution: ${errorMessage$4(error)}`);
93226
+ return settleError(effectiveArgs, error instanceof PathSecurityError ? error.message : `Tool "${call.toolName}" failed to resolve execution: ${errorMessage$5(error)}`);
91759
93227
  }
91760
93228
  const displayFields = toolCallDisplayFieldsFromExecution(execution);
91761
93229
  const settleAborted = () => settleError(effectiveArgs, abortedToolOutput(call.toolName, step.signal), displayFields);
@@ -91814,7 +93282,7 @@ async function runPrepareToolExecutionHook(step, call) {
91814
93282
  return {
91815
93283
  kind: "hookFailed",
91816
93284
  args,
91817
- output: `prepareToolExecution hook failed for "${call.toolName}": ${errorMessage$4(error)}`
93285
+ output: `prepareToolExecution hook failed for "${call.toolName}": ${errorMessage$5(error)}`
91818
93286
  };
91819
93287
  }
91820
93288
  const effectiveArgs = hookResult?.updatedArgs ?? args;
@@ -91855,7 +93323,7 @@ async function runAuthorizeToolExecutionHook(step, call, args, execution) {
91855
93323
  };
91856
93324
  return {
91857
93325
  block: true,
91858
- reason: `authorizeToolExecution hook failed for "${call.toolName}": ${errorMessage$4(error)}`
93326
+ reason: `authorizeToolExecution hook failed for "${call.toolName}": ${errorMessage$5(error)}`
91859
93327
  };
91860
93328
  }
91861
93329
  }
@@ -91882,7 +93350,7 @@ async function runRunnableToolCall(step, call, effectiveArgs, metadata, executio
91882
93350
  toolCallId: toolCall.id,
91883
93351
  error
91884
93352
  });
91885
- return makeErrorToolResult(call, effectiveArgs, aborted ? abortedToolOutput(toolName, signal) : `Tool "${toolName}" failed: ${errorMessage$4(error)}`);
93353
+ return makeErrorToolResult(call, effectiveArgs, aborted ? abortedToolOutput(toolName, signal) : `Tool "${toolName}" failed: ${errorMessage$5(error)}`);
91886
93354
  }
91887
93355
  return makeToolResult(call, effectiveArgs, toolResult);
91888
93356
  }
@@ -91914,7 +93382,7 @@ async function finalizePendingToolResult(step, pendingResult) {
91914
93382
  toolCallId: pendingResult.toolCall.id,
91915
93383
  error
91916
93384
  });
91917
- const output = aborted ? `Tool "${pendingResult.toolName}" aborted during finalizeToolResult hook.` : `finalizeToolResult hook failed for "${pendingResult.toolName}": ${errorMessage$4(error)}`;
93385
+ const output = aborted ? `Tool "${pendingResult.toolName}" aborted during finalizeToolResult hook.` : `finalizeToolResult hook failed for "${pendingResult.toolName}": ${errorMessage$5(error)}`;
91918
93386
  return {
91919
93387
  ...pendingResult,
91920
93388
  stopTurn: pendingResult.stopTurn,
@@ -92427,7 +93895,7 @@ async function runTurn(input) {
92427
93895
  usage
92428
93896
  };
92429
93897
  }
92430
- dispatchEvent(makeInterruptedEvent(isMaxStepsExceededError(error) ? "max_steps" : "error", steps, activeStep, errorMessage$4(error)));
93898
+ dispatchEvent(makeInterruptedEvent(isMaxStepsExceededError(error) ? "max_steps" : "error", steps, activeStep, errorMessage$5(error)));
92431
93899
  throw error;
92432
93900
  }
92433
93901
  return {
@@ -96373,7 +97841,7 @@ function buildSelfMap(options) {
96373
97841
  "",
96374
97842
  "Data:",
96375
97843
  `- skills/ — user skills: \`${userSkillsDir}\` (project skills are listed with their \`Path\` under Available skills above)`,
96376
- `- plugins/ — managed plugins and plugin-managed skills: \`${pluginsDir}\` (installed.json + managed/<name>/)`,
97844
+ `- plugins/ — managed plugins and plugin-managed skills: \`${pluginsDir}\` (installed.json + managed/<name>/); manage them with the \`ManagePlugin\` builtin (read-only actions are free, mutations ask for approval) — never edit installed.json by hand`,
96377
97845
  `- memory/ — persistent cross-session memory: \`${memoryDir}\` (memos.sqlite + entries.jsonl)`,
96378
97846
  `- knowledge/ — local knowledge base (via the KnowledgeLookup tool): \`${knowledgeDir}\` (knowledge.db)`,
96379
97847
  "",
@@ -96567,7 +98035,7 @@ function normalizeSourcePath(path) {
96567
98035
  }
96568
98036
  //#endregion
96569
98037
  //#region ../../packages/agent-core/src/profile/default/agent.yaml
96570
- var agent_default = "name: agent\ndescription: Default Scream Code agent\n\nsystemPromptPath: ./system.md\npromptVars:\n roleAdditional: ''\n\ntools:\n - Read\n - Write\n - Edit\n - Grep\n - Glob\n - Bash\n - TaskList\n - TaskOutput\n - TaskStop\n - CronCreate\n - CronList\n - CronDelete\n - CreateGoal\n - GetGoal\n - SetGoalBudget\n - UpdateGoal\n - ReadMediaFile\n - TodoList\n - MemoryLookup\n - MemoryConsolidatePlan\n - MemoryConsolidateApply\n - MemoryWrite\n - KnowledgeLookup\n - InspectOwnAssets\n - Skill\n - MakeSkillPlan\n - MakeSkillApply\n - WebSearch\n - Agent\n - WolfPack\n\n - FetchURL\n - AskUserQuestion\n - EnterPlanMode\n - FusionPlan\n - ExitPlanMode\n - mcp__*\n\nsubagents:\n coder:\n description: Good at general software engineering tasks.\n explore:\n description: Fast codebase exploration with prompt-enforced read-only behavior.\n plan:\n description: Read-only implementation planning and architecture design.\n verify:\n description: Verification specialist. Runs build, test, and lint commands to validate code changes.\n reviewer:\n description: Code review specialist. Identifies bugs and API contract violations before merge.\n oracle:\n description: Deep debugging, architecture decisions, and second opinions.\n worker:\n description: Office and document automation worker. Performs format conversion, batch file processing, file organization, and document transformation; never modifies code and does not write content.\n writer:\n description: Professional writing and document specialist. Researches, drafts, rewrites, edits, translates, summarizes, and uses available workspace-local toolchains to produce or revise Markdown, text, HTML, PDF/Office-compatible, spreadsheet-style, and presentation-oriented artifacts.\n";
98038
+ var agent_default = "name: agent\ndescription: Default Scream Code agent\n\nsystemPromptPath: ./system.md\npromptVars:\n roleAdditional: ''\n\ntools:\n - Read\n - Write\n - Edit\n - Grep\n - Glob\n - Bash\n - TaskList\n - TaskOutput\n - TaskStop\n - CronCreate\n - CronList\n - CronDelete\n - CreateGoal\n - GetGoal\n - SetGoalBudget\n - UpdateGoal\n - ReadMediaFile\n - TodoList\n - MemoryLookup\n - MemoryConsolidatePlan\n - MemoryConsolidateApply\n - MemoryWrite\n - KnowledgeLookup\n - InspectOwnAssets\n - Skill\n - MakeSkillPlan\n - MakeSkillApply\n # Main-agent capability management. Subagent profiles inherit this list, but\n # the tool is only registered for main agents, so it never reaches their\n # model-visible tool set.\n - ManagePlugin\n - WebSearch\n - Agent\n - WolfPack\n\n - FetchURL\n - AskUserQuestion\n - EnterPlanMode\n - FusionPlan\n - ExitPlanMode\n - mcp__*\n\nsubagents:\n coder:\n description: Good at general software engineering tasks.\n explore:\n description: Fast codebase exploration with prompt-enforced read-only behavior.\n plan:\n description: Read-only implementation planning and architecture design.\n verify:\n description: Verification specialist. Runs build, test, and lint commands to validate code changes.\n reviewer:\n description: Code review specialist. Identifies bugs and API contract violations before merge.\n oracle:\n description: Deep debugging, architecture decisions, and second opinions.\n worker:\n description: Office and document automation worker. Performs format conversion, batch file processing, file organization, and document transformation; never modifies code and does not write content.\n writer:\n description: Professional writing and document specialist. Researches, drafts, rewrites, edits, translates, summarizes, and uses available workspace-local toolchains to produce or revise Markdown, text, HTML, PDF/Office-compatible, spreadsheet-style, and presentation-oriented artifacts.\n";
96571
98039
  //#endregion
96572
98040
  //#region ../../packages/agent-core/src/profile/default/coder.yaml
96573
98041
  var coder_default = "extends: agent\nname: coder\npromptVars:\n roleAdditional: |\n You are now running as a subagent. All the `user` messages are sent by the main agent. The main agent cannot see your context, it can only see your last message when you finish the task. You must treat the parent agent as your caller. Do not directly ask the end user questions. If something is unclear, explain the ambiguity in your final summary to the parent agent.\nwhenToUse: |\n Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent.\ntools:\n - Bash\n - Read\n - ReadMediaFile\n - Glob\n - Grep\n - Write\n - Edit\n - WebSearch\n - FetchURL\n - MemoryLookup\n - MemoryConsolidatePlan\n - MemoryConsolidateApply\n - mcp__*\n";
@@ -96728,6 +98196,20 @@ function cloneTodos(todos) {
96728
98196
  phase: todo.phase
96729
98197
  }));
96730
98198
  }
98199
+ /** Parse `plugin-<id>:<server>` back to the owning plugin id (undefined otherwise). */
98200
+ function pluginOwnerFromMcpServerName(serverName) {
98201
+ if (!serverName.startsWith("plugin-")) return void 0;
98202
+ const colon = serverName.indexOf(":");
98203
+ return colon > 7 ? serverName.slice(7, colon) : void 0;
98204
+ }
98205
+ /** Advisory appended to a tool result when its plugin just tripped the circuit. */
98206
+ function circuitTripNote(pluginId) {
98207
+ return `[circuit] Plugin "${pluginId}" failed ${String(3)} tool calls in a row and was disabled; its tools, MCP servers, skills, and hooks were pulled from this session. Inspect it with ManagePlugin {action:"check", id:"${pluginId}"}; recover with {action:"reset", id:"${pluginId}"} or abandon it with {action:"remove", id:"${pluginId}"}.`;
98208
+ }
98209
+ /** Render any tool payload for the advisory appendage without '[object Object]'. */
98210
+ function describeToolOutput(output) {
98211
+ return typeof output === "string" ? output : JSON.stringify(output);
98212
+ }
96731
98213
  const CRITERIA_SYSTEM_PROMPT = [
96732
98214
  "You generate concrete, verifiable acceptance criteria for a given objective.",
96733
98215
  "Criteria must be specific and testable — state what should work end-to-end, not just what should exist.",
@@ -96948,6 +98430,11 @@ var ToolManager = class {
96948
98430
  agent;
96949
98431
  builtinTools = /* @__PURE__ */ new Map();
96950
98432
  userTools = /* @__PURE__ */ new Map();
98433
+ /** tool name → owning plugin id; the index `unregisterToolsByOwner` reads. */
98434
+ toolOwners = /* @__PURE__ */ new Map();
98435
+ /** plugin id → consecutive capability failures; a success resets to zero. */
98436
+ circuitFailures = /* @__PURE__ */ new Map();
98437
+ trippedPlugins = /* @__PURE__ */ new Set();
96951
98438
  mcpTools = /* @__PURE__ */ new Map();
96952
98439
  /** server name → list of qualified tool names registered for that server. */
96953
98440
  mcpToolsByServer = /* @__PURE__ */ new Map();
@@ -97005,11 +98492,14 @@ var ToolManager = class {
97005
98492
  * this method takes a hand-written registration.
97006
98493
  */
97007
98494
  registerUserTool(input) {
98495
+ const { name, description, parameters, ownerPluginId, execute } = input;
97008
98496
  this.agent.records.logRecord({
97009
98497
  type: "tools.register_user_tool",
97010
- ...input
98498
+ name,
98499
+ description,
98500
+ parameters,
98501
+ ...ownerPluginId !== void 0 ? { ownerPluginId } : {}
97011
98502
  });
97012
- const { name, description, parameters } = input;
97013
98503
  const tool = {
97014
98504
  name,
97015
98505
  description,
@@ -97019,17 +98509,48 @@ var ToolManager = class {
97019
98509
  description,
97020
98510
  approvalRule: name,
97021
98511
  execute: async (context) => {
97022
- return this.agent.rpc.toolCall({
98512
+ const noteOutcome = (outcome) => {
98513
+ if (ownerPluginId === void 0) return outcome;
98514
+ if (this.reportOwnerOutcome(ownerPluginId, outcome.isError !== true) !== "tripped") return outcome;
98515
+ return {
98516
+ ...outcome,
98517
+ output: `${describeToolOutput(outcome.output)}\n\n${circuitTripNote(ownerPluginId)}`
98518
+ };
98519
+ };
98520
+ if (execute !== void 0) try {
98521
+ return noteOutcome(await execute(args ?? {}, context) ?? {
98522
+ output: `Tool "${name}" returned no result.`,
98523
+ isError: true
98524
+ });
98525
+ } catch (error) {
98526
+ return noteOutcome({
98527
+ output: `Tool "${name}" failed: ${error instanceof Error ? error.message : String(error)}`,
98528
+ isError: true
98529
+ });
98530
+ }
98531
+ const rpc = this.agent.rpc;
98532
+ if (rpc?.toolCall === void 0) return {
98533
+ output: `User-registered tool "${name}" cannot execute: host does not support in-band tool callbacks.`,
98534
+ isError: true
98535
+ };
98536
+ return noteOutcome(await rpc.toolCall({
97023
98537
  turnId: Number(context.turnId),
97024
98538
  toolCallId: context.toolCallId,
97025
98539
  args
97026
- }, { signal: context.signal });
98540
+ }, { signal: context.signal }));
97027
98541
  }
97028
98542
  };
97029
98543
  }
97030
98544
  };
97031
98545
  this.userTools.set(name, tool);
97032
98546
  this.enabledTools.add(name);
98547
+ if (ownerPluginId !== void 0) this.toolOwners.set(name, ownerPluginId);
98548
+ else this.toolOwners.delete(name);
98549
+ this.agent.emitEvent({
98550
+ type: "tool.list.updated",
98551
+ reason: "user.registered",
98552
+ toolName: name
98553
+ });
97033
98554
  }
97034
98555
  unregisterUserTool(name) {
97035
98556
  this.agent.records.logRecord({
@@ -97038,6 +98559,80 @@ var ToolManager = class {
97038
98559
  });
97039
98560
  this.userTools.delete(name);
97040
98561
  this.enabledTools.delete(name);
98562
+ this.toolOwners.delete(name);
98563
+ this.agent.emitEvent({
98564
+ type: "tool.list.updated",
98565
+ reason: "user.unregistered",
98566
+ toolName: name
98567
+ });
98568
+ }
98569
+ /**
98570
+ * Drop every user tool owned by `ownerPluginId` (a plugin being removed,
98571
+ * disabled, or circuit-tripped). Returns the removed count. The loop rebuilds
98572
+ * its tool list from `loopTools` on the next step, so no extra refresh is
98573
+ * needed beyond the per-tool events emitted here.
98574
+ */
98575
+ unregisterToolsByOwner(ownerPluginId) {
98576
+ let removed = 0;
98577
+ for (const [toolName, owner] of this.toolOwners) {
98578
+ if (owner !== ownerPluginId) continue;
98579
+ this.unregisterUserTool(toolName);
98580
+ removed += 1;
98581
+ }
98582
+ return removed;
98583
+ }
98584
+ /**
98585
+ * Circuit breaker bookkeeping (see `PLUGIN_CIRCUIT_TRIP_THRESHOLD`): every
98586
+ * plugin-owned capability outcome flows here. `ok` clears the streak; a
98587
+ * failure streak reaching the threshold trips the plugin exactly once and
98588
+ * starts its asynchronous teardown. Returns 'tripped' on the call that
98589
+ * crossed the line, so the caller can attach the advisory to its result.
98590
+ */
98591
+ reportOwnerOutcome(pluginId, ok) {
98592
+ this.agent.toolServices?.plugins?.recordUsage?.(pluginId, ok);
98593
+ if (this.trippedPlugins.has(pluginId)) return void 0;
98594
+ if (ok) {
98595
+ this.circuitFailures.delete(pluginId);
98596
+ return;
98597
+ }
98598
+ const failures = (this.circuitFailures.get(pluginId) ?? 0) + 1;
98599
+ this.circuitFailures.set(pluginId, failures);
98600
+ if (failures < 3) return void 0;
98601
+ this.trippedPlugins.add(pluginId);
98602
+ this.tripPlugin(pluginId, failures);
98603
+ return "tripped";
98604
+ }
98605
+ getCircuitFailures(pluginId) {
98606
+ return this.circuitFailures.get(pluginId) ?? 0;
98607
+ }
98608
+ isCircuitTripped(pluginId) {
98609
+ return this.trippedPlugins.has(pluginId);
98610
+ }
98611
+ /** Clear the breaker for a plugin (ManagePlugin `reset`); it starts clean. */
98612
+ resetCircuit(pluginId) {
98613
+ this.circuitFailures.delete(pluginId);
98614
+ this.trippedPlugins.delete(pluginId);
98615
+ }
98616
+ /**
98617
+ * Take a tripped plugin out of service: record why, persist it disabled,
98618
+ * then let the host's session-sync pass tear its code, tools, MCP servers,
98619
+ * and skills out of every live session. Best-effort: a teardown surprise
98620
+ * must never propagate back into the tool loop that detected the failures.
98621
+ */
98622
+ async tripPlugin(pluginId, failures) {
98623
+ const services = this.agent.toolServices;
98624
+ try {
98625
+ if (services?.plugins !== void 0) {
98626
+ await services.plugins.markError(pluginId, `circuit tripped after ${String(failures)} consecutive tool failures`);
98627
+ await services.plugins.setEnabled(pluginId, false);
98628
+ }
98629
+ } catch {}
98630
+ try {
98631
+ await services?.pluginSync?.([pluginId]);
98632
+ } catch {}
98633
+ try {
98634
+ await services?.plugins?.appendQuarantine(pluginId, `circuit tripped after ${String(failures)} consecutive tool failures`);
98635
+ } catch {}
97041
98636
  }
97042
98637
  /**
97043
98638
  * Register an MCP server (and its tools). Public entry point for third-party
@@ -97085,38 +98680,48 @@ var ToolManager = class {
97085
98680
  description: tool.description,
97086
98681
  approvalRule: qualified,
97087
98682
  execute: async (context) => {
97088
- const runCall = async (targetClient) => {
97089
- return mcpResultToExecutableOutput(await targetClient.callTool(tool.name, args ?? {}, context.signal), qualified);
97090
- };
97091
- try {
97092
- return await runCall(client);
97093
- } catch (error) {
97094
- const mcp = options?.mcp;
97095
- if (mcp === void 0 || context.signal.aborted || !(isRetriableMcpCallError(error) || mcp.get(serverName)?.status !== "connected")) return {
97096
- isError: true,
97097
- output: `MCP tool "${tool.name}" failed: ` + (error instanceof Error ? error.message : String(error))
98683
+ const invoke = async () => {
98684
+ const runCall = async (targetClient) => {
98685
+ return mcpResultToExecutableOutput(await targetClient.callTool(tool.name, args ?? {}, context.signal), qualified);
97098
98686
  };
97099
98687
  try {
97100
- await mcp.reconnect(serverName);
97101
- } catch {}
97102
- if (context.signal.aborted) return {
97103
- isError: true,
97104
- output: `MCP tool "${tool.name}" aborted during reconnect.`
97105
- };
97106
- const resolved = mcp.resolved(serverName);
97107
- if (resolved === void 0) return {
97108
- isError: true,
97109
- output: `MCP tool "${tool.name}" failed: ` + (error instanceof Error ? error.message : String(error))
97110
- };
97111
- try {
97112
- return await runCall(resolved.client);
97113
- } catch (retryError) {
97114
- return {
98688
+ return await runCall(client);
98689
+ } catch (error) {
98690
+ const mcp = options?.mcp;
98691
+ if (mcp === void 0 || context.signal.aborted || !(isRetriableMcpCallError(error) || mcp.get(serverName)?.status !== "connected")) return {
98692
+ isError: true,
98693
+ output: `MCP tool "${tool.name}" failed: ` + (error instanceof Error ? error.message : String(error))
98694
+ };
98695
+ try {
98696
+ await mcp.reconnect(serverName);
98697
+ } catch {}
98698
+ if (context.signal.aborted) return {
97115
98699
  isError: true,
97116
- output: `MCP tool "${tool.name}" failed after reconnect: ` + (retryError instanceof Error ? retryError.message : String(retryError))
98700
+ output: `MCP tool "${tool.name}" aborted during reconnect.`
97117
98701
  };
98702
+ const resolved = mcp.resolved(serverName);
98703
+ if (resolved === void 0) return {
98704
+ isError: true,
98705
+ output: `MCP tool "${tool.name}" failed: ` + (error instanceof Error ? error.message : String(error))
98706
+ };
98707
+ try {
98708
+ return await runCall(resolved.client);
98709
+ } catch (retryError) {
98710
+ return {
98711
+ isError: true,
98712
+ output: `MCP tool "${tool.name}" failed after reconnect: ` + (retryError instanceof Error ? retryError.message : String(retryError))
98713
+ };
98714
+ }
97118
98715
  }
97119
- }
98716
+ };
98717
+ const outcome = await invoke();
98718
+ const owner = pluginOwnerFromMcpServerName(serverName);
98719
+ if (owner === void 0) return outcome;
98720
+ if (this.reportOwnerOutcome(owner, outcome.isError !== true) !== "tripped") return outcome;
98721
+ return {
98722
+ ...outcome,
98723
+ output: `${describeToolOutput(outcome.output)}\n\n${circuitTripNote(owner)}`
98724
+ };
97120
98725
  }
97121
98726
  };
97122
98727
  }
@@ -97229,6 +98834,26 @@ var ToolManager = class {
97229
98834
  isMcpToolEnabled(name) {
97230
98835
  return this.mcpAccessPatterns.some((pattern) => import_picomatch.default.isMatch(name, pattern));
97231
98836
  }
98837
+ /** The enabled-name filter a turn freezes at its start. */
98838
+ snapshotEnabledTools() {
98839
+ return {
98840
+ names: new Set(this.enabledTools),
98841
+ mcpPatterns: [...this.mcpAccessPatterns]
98842
+ };
98843
+ }
98844
+ /**
98845
+ * Build the offered tool table against an explicit enabled-name filter
98846
+ * while the registration maps (user tools, MCP servers) stay live. A turn
98847
+ * uses this with its frozen filter: a mid-turn `setActiveTools` then
98848
+ * applies to the NEXT turn by design (turn config stability), while a
98849
+ * plugin tool registered — or reclaimed by the breaker — mid-turn still
98850
+ * shows up on the very next step.
98851
+ */
98852
+ loopToolsFor(filter) {
98853
+ const mcpNames = [...this.mcpTools.keys()].filter((name) => filter.mcpPatterns.some((pattern) => import_picomatch.default.isMatch(name, pattern)));
98854
+ const hideGoalMutationTools = this.agent.goal.getGoal().goal === null;
98855
+ return uniq([...filter.names, ...mcpNames]).toSorted((a, b) => a.localeCompare(b)).filter((name) => !(hideGoalMutationTools && (name === "SetGoalBudget" || name === "UpdateGoal" || name === "WriteGoalNote"))).map((name) => this.userTools.get(name) ?? this.mcpTools.get(name)?.tool ?? this.builtinTools.get(name)).filter((tool) => !!tool);
98856
+ }
97232
98857
  *toolInfos() {
97233
98858
  for (const tool of this.builtinTools.values()) yield {
97234
98859
  name: tool.name,
@@ -97306,6 +98931,7 @@ var ToolManager = class {
97306
98931
  this.agent.skills?.registry.listInvocableSkills().length && new SkillTool(this.agent),
97307
98932
  this.agent.type === "main" && new MakeSkillPlanTool(this.agent),
97308
98933
  this.agent.type === "main" && new MakeSkillApplyTool(this.agent),
98934
+ this.agent.type === "main" && new ManagePluginTool(this.agent),
97309
98935
  canSpawn && new AgentTool(this.agent.subagentHost, background, visibleSubagents, {
97310
98936
  allowBackground,
97311
98937
  log: this.agent.log,
@@ -97331,9 +98957,10 @@ var ToolManager = class {
97331
98957
  return (input) => withAuth((auth) => uploadVideo(input, { auth }));
97332
98958
  }
97333
98959
  get loopTools() {
97334
- const mcpNames = [...this.mcpTools.keys()].filter((name) => this.isMcpToolEnabled(name));
97335
- const hideGoalMutationTools = this.agent.goal.getGoal().goal === null;
97336
- return uniq([...this.enabledTools, ...mcpNames]).toSorted((a, b) => a.localeCompare(b)).filter((name) => !(hideGoalMutationTools && (name === "SetGoalBudget" || name === "UpdateGoal" || name === "WriteGoalNote"))).map((name) => this.userTools.get(name) ?? this.mcpTools.get(name)?.tool ?? this.builtinTools.get(name)).filter((tool) => !!tool);
98960
+ return this.loopToolsFor({
98961
+ names: this.enabledTools,
98962
+ mcpPatterns: this.mcpAccessPatterns
98963
+ });
97337
98964
  }
97338
98965
  };
97339
98966
  //#endregion
@@ -98217,6 +99844,7 @@ var TurnFlow = class {
98217
99844
  async runTurn(turnId, signal) {
98218
99845
  let stopHookContinuationUsed = false;
98219
99846
  const deduper = new ToolCallDeduplicator();
99847
+ const turnToolFilter = this.agent.tools.snapshotEnabledTools();
98220
99848
  await Promise.race([this.agent.mcp?.waitForInitialLoad(signal) ?? Promise.resolve(), new Promise((resolve) => setTimeout(resolve, MCP_WAIT_TIMEOUT_MS))]);
98221
99849
  while (true) {
98222
99850
  signal.throwIfAborted();
@@ -98230,6 +99858,7 @@ var TurnFlow = class {
98230
99858
  buildMessages: () => this.agent.context.messagesForLLM(),
98231
99859
  dispatchEvent: this.buildDispatchEvent(turnId),
98232
99860
  tools: this.agent.tools.loopTools,
99861
+ buildTools: () => this.agent.tools.loopToolsFor(turnToolFilter),
98233
99862
  log: this.agent.log,
98234
99863
  maxSteps: loopControl?.maxStepsPerTurn,
98235
99864
  maxRetryAttempts: loopControl?.maxRetriesPerStep,
@@ -99683,6 +101312,7 @@ function applyEnvModelConfig(config, env = process.env) {
99683
101312
  const displayName = trimmed(env["SCREAM_MODEL_DISPLAY_NAME"]);
99684
101313
  const reasoningKey = trimmed(env["SCREAM_MODEL_REASONING_KEY"]);
99685
101314
  const adaptiveThinking = parseBooleanVar(env["SCREAM_MODEL_ADAPTIVE_THINKING"], "SCREAM_MODEL_ADAPTIVE_THINKING");
101315
+ const forceThinking = parseBooleanVar(env["SCREAM_MODEL_FORCE_THINKING"], "SCREAM_MODEL_FORCE_THINKING");
99686
101316
  const alias = {
99687
101317
  provider: ENV_MODEL_PROVIDER_KEY,
99688
101318
  model,
@@ -99691,7 +101321,8 @@ function applyEnvModelConfig(config, env = process.env) {
99691
101321
  ...displayName !== void 0 ? { displayName } : {},
99692
101322
  ...maxOutputSize !== void 0 ? { maxOutputSize } : {},
99693
101323
  ...reasoningKey !== void 0 ? { reasoningKey } : {},
99694
- ...adaptiveThinking !== void 0 ? { adaptiveThinking } : {}
101324
+ ...adaptiveThinking !== void 0 ? { adaptiveThinking } : {},
101325
+ ...forceThinking !== void 0 ? { forceThinking } : {}
99695
101326
  };
99696
101327
  const thinkingMode = trimmed(env["SCREAM_MODEL_THINKING_MODE"]);
99697
101328
  const thinkingEffort = trimmed(env["SCREAM_MODEL_THINKING_EFFORT"]);
@@ -104535,9 +106166,10 @@ function createRPC() {
104535
106166
  * - `load()` — dynamic-import the entry point (cached per path)
104536
106167
  * - `activate()` — inject declared manifest hooks into the agent's HookEngine,
104537
106168
  * then call the plugin's `activate(context)`; tools are registered by the
104538
- * plugin itself via `context.services.tools.registerUserTool`
104539
- * - `deactivate()` symmetric removal (hooks + deactivate hook), isolated so
104540
- * a failing plugin never breaks the agent
106169
+ * plugin itself via `context.services.tools.registerUserTool`. Hooks and
106170
+ * event subscriptions made during a failed activation are rolled back
106171
+ * - `deactivate()` symmetric removal (hooks + event subscriptions +
106172
+ * deactivate hook), isolated so a failing plugin never breaks the agent
104541
106173
  *
104542
106174
  * Activation is deliberately lazy (a `/plugin activate` command or explicit
104543
106175
  * config opt-in), so merely installing a plugin never executes its code.
@@ -104572,9 +106204,15 @@ var ExtensionRuntime = class {
104572
106204
  const module = await this.load(extension.entryPoint);
104573
106205
  let hooksUnregister;
104574
106206
  if (extension.manifest.hooks !== void 0 && extension.manifest.hooks.length > 0) hooksUnregister = agent.hooks?.registerAll(extension.manifest.hooks);
106207
+ const subscriptions = [];
106208
+ let handlerFaults = 0;
106209
+ const onHandlerFault = () => {
106210
+ handlerFaults += 1;
106211
+ if (handlerFaults >= 3 && this.isActive(extension.pluginId)) this.deactivate(extension.pluginId);
106212
+ };
104575
106213
  const context = {
104576
- services: agent.services,
104577
- events: agent.eventBus,
106214
+ services: withPluginToolOwnership(agent.services, extension.pluginId),
106215
+ events: createTrackedEventBus(agent.eventBus, subscriptions, onHandlerFault),
104578
106216
  config: extension.manifest.config,
104579
106217
  pluginId: extension.pluginId
104580
106218
  };
@@ -104582,20 +106220,42 @@ var ExtensionRuntime = class {
104582
106220
  await module.activate(context);
104583
106221
  } catch (error) {
104584
106222
  hooksUnregister?.();
106223
+ releaseSubscriptions(subscriptions);
104585
106224
  throw error;
104586
106225
  }
104587
106226
  this.activations.set(extension.pluginId, {
104588
106227
  module,
104589
- hooksUnregister
106228
+ hooksUnregister,
106229
+ subscriptions,
106230
+ log: agent.log,
106231
+ agent
104590
106232
  });
104591
106233
  }
104592
- /** Deactivate a plugin: remove its hooks and call its deactivate hook. */
106234
+ /** Deactivate a plugin: drop its hooks/subscriptions, then call its deactivate. */
104593
106235
  async deactivate(pluginId) {
104594
106236
  const activation = this.activations.get(pluginId);
104595
106237
  if (activation === void 0) return;
104596
106238
  activation.hooksUnregister?.();
106239
+ releaseSubscriptions(activation.subscriptions);
104597
106240
  this.activations.delete(pluginId);
104598
- await activation.module.deactivate?.();
106241
+ try {
106242
+ activation.agent.services.tools?.unregisterToolsByOwner?.(pluginId);
106243
+ } catch {}
106244
+ try {
106245
+ await activation.module.deactivate?.();
106246
+ } catch (error) {
106247
+ activation.log?.warn("plugin deactivate failed", {
106248
+ pluginId,
106249
+ error: error instanceof Error ? error.message : String(error)
106250
+ });
106251
+ }
106252
+ }
106253
+ /**
106254
+ * Deactivate every live plugin. Provided so a host can tear down all code
106255
+ * plugins in one call; each plugin's failure is isolated from the others.
106256
+ */
106257
+ async deactivateAll() {
106258
+ for (const pluginId of Array.from(this.activations.keys())) await this.deactivate(pluginId);
104599
106259
  }
104600
106260
  isActive(pluginId) {
104601
106261
  return this.activations.has(pluginId);
@@ -104609,6 +106269,67 @@ function normalizeExtensionModule(mod) {
104609
106269
  if (candidate === void 0 || typeof candidate.activate !== "function") throw new Error("Plugin entry point must export an activate(context) function");
104610
106270
  return candidate;
104611
106271
  }
106272
+ /**
106273
+ * Wrap the agent's bus in a per-activation view that records every unsubscribe
106274
+ * handle the plugin gets, so the runtime can release them if activation fails.
106275
+ */
106276
+ function createTrackedEventBus(bus, subscriptions, onFault) {
106277
+ return {
106278
+ subscribe: (type, handler) => {
106279
+ const guarded = (event) => {
106280
+ try {
106281
+ const out = handler(event);
106282
+ if (out !== null && typeof out.then === "function") out.catch(() => {
106283
+ onFault?.();
106284
+ });
106285
+ return out;
106286
+ } catch (error) {
106287
+ onFault?.();
106288
+ throw error;
106289
+ }
106290
+ };
106291
+ const unsubscribe = bus.subscribe(type, guarded);
106292
+ subscriptions.push(unsubscribe);
106293
+ return unsubscribe;
106294
+ },
106295
+ clear: () => {
106296
+ releaseSubscriptions(subscriptions);
106297
+ }
106298
+ };
106299
+ }
106300
+ /** Release every subscription recorded for one activation, isolating failures. */
106301
+ function releaseSubscriptions(subscriptions) {
106302
+ while (subscriptions.length > 0) {
106303
+ const unsubscribe = subscriptions.pop();
106304
+ try {
106305
+ unsubscribe?.();
106306
+ } catch {}
106307
+ }
106308
+ }
106309
+ /**
106310
+ * A services view handed to one activated plugin. It forwards everything to
106311
+ * the agent's real manifest except `tools.registerUserTool`, where it stamps
106312
+ * `ownerPluginId` with this plugin's id (an explicit id from the plugin wins).
106313
+ * Ownership is what lets remove/disable/circuit-trip reclaim the plugin's
106314
+ * tools from live agents without trusting the plugin to clean up after itself.
106315
+ */
106316
+ function withPluginToolOwnership(services, pluginId) {
106317
+ const tools = services.tools;
106318
+ if (typeof tools !== "object" || tools === null) return services;
106319
+ const ownedTools = new Proxy(tools, { get(target, prop, receiver) {
106320
+ if (prop === "registerUserTool") return (input) => {
106321
+ target.registerUserTool({
106322
+ ...input,
106323
+ ownerPluginId: input.ownerPluginId ?? pluginId
106324
+ });
106325
+ };
106326
+ return Reflect.get(target, prop, receiver);
106327
+ } });
106328
+ return {
106329
+ ...services,
106330
+ tools: ownedTools
106331
+ };
106332
+ }
104612
106333
  //#endregion
104613
106334
  //#region ../../packages/agent-core/src/tools/providers/fetch-cache.ts
104614
106335
  /**
@@ -119096,7 +120817,7 @@ var ProviderManager = class {
119096
120817
  const providerConfig = this.config.providers[providerName];
119097
120818
  if (providerConfig === void 0) throw new ScreamError(ErrorCodes.CONFIG_INVALID, `Provider "${providerName}" for model "${model}" is not configured.`);
119098
120819
  if (!Number.isInteger(alias.maxContextSize) || alias.maxContextSize <= 0) throw new ScreamError(ErrorCodes.CONFIG_INVALID, `Model "${model}" must define a positive max_context_size in config.toml.`);
119099
- const provider = toLtodProviderConfig(providerConfig, alias.model, this.options.screamRequestHeaders, alias.maxOutputSize, alias.reasoningKey, this.options.promptCacheKey, alias.adaptiveThinking);
120820
+ const provider = toLtodProviderConfig(providerConfig, alias.model, this.options.screamRequestHeaders, alias.maxOutputSize, alias.reasoningKey, this.options.promptCacheKey, alias.adaptiveThinking, alias.forceThinking);
119100
120821
  return {
119101
120822
  providerName,
119102
120823
  provider,
@@ -119158,7 +120879,7 @@ function resolveModelCapabilities(alias, provider) {
119158
120879
  max_context_tokens: alias.maxContextSize
119159
120880
  };
119160
120881
  }
119161
- function toLtodProviderConfig(provider, model, screamRequestHeaders, maxOutputSize, reasoningKey, promptCacheKey, adaptiveThinking) {
120882
+ function toLtodProviderConfig(provider, model, screamRequestHeaders, maxOutputSize, reasoningKey, promptCacheKey, adaptiveThinking, forceThinking) {
119162
120883
  switch (provider.type) {
119163
120884
  case "anthropic": return {
119164
120885
  type: "anthropic",
@@ -119175,6 +120896,7 @@ function toLtodProviderConfig(provider, model, screamRequestHeaders, maxOutputSi
119175
120896
  baseUrl: providerValue(provider.baseUrl, provider.env, "OPENAI_BASE_URL"),
119176
120897
  apiKey: providerApiKey(provider),
119177
120898
  reasoningKey,
120899
+ ...forceThinking !== void 0 ? { forceThinking } : {},
119178
120900
  ...defaultHeadersField(provider.customHeaders)
119179
120901
  };
119180
120902
  case "scream": return {
@@ -119183,6 +120905,7 @@ function toLtodProviderConfig(provider, model, screamRequestHeaders, maxOutputSi
119183
120905
  baseUrl: providerValue(provider.baseUrl, provider.env, "SCREAM_BASE_URL"),
119184
120906
  apiKey: providerApiKey(provider),
119185
120907
  generationKwargs: { prompt_cache_key: promptCacheKey },
120908
+ ...forceThinking !== void 0 ? { forceThinking } : {},
119186
120909
  ...defaultHeadersField({
119187
120910
  ...screamRequestHeaders,
119188
120911
  ...provider.customHeaders
@@ -121244,12 +122967,36 @@ var ScreamCore = class {
121244
122967
  if (plugin === void 0) throw new ScreamError(ErrorCodes.PLUGIN_NOT_FOUND, `Plugin "${pluginId}" was not found`, { details: { pluginId } });
121245
122968
  const [extension] = this.extensionRuntime.discover([plugin]);
121246
122969
  if (extension === void 0) throw new ScreamError(ErrorCodes.PLUGIN_NOT_FOUND, `Plugin "${pluginId}" has no code entry point`, { details: { pluginId } });
121247
- await this.extensionRuntime.activate(agent, extension);
122970
+ try {
122971
+ await this.extensionRuntime.activate(agent, extension);
122972
+ } catch (error) {
122973
+ await this.markPluginError(pluginId, error);
122974
+ throw error;
122975
+ }
122976
+ await this.syncSessionsBestEffort([pluginId]);
122977
+ }
122978
+ /**
122979
+ * Best-effort bookkeeping for a failed activation: a `markError` failure must
122980
+ * not replace the activation error the caller is about to see.
122981
+ */
122982
+ async markPluginError(pluginId, error) {
122983
+ const message = error instanceof Error ? error.message : String(error);
122984
+ try {
122985
+ await this.plugins.markError(pluginId, message);
122986
+ } catch (markError) {
122987
+ log.warn("failed to record plugin activation error", {
122988
+ pluginId,
122989
+ reason: markError instanceof Error ? markError.message : String(markError)
122990
+ });
122991
+ }
121248
122992
  }
121249
122993
  /** Deactivate a code plugin (removes its hooks and runs its deactivate). */
121250
122994
  async deactivatePlugin({ pluginId }) {
121251
122995
  await this.pluginsReady;
121252
122996
  await this.extensionRuntime.deactivate(pluginId);
122997
+ for (const session of this.sessions.values()) for (const agent of session.agents.values()) try {
122998
+ agent.tools.unregisterToolsByOwner(pluginId);
122999
+ } catch {}
121253
123000
  }
121254
123001
  /** Code plugins the runtime can load, with their activation state. */
121255
123002
  async pluginExtensionStatus() {
@@ -121397,13 +123144,202 @@ var ScreamCore = class {
121397
123144
  removeMcpServer({ sessionId, ...payload }) {
121398
123145
  return this.sessionApi(sessionId).removeMcpServer(payload);
121399
123146
  }
123147
+ /**
123148
+ * Push the current plugin table into every live session: plugin MCP servers,
123149
+ * plugin skills, and in-process code/tool teardown for plugins that stopped
123150
+ * being live. This is the hot-apply path behind `ToolServices.pluginSync`
123151
+ * and the plugin write RPCs.
123152
+ *
123153
+ * Contract:
123154
+ * - MCP edits are confined to plugin-owned runtime names (`plugin-<id>:<server>`);
123155
+ * user-configured servers are never touched.
123156
+ * - `skipMcpAdd` honors "install never executes": skill data hot-applies, but
123157
+ * no plugin MCP process is started until an explicit enable/activate/reload.
123158
+ * - A changed id whose record is gone, disabled, or errored gets its code
123159
+ * deactivated and its owned user tools dropped before its capabilities are
123160
+ * pulled from sessions. A live id is never activated here — running code
123161
+ * still requires an explicit, separately approved `activate`.
123162
+ * - Every sub-action is isolated: failures accumulate in `failed[]` and this
123163
+ * returns a report; it never throws back to a caller whose plugin mutation
123164
+ * already succeeded.
123165
+ */
123166
+ async applyPluginChangesToSessions(changedIds, options) {
123167
+ await this.pluginsReady;
123168
+ const applied = [];
123169
+ const failed = [];
123170
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
123171
+ let desired = {};
123172
+ let desiredUnknown = false;
123173
+ try {
123174
+ desired = this.plugins.enabledMcpServers();
123175
+ } catch (error) {
123176
+ desiredUnknown = true;
123177
+ failed.push({
123178
+ step: "mcp.add",
123179
+ message: messageOf(error)
123180
+ });
123181
+ }
123182
+ const targets = new Set(changedIds ?? []);
123183
+ if (changedIds === void 0) {
123184
+ try {
123185
+ for (const record of this.plugins.list()) targets.add(record.id);
123186
+ } catch (error) {
123187
+ failed.push({
123188
+ step: "mcp.add",
123189
+ message: messageOf(error)
123190
+ });
123191
+ }
123192
+ for (const [sessionId, session] of this.sessions) try {
123193
+ for (const entry of session.mcp.list()) {
123194
+ if (!isPluginMcpRuntimeName(entry.name)) continue;
123195
+ const id = pluginIdFromMcpRuntimeName(entry.name);
123196
+ if (id !== void 0) targets.add(id);
123197
+ }
123198
+ } catch (error) {
123199
+ failed.push({
123200
+ step: "mcp.remove",
123201
+ message: `session ${sessionId}: ${messageOf(error)}`
123202
+ });
123203
+ }
123204
+ }
123205
+ const isLive = (id) => {
123206
+ const record = this.plugins.get(id);
123207
+ return record !== void 0 && record.enabled && record.state === "ok";
123208
+ };
123209
+ for (const id of targets) {
123210
+ if (isLive(id)) continue;
123211
+ if (this.extensionRuntime.isActive(id)) try {
123212
+ await this.extensionRuntime.deactivate(id);
123213
+ applied.push({
123214
+ kind: "plugin.deactivate",
123215
+ name: id,
123216
+ session: "host"
123217
+ });
123218
+ } catch (error) {
123219
+ failed.push({
123220
+ step: "plugin.deactivate",
123221
+ pluginId: id,
123222
+ message: messageOf(error)
123223
+ });
123224
+ }
123225
+ for (const [sessionId, session] of this.sessions) for (const agent of session.agents.values()) try {
123226
+ if (agent.tools.unregisterToolsByOwner(id) > 0) applied.push({
123227
+ kind: "tools.remove",
123228
+ name: id,
123229
+ session: sessionId
123230
+ });
123231
+ } catch (error) {
123232
+ failed.push({
123233
+ step: "tools.remove",
123234
+ pluginId: id,
123235
+ message: messageOf(error)
123236
+ });
123237
+ }
123238
+ }
123239
+ for (const [sessionId, session] of this.sessions) {
123240
+ let actualNames = /* @__PURE__ */ new Set();
123241
+ try {
123242
+ actualNames = new Set(session.mcp.list().map((entry) => entry.name));
123243
+ } catch (error) {
123244
+ failed.push({
123245
+ step: "mcp.add",
123246
+ message: messageOf(error)
123247
+ });
123248
+ }
123249
+ for (const id of targets) {
123250
+ const live = isLive(id);
123251
+ if (!desiredUnknown) {
123252
+ for (const name of actualNames) {
123253
+ if (!isPluginMcpRuntimeName(name)) continue;
123254
+ if (pluginIdFromMcpRuntimeName(name) !== id) continue;
123255
+ if (live && desired[name] !== void 0) continue;
123256
+ try {
123257
+ await session.mcp.removeServer(name);
123258
+ applied.push({
123259
+ kind: "mcp.remove",
123260
+ name,
123261
+ session: sessionId
123262
+ });
123263
+ actualNames.delete(name);
123264
+ } catch (error) {
123265
+ failed.push({
123266
+ step: "mcp.remove",
123267
+ pluginId: id,
123268
+ message: messageOf(error)
123269
+ });
123270
+ }
123271
+ }
123272
+ if (live && options?.skipMcpAdd !== true) for (const [name, config] of Object.entries(desired)) {
123273
+ if (pluginIdFromMcpRuntimeName(name) !== id) continue;
123274
+ if (actualNames.has(name)) continue;
123275
+ try {
123276
+ await session.mcp.addServer(name, config);
123277
+ applied.push({
123278
+ kind: "mcp.add",
123279
+ name,
123280
+ session: sessionId
123281
+ });
123282
+ actualNames.add(name);
123283
+ } catch (error) {
123284
+ failed.push({
123285
+ step: "mcp.add",
123286
+ pluginId: id,
123287
+ message: messageOf(error)
123288
+ });
123289
+ }
123290
+ }
123291
+ }
123292
+ try {
123293
+ session.ejectPlugin(id);
123294
+ applied.push({
123295
+ kind: "skills.eject",
123296
+ name: id,
123297
+ session: sessionId
123298
+ });
123299
+ if (live) {
123300
+ const roots = this.plugins.pluginSkillRoots().filter((root) => root.plugin?.id === id);
123301
+ if (roots.length > 0) {
123302
+ await session.injectSkillRoots(roots);
123303
+ applied.push({
123304
+ kind: "skills.inject",
123305
+ name: id,
123306
+ session: sessionId
123307
+ });
123308
+ }
123309
+ }
123310
+ } catch (error) {
123311
+ failed.push({
123312
+ step: live ? "skills.inject" : "skills.eject",
123313
+ pluginId: id,
123314
+ message: messageOf(error)
123315
+ });
123316
+ }
123317
+ }
123318
+ }
123319
+ return {
123320
+ ok: failed.length === 0,
123321
+ sessions: this.sessions.size,
123322
+ applied,
123323
+ failed
123324
+ };
123325
+ }
123326
+ /**
123327
+ * Best-effort wrapper for hot-apply after a successful plugin write: the
123328
+ * mutation already landed, so a sync surprise must never fail the RPC or
123329
+ * the tool call that triggered it.
123330
+ */
123331
+ async syncSessionsBestEffort(changedIds, options) {
123332
+ try {
123333
+ await this.applyPluginChangesToSessions(changedIds, options);
123334
+ } catch (error) {
123335
+ log.warn("plugin hot-apply failed", { reason: error instanceof Error ? error.message : String(error) });
123336
+ }
123337
+ }
121400
123338
  async removePlugin({ id }) {
121401
123339
  await this.pluginsReady;
121402
123340
  this.assertPluginsLoaded();
121403
123341
  await this.plugins.remove(id);
121404
- for (const session of this.sessions.values()) try {
121405
- session.ejectPlugin(id);
121406
- } catch {}
123342
+ await this.applyPluginChangesToSessions([id]);
121407
123343
  }
121408
123344
  generateAgentsMd({ sessionId, ...payload }) {
121409
123345
  return this.sessionApi(sessionId).generateAgentsMd(payload);
@@ -121412,6 +123348,7 @@ var ScreamCore = class {
121412
123348
  await this.pluginsReady;
121413
123349
  this.assertPluginsLoaded();
121414
123350
  const record = await this.plugins.install(payload.source);
123351
+ await this.syncSessionsBestEffort([record.id], { skipMcpAdd: true });
121415
123352
  return this.plugins.summaries().find((s) => s.id === record.id);
121416
123353
  }
121417
123354
  async listPlugins(_) {
@@ -121423,16 +123360,19 @@ var ScreamCore = class {
121423
123360
  await this.pluginsReady;
121424
123361
  this.assertPluginsLoaded();
121425
123362
  await this.plugins.setEnabled(id, enabled);
123363
+ await this.syncSessionsBestEffort([id]);
121426
123364
  }
121427
123365
  async setPluginMcpServerEnabled({ id, server, enabled }) {
121428
123366
  await this.pluginsReady;
121429
123367
  this.assertPluginsLoaded();
121430
123368
  await this.plugins.setMcpServerEnabled(id, server, enabled);
123369
+ await this.syncSessionsBestEffort([id]);
121431
123370
  }
121432
123371
  async reloadPlugins(_) {
121433
123372
  try {
121434
123373
  const summary = await this.plugins.reload();
121435
123374
  this.pluginsLoadError = void 0;
123375
+ await this.syncSessionsBestEffort();
121436
123376
  return summary;
121437
123377
  } catch (error) {
121438
123378
  this.pluginsLoadError = error instanceof Error ? error : new Error(String(error));
@@ -121457,10 +123397,14 @@ var ScreamCore = class {
121457
123397
  });
121458
123398
  }
121459
123399
  async resolveRuntime(config) {
121460
- if (this.runtime !== void 0) return this.runtime;
121461
- const runtime = await createRuntimeConfig({ config });
121462
- this.runtime = runtime;
121463
- return runtime;
123400
+ const base = this.runtime ?? await createRuntimeConfig({ config });
123401
+ this.runtime = base;
123402
+ return {
123403
+ ...base,
123404
+ plugins: this.plugins,
123405
+ extensionRuntime: this.extensionRuntime,
123406
+ pluginSync: (changedIds, syncOptions) => this.applyPluginChangesToSessions(changedIds, syncOptions)
123407
+ };
121464
123408
  }
121465
123409
  resolveSessionSkillConfig(config) {
121466
123410
  const explicitDirs = this.skillDirs.length > 0 ? this.skillDirs : void 0;
@@ -127027,9 +128971,8 @@ function lerpGradient(t) {
127027
128971
  const b = Math.round(b0 + (b1 - b0) * localT);
127028
128972
  return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
127029
128973
  }
127030
- function buildStatusLine(streamingPhase, streamingStartTime, reconnectAttempt) {
128974
+ function buildStatusLine(streamingPhase, streamingStartTime) {
127031
128975
  if (streamingPhase === "idle") return t("status.idle");
127032
- if (reconnectAttempt > 0 && streamingPhase === "waiting") return chalk.hex("#E85454").bold("◎") + " " + chalk.hex("#E85454")(`${t("status.reconnecting")} ${String(reconnectAttempt)}`);
127033
128976
  let label;
127034
128977
  if (streamingPhase === "tool") label = t("status.tool");
127035
128978
  else if (streamingPhase === "waiting") label = t("status.waiting");
@@ -127207,7 +129150,7 @@ var FooterComponent = class {
127207
129150
  let rightText;
127208
129151
  if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
127209
129152
  else {
127210
- const statusLine = buildStatusLine(state.streamingPhase, state.streamingStartTime, state.reconnectAttempt);
129153
+ const statusLine = buildStatusLine(state.streamingPhase, state.streamingStartTime);
127211
129154
  const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
127212
129155
  const sessionUsage = state.sessionUsage ?? {
127213
129156
  inputOther: 0,
@@ -127302,7 +129245,7 @@ function isStreaming(state) {
127302
129245
  /** True when the session is busy and should reject state-changing operations.
127303
129246
  * Covers both active streaming and context compaction. */
127304
129247
  function isBusy(state) {
127305
- return isStreaming(state) || state.isCompacting;
129248
+ return isStreaming(state) || state.isCompacting || state.isSwitchingSession;
127306
129249
  }
127307
129250
  //#endregion
127308
129251
  //#region src/tui/constant/terminal.ts
@@ -129526,7 +131469,7 @@ async function guidedGoalSetup(host) {
129526
131469
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
129527
131470
  return;
129528
131471
  }
129529
- const { TextInputDialogComponent } = await import("./text-input-dialog-FGVh3qGR.mjs");
131472
+ const { TextInputDialogComponent } = await import("./text-input-dialog-B9uwX1Uy.mjs");
129530
131473
  const initialDesc = await promptText(host, TextInputDialogComponent, {
129531
131474
  title: t("goal.setup_title_initial"),
129532
131475
  subtitle: t("goal.setup_desc_hint"),
@@ -129547,7 +131490,7 @@ async function guidedGoalSetup(host) {
129547
131490
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
129548
131491
  }
129549
131492
  async function showGoalConfigWizard(host, session, objective, replace) {
129550
- const { TextInputDialogComponent } = await import("./text-input-dialog-FGVh3qGR.mjs");
131493
+ const { TextInputDialogComponent } = await import("./text-input-dialog-B9uwX1Uy.mjs");
129551
131494
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
129552
131495
  title: t("goal.wizard_title", { objective }),
129553
131496
  subtitle: t("goal.budget_turns_hint"),
@@ -131629,7 +133572,7 @@ var TruncatedOutputComponent = class {
131629
133572
  const remaining = contentLines.length - this.maxLines;
131630
133573
  const tail = this.maxLines <= 0 ? [] : contentLines.slice(-this.maxLines);
131631
133574
  const expandHint = this.hintFormatter ? this.hintFormatter(remaining) : t("shell.more_lines", { count: String(remaining) });
131632
- return [chalk.dim(expandHint), ...tail];
133575
+ return [...new Text(chalk.dim(expandHint), 2, 0).render(width), ...tail];
131633
133576
  }
131634
133577
  };
131635
133578
  const renderTruncated = (_toolCall, result, ctx) => {