initJsExtensions method

Future<void> initJsExtensions()

Loads installed JS extensions in the background (fire-and-forget from the constructor, like MCP boot — never blocks the REPL): build the store + host, wire the sinks, loadAll, register the tool surface + prompt section, and attach the hooks.

Attach ORDER matters: the constructor already wrapped approval + redaction, so JsExtensionHost.attachHooks lands OUTERMOST — the JS hooks run LAST (after redaction) and a Dart-side deny always wins.

Implementation

Future<void> initJsExtensions() async {
  final factory = config.extRuntimeFactory;
  if (!config.jsExtensionsEnabled || factory == null) return;
  final host = JsExtensionHost(
    env: _env,
    store: _extStore(),
    runtimeFactory: factory,
    bootstrapJs: config.extBootstrapJs,
    config: ExtHostConfig(
      redact: config.redactionPipeline == null
          ? null
          : (text) => config.redactionPipeline!.redact(text),
    ),
  );
  _ext.host = host;
  host.onLog = (line) => io.writeln('[ext] $line');
  host.onAppendNote = _appendExtNote;
  host.onFollowUp = (text) => _agent.followUp(UserMessage.text(text));
  host.onIoWrite = io.write;
  host.onIoWriteln = io.writeln;
  host.setToolSyncCallbacks(
    syncTools: _syncExtTools,
    rebuildPrompt: _extRefreshSurface,
  );
  final report = await host.loadAll(
    platform: ExtPlatformTag.cli,
    trustPrompt: io.isInteractive ? _extTrustPrompt : null,
    reservedNames: _extReservedNames(),
  );
  _reportExtLoad(report);
  if (host.tools.isNotEmpty) {
    _toolRegistry.registerAll(host.tools);
    _agent.state.tools = _toolRegistry.tools;
  }
  _extRefreshSurface();
  final resolution = _toolGate.resolution;
  if (resolution != null) {
    // Re-apply the availability decision to the fresh ext surface (same
    // path as /tools reload — mirrors _onMcpChanged).
    AgentCliTools(this).refilterMcpTools(resolution);
  }
  // pi mode (issue #679): the benchmark profile carries no JS
  // extension hook side effects — `beforeToolCall`/`afterToolCall`
  // hooks (logging, notes, follow-ups) stay off the agent. The tool
  // surface is already swept; this is the other half of the pin.
  if (!_piActive) {
    host.attachHooks(_agent);
  }
  // v1: fired once after the load completes; extension side effects land
  // through the session bridges (notes append once a session exists).
  await host.sessionStart();
}