@chosengeneration/light-code 0.2.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/cli.js +2333 -626
  2. package/dist/client/client.js +50 -47
  3. package/dist/client/guide/appearance-dark.svg +1 -0
  4. package/dist/client/guide/appearance-light.svg +1 -0
  5. package/dist/client/guide/approvals-dark.svg +1 -0
  6. package/dist/client/guide/approvals-light.svg +1 -0
  7. package/dist/client/guide/chat-dark.svg +1 -0
  8. package/dist/client/guide/chat-light.svg +1 -0
  9. package/dist/client/guide/expert-dark.svg +1 -0
  10. package/dist/client/guide/expert-light.svg +1 -0
  11. package/dist/client/guide/mcp-dark.svg +1 -0
  12. package/dist/client/guide/mcp-light.svg +1 -0
  13. package/dist/client/guide/network-dark.svg +1 -0
  14. package/dist/client/guide/network-light.svg +1 -0
  15. package/dist/client/guide/orientation-dark.svg +1 -0
  16. package/dist/client/guide/orientation-light.svg +1 -0
  17. package/dist/client/guide/privacy-dark.svg +1 -0
  18. package/dist/client/guide/privacy-light.svg +1 -0
  19. package/dist/client/guide/providers-dark.svg +1 -0
  20. package/dist/client/guide/providers-light.svg +1 -0
  21. package/dist/client/guide/python-dark.svg +1 -0
  22. package/dist/client/guide/python-light.svg +1 -0
  23. package/dist/client/guide/schedules-dark.svg +1 -0
  24. package/dist/client/guide/schedules-light.svg +1 -0
  25. package/dist/client/guide/search-dark.svg +1 -0
  26. package/dist/client/guide/search-light.svg +1 -0
  27. package/dist/client/guide/skills-dark.svg +1 -0
  28. package/dist/client/guide/skills-light.svg +1 -0
  29. package/dist/client/guide/tools-dark.svg +1 -0
  30. package/dist/client/guide/tools-light.svg +1 -0
  31. package/dist/server.js +1799 -499
  32. package/package.json +65 -63
package/dist/server.js CHANGED
@@ -1142,14 +1142,14 @@ var require_util = __commonJS({
1142
1142
  }
1143
1143
  const port = url2.port != null ? url2.port : url2.protocol === "https:" ? 443 : 80;
1144
1144
  let origin = url2.origin != null ? url2.origin : `${url2.protocol || ""}//${url2.hostname || ""}:${port}`;
1145
- let path24 = url2.path != null ? url2.path : `${url2.pathname || ""}${url2.search || ""}`;
1145
+ let path26 = url2.path != null ? url2.path : `${url2.pathname || ""}${url2.search || ""}`;
1146
1146
  if (origin[origin.length - 1] === "/") {
1147
1147
  origin = origin.slice(0, origin.length - 1);
1148
1148
  }
1149
- if (path24 && path24[0] !== "/") {
1150
- path24 = `/${path24}`;
1149
+ if (path26 && path26[0] !== "/") {
1150
+ path26 = `/${path26}`;
1151
1151
  }
1152
- return new URL(`${origin}${path24}`);
1152
+ return new URL(`${origin}${path26}`);
1153
1153
  }
1154
1154
  if (!isHttpOrHttpsPrefixed(url2.origin || url2.protocol)) {
1155
1155
  throw new InvalidArgumentError("Invalid URL protocol: the URL must start with `http:` or `https:`.");
@@ -2020,9 +2020,9 @@ var require_diagnostics = __commonJS({
2020
2020
  "undici:client:sendHeaders",
2021
2021
  (evt) => {
2022
2022
  const {
2023
- request: { method, path: path24, origin }
2023
+ request: { method, path: path26, origin }
2024
2024
  } = evt;
2025
- debugLog("sending request to %s %s%s", method, origin, path24);
2025
+ debugLog("sending request to %s %s%s", method, origin, path26);
2026
2026
  }
2027
2027
  );
2028
2028
  }
@@ -2040,14 +2040,14 @@ var require_diagnostics = __commonJS({
2040
2040
  "undici:request:headers",
2041
2041
  (evt) => {
2042
2042
  const {
2043
- request: { method, path: path24, origin },
2043
+ request: { method, path: path26, origin },
2044
2044
  response: { statusCode }
2045
2045
  } = evt;
2046
2046
  debugLog(
2047
2047
  "received response to %s %s%s - HTTP %d",
2048
2048
  method,
2049
2049
  origin,
2050
- path24,
2050
+ path26,
2051
2051
  statusCode
2052
2052
  );
2053
2053
  }
@@ -2056,23 +2056,23 @@ var require_diagnostics = __commonJS({
2056
2056
  "undici:request:trailers",
2057
2057
  (evt) => {
2058
2058
  const {
2059
- request: { method, path: path24, origin }
2059
+ request: { method, path: path26, origin }
2060
2060
  } = evt;
2061
- debugLog("trailers received from %s %s%s", method, origin, path24);
2061
+ debugLog("trailers received from %s %s%s", method, origin, path26);
2062
2062
  }
2063
2063
  );
2064
2064
  diagnosticsChannel.subscribe(
2065
2065
  "undici:request:error",
2066
2066
  (evt) => {
2067
2067
  const {
2068
- request: { method, path: path24, origin },
2068
+ request: { method, path: path26, origin },
2069
2069
  error: error51
2070
2070
  } = evt;
2071
2071
  debugLog(
2072
2072
  "request to %s %s%s errored - %s",
2073
2073
  method,
2074
2074
  origin,
2075
- path24,
2075
+ path26,
2076
2076
  error51.message
2077
2077
  );
2078
2078
  }
@@ -2227,7 +2227,7 @@ var require_request = __commonJS({
2227
2227
  };
2228
2228
  var Request = class {
2229
2229
  constructor(origin, {
2230
- path: path24,
2230
+ path: path26,
2231
2231
  method,
2232
2232
  body,
2233
2233
  headers,
@@ -2244,11 +2244,11 @@ var require_request = __commonJS({
2244
2244
  maxRedirections,
2245
2245
  typeOfService
2246
2246
  }, handler) {
2247
- if (typeof path24 !== "string") {
2247
+ if (typeof path26 !== "string") {
2248
2248
  throw new InvalidArgumentError("path must be a string");
2249
- } else if (path24[0] !== "/" && !(path24.startsWith("http://") || path24.startsWith("https://")) && method !== "CONNECT") {
2249
+ } else if (path26[0] !== "/" && !(path26.startsWith("http://") || path26.startsWith("https://")) && method !== "CONNECT") {
2250
2250
  throw new InvalidArgumentError("path must be an absolute URL or start with a slash");
2251
- } else if (invalidPathRegex.test(path24)) {
2251
+ } else if (invalidPathRegex.test(path26)) {
2252
2252
  throw new InvalidArgumentError("invalid request path");
2253
2253
  }
2254
2254
  if (typeof method !== "string") {
@@ -2323,7 +2323,7 @@ var require_request = __commonJS({
2323
2323
  this.completed = false;
2324
2324
  this.aborted = false;
2325
2325
  this.upgrade = upgrade || null;
2326
- this.path = query ? serializePathWithQuery(path24, query) : path24;
2326
+ this.path = query ? serializePathWithQuery(path26, query) : path26;
2327
2327
  this.origin = origin;
2328
2328
  this.protocol = getProtocolFromUrlString(origin);
2329
2329
  this.idempotent = idempotent == null ? method === "HEAD" || method === "GET" || method === "QUERY" : idempotent;
@@ -7409,7 +7409,7 @@ var require_client_h1 = __commonJS({
7409
7409
  }
7410
7410
  }
7411
7411
  function writeH1(client, request) {
7412
- const { method, path: path24, host, upgrade, blocking, reset } = request;
7412
+ const { method, path: path26, host, upgrade, blocking, reset } = request;
7413
7413
  let { body, headers, contentLength } = request;
7414
7414
  const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH" || method === "QUERY" || method === "PROPFIND" || method === "PROPPATCH";
7415
7415
  if (util.isFormDataLike(body)) {
@@ -7485,7 +7485,7 @@ var require_client_h1 = __commonJS({
7485
7485
  socket[kBlocking] = true;
7486
7486
  }
7487
7487
  setTypeOfService(socket, request);
7488
- let header = `${method} ${path24} HTTP/1.1\r
7488
+ let header = `${method} ${path26} HTTP/1.1\r
7489
7489
  `;
7490
7490
  if (typeof host === "string") {
7491
7491
  header += `host: ${host}\r
@@ -8566,7 +8566,7 @@ var require_client_h2 = __commonJS({
8566
8566
  const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout];
8567
8567
  const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout];
8568
8568
  const session = client[kHTTP2Session];
8569
- const { method, path: path24, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request;
8569
+ const { method, path: path26, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request;
8570
8570
  if (upgrade != null && upgrade !== "websocket") {
8571
8571
  util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`));
8572
8572
  return false;
@@ -8629,7 +8629,7 @@ var require_client_h2 = __commonJS({
8629
8629
  }
8630
8630
  headers[HTTP2_HEADER_METHOD] = "CONNECT";
8631
8631
  headers[HTTP2_HEADER_PROTOCOL] = "websocket";
8632
- headers[HTTP2_HEADER_PATH] = path24;
8632
+ headers[HTTP2_HEADER_PATH] = path26;
8633
8633
  if (protocol === "ws:" || protocol === "wss:") {
8634
8634
  headers[HTTP2_HEADER_SCHEME] = protocol === "ws:" ? "http" : "https";
8635
8635
  } else {
@@ -8651,7 +8651,7 @@ var require_client_h2 = __commonJS({
8651
8651
  setupUpgradeStream(stream, state);
8652
8652
  return true;
8653
8653
  }
8654
- headers[HTTP2_HEADER_PATH] = path24;
8654
+ headers[HTTP2_HEADER_PATH] = path26;
8655
8655
  headers[HTTP2_HEADER_SCHEME] = protocol === "http:" ? "http" : "https";
8656
8656
  const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH" || method === "QUERY" || method === "PROPFIND" || method === "PROPPATCH";
8657
8657
  let body = state.body;
@@ -11321,10 +11321,10 @@ var require_proxy_agent = __commonJS({
11321
11321
  };
11322
11322
  const {
11323
11323
  origin,
11324
- path: path24 = "/",
11324
+ path: path26 = "/",
11325
11325
  headers = {}
11326
11326
  } = opts;
11327
- opts.path = origin + path24;
11327
+ opts.path = origin + path26;
11328
11328
  if (!("host" in headers) && !("Host" in headers)) {
11329
11329
  const { host } = new URL(origin);
11330
11330
  headers.host = host;
@@ -13589,20 +13589,20 @@ var require_mock_utils = __commonJS({
13589
13589
  }
13590
13590
  return normalizedQp;
13591
13591
  }
13592
- function safeUrl(path24) {
13593
- if (typeof path24 !== "string") {
13594
- return path24;
13592
+ function safeUrl(path26) {
13593
+ if (typeof path26 !== "string") {
13594
+ return path26;
13595
13595
  }
13596
- const pathSegments = path24.split("?", 3);
13596
+ const pathSegments = path26.split("?", 3);
13597
13597
  if (pathSegments.length !== 2) {
13598
- return path24;
13598
+ return path26;
13599
13599
  }
13600
13600
  const qp = new URLSearchParams(pathSegments.pop());
13601
13601
  qp.sort();
13602
13602
  return [...pathSegments, qp.toString()].join("?");
13603
13603
  }
13604
- function matchKey(mockDispatch2, { path: path24, method, body, headers }) {
13605
- const pathMatch = matchValue(mockDispatch2.path, path24);
13604
+ function matchKey(mockDispatch2, { path: path26, method, body, headers }) {
13605
+ const pathMatch = matchValue(mockDispatch2.path, path26);
13606
13606
  const methodMatch = matchValue(mockDispatch2.method, method);
13607
13607
  const bodyMatch = typeof mockDispatch2.body !== "undefined" ? matchValue(mockDispatch2.body, body) : true;
13608
13608
  const headersMatch = matchHeaders(mockDispatch2, headers);
@@ -13629,8 +13629,8 @@ var require_mock_utils = __commonJS({
13629
13629
  const basePath = key.query ? serializePathWithQuery(key.path, key.query) : key.path;
13630
13630
  const resolvedPath = typeof basePath === "string" ? safeUrl(basePath) : basePath;
13631
13631
  const resolvedPathWithoutTrailingSlash = removeTrailingSlash(resolvedPath);
13632
- let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path: path24, ignoreTrailingSlash }) => {
13633
- return ignoreTrailingSlash ? matchValue(removeTrailingSlash(safeUrl(path24)), resolvedPathWithoutTrailingSlash) : matchValue(safeUrl(path24), resolvedPath);
13632
+ let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path: path26, ignoreTrailingSlash }) => {
13633
+ return ignoreTrailingSlash ? matchValue(removeTrailingSlash(safeUrl(path26)), resolvedPathWithoutTrailingSlash) : matchValue(safeUrl(path26), resolvedPath);
13634
13634
  });
13635
13635
  if (matchedMockDispatches.length === 0) {
13636
13636
  throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`);
@@ -13669,22 +13669,22 @@ var require_mock_utils = __commonJS({
13669
13669
  mockDispatches.splice(index, 1);
13670
13670
  }
13671
13671
  }
13672
- function removeTrailingSlash(path24) {
13673
- if (typeof path24 !== "string") {
13674
- return path24;
13672
+ function removeTrailingSlash(path26) {
13673
+ if (typeof path26 !== "string") {
13674
+ return path26;
13675
13675
  }
13676
- while (path24.endsWith("/")) {
13677
- path24 = path24.slice(0, -1);
13676
+ while (path26.endsWith("/")) {
13677
+ path26 = path26.slice(0, -1);
13678
13678
  }
13679
- if (path24.length === 0) {
13680
- path24 = "/";
13679
+ if (path26.length === 0) {
13680
+ path26 = "/";
13681
13681
  }
13682
- return path24;
13682
+ return path26;
13683
13683
  }
13684
13684
  function buildKey(opts) {
13685
- const { path: path24, method, body, headers, query } = opts;
13685
+ const { path: path26, method, body, headers, query } = opts;
13686
13686
  return {
13687
- path: path24,
13687
+ path: path26,
13688
13688
  method,
13689
13689
  body,
13690
13690
  headers,
@@ -14555,10 +14555,10 @@ var require_pending_interceptors_formatter = __commonJS({
14555
14555
  }
14556
14556
  format(pendingInterceptors) {
14557
14557
  const withPrettyHeaders = pendingInterceptors.map(
14558
- ({ method, path: path24, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
14558
+ ({ method, path: path26, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
14559
14559
  Method: method,
14560
14560
  Origin: origin,
14561
- Path: path24,
14561
+ Path: path26,
14562
14562
  "Status code": statusCode,
14563
14563
  Persistent: persist ? PERSISTENT : NOT_PERSISTENT,
14564
14564
  Invocations: timesInvoked,
@@ -14640,9 +14640,9 @@ var require_mock_agent = __commonJS({
14640
14640
  const acceptNonStandardSearchParameters = this[kMockAgentAcceptsNonStandardSearchParameters];
14641
14641
  const dispatchOpts = { ...opts };
14642
14642
  if (acceptNonStandardSearchParameters && dispatchOpts.path) {
14643
- const [path24, searchParams] = dispatchOpts.path.split("?");
14643
+ const [path26, searchParams] = dispatchOpts.path.split("?");
14644
14644
  const normalizedSearchParams = normalizeSearchParams(searchParams, acceptNonStandardSearchParameters);
14645
- dispatchOpts.path = `${path24}?${normalizedSearchParams}`;
14645
+ dispatchOpts.path = `${path26}?${normalizedSearchParams}`;
14646
14646
  }
14647
14647
  return this[kAgent].dispatch(dispatchOpts, handler);
14648
14648
  }
@@ -14770,8 +14770,8 @@ var require_snapshot_utils = __commonJS({
14770
14770
  match: new Set(matchHeaders.map((header) => caseSensitive ? header : header.toLowerCase()))
14771
14771
  };
14772
14772
  }
14773
- var crypto6 = runtimeFeatures.has("crypto") ? __require("node:crypto") : null;
14774
- var hashId = crypto6?.hash ? (value) => crypto6.hash("sha256", value, "base64url") : (value) => Buffer.from(value).toString("base64url");
14773
+ var crypto7 = runtimeFeatures.has("crypto") ? __require("node:crypto") : null;
14774
+ var hashId = crypto7?.hash ? (value) => crypto7.hash("sha256", value, "base64url") : (value) => Buffer.from(value).toString("base64url");
14775
14775
  function isUndiciHeaders(headers) {
14776
14776
  return Array.isArray(headers) && (headers.length & 1) === 0;
14777
14777
  }
@@ -15058,12 +15058,12 @@ var require_snapshot_recorder = __commonJS({
15058
15058
  * @return {Promise<void>} - Resolves when snapshots are loaded
15059
15059
  */
15060
15060
  async loadSnapshots(filePath) {
15061
- const path24 = filePath || this.#snapshotPath;
15062
- if (!path24) {
15061
+ const path26 = filePath || this.#snapshotPath;
15062
+ if (!path26) {
15063
15063
  throw new InvalidArgumentError("Snapshot path is required");
15064
15064
  }
15065
15065
  try {
15066
- const data = await readFile2(resolve(path24), "utf8");
15066
+ const data = await readFile2(resolve(path26), "utf8");
15067
15067
  const parsed = JSON.parse(data);
15068
15068
  if (Array.isArray(parsed)) {
15069
15069
  this.#snapshots.clear();
@@ -15077,7 +15077,7 @@ var require_snapshot_recorder = __commonJS({
15077
15077
  if (error51.code === "ENOENT") {
15078
15078
  this.#snapshots.clear();
15079
15079
  } else {
15080
- throw new UndiciError(`Failed to load snapshots from ${path24}`, { cause: error51 });
15080
+ throw new UndiciError(`Failed to load snapshots from ${path26}`, { cause: error51 });
15081
15081
  }
15082
15082
  }
15083
15083
  }
@@ -15088,11 +15088,11 @@ var require_snapshot_recorder = __commonJS({
15088
15088
  * @returns {Promise<void>} - Resolves when snapshots are saved
15089
15089
  */
15090
15090
  async saveSnapshots(filePath) {
15091
- const path24 = filePath || this.#snapshotPath;
15092
- if (!path24) {
15091
+ const path26 = filePath || this.#snapshotPath;
15092
+ if (!path26) {
15093
15093
  throw new InvalidArgumentError("Snapshot path is required");
15094
15094
  }
15095
- const resolvedPath = resolve(path24);
15095
+ const resolvedPath = resolve(path26);
15096
15096
  await mkdir(dirname(resolvedPath), { recursive: true });
15097
15097
  const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot]) => ({
15098
15098
  hash: hash2,
@@ -15729,15 +15729,15 @@ var require_redirect_handler = __commonJS({
15729
15729
  return;
15730
15730
  }
15731
15731
  const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)));
15732
- const path24 = search ? `${pathname}${search}` : pathname;
15733
- const redirectUrlString = `${origin}${path24}`;
15732
+ const path26 = search ? `${pathname}${search}` : pathname;
15733
+ const redirectUrlString = `${origin}${path26}`;
15734
15734
  for (const historyUrl of this.history) {
15735
15735
  if (historyUrl.toString() === redirectUrlString) {
15736
15736
  throw new InvalidArgumentError(`Redirect loop detected. Cannot redirect to ${origin}. This typically happens when using a Client or Pool with cross-origin redirects. Use an Agent for cross-origin redirects.`);
15737
15737
  }
15738
15738
  }
15739
15739
  this.opts.headers = cleanRequestHeaders(this.opts.headers, removeContentHeaders, this.opts.origin !== origin, this.stripHeadersOnRedirect, this.stripHeadersOnCrossOriginRedirect);
15740
- this.opts.path = path24;
15740
+ this.opts.path = path26;
15741
15741
  this.opts.origin = origin;
15742
15742
  this.opts.query = null;
15743
15743
  }
@@ -17565,10 +17565,10 @@ var require_cache_handler = __commonJS({
17565
17565
  }
17566
17566
  return locationUrl.pathname + locationUrl.search;
17567
17567
  }
17568
- function deleteCachedUri(store, cacheKey, path24) {
17568
+ function deleteCachedUri(store, cacheKey, path26) {
17569
17569
  deleteCachedValue(store, {
17570
17570
  ...cacheKey,
17571
- path: path24
17571
+ path: path26
17572
17572
  });
17573
17573
  for (let i = 0; i < util.safeHTTPMethods.length; i++) {
17574
17574
  const method = util.safeHTTPMethods[i];
@@ -17576,7 +17576,7 @@ var require_cache_handler = __commonJS({
17576
17576
  deleteCachedValue(store, {
17577
17577
  ...cacheKey,
17578
17578
  method,
17579
- path: path24
17579
+ path: path26
17580
17580
  });
17581
17581
  }
17582
17582
  }
@@ -17587,9 +17587,9 @@ var require_cache_handler = __commonJS({
17587
17587
  }
17588
17588
  const values = Array.isArray(headerValue) ? headerValue : [headerValue];
17589
17589
  for (let i = 0; i < values.length; i++) {
17590
- const path24 = getSameOriginPath(cacheKey, values[i]);
17591
- if (path24 !== void 0) {
17592
- deleteCachedUri(store, cacheKey, path24);
17590
+ const path26 = getSameOriginPath(cacheKey, values[i]);
17591
+ if (path26 !== void 0) {
17592
+ deleteCachedUri(store, cacheKey, path26);
17593
17593
  }
17594
17594
  }
17595
17595
  }
@@ -21462,10 +21462,10 @@ var require_subresource_integrity = __commonJS({
21462
21462
  var assert2 = __require("node:assert");
21463
21463
  var { runtimeFeatures } = require_runtime_features();
21464
21464
  var validSRIHashAlgorithmTokenSet = /* @__PURE__ */ new Map([["sha256", 0], ["sha384", 1], ["sha512", 2]]);
21465
- var crypto6;
21465
+ var crypto7;
21466
21466
  if (runtimeFeatures.has("crypto")) {
21467
- crypto6 = __require("node:crypto");
21468
- const cryptoHashes = crypto6.getHashes();
21467
+ crypto7 = __require("node:crypto");
21468
+ const cryptoHashes = crypto7.getHashes();
21469
21469
  if (cryptoHashes.length === 0) {
21470
21470
  validSRIHashAlgorithmTokenSet.clear();
21471
21471
  }
@@ -21555,7 +21555,7 @@ var require_subresource_integrity = __commonJS({
21555
21555
  return result;
21556
21556
  }
21557
21557
  var applyAlgorithmToBytes = (algorithm, bytes) => {
21558
- return crypto6.hash(algorithm, bytes, "base64");
21558
+ return crypto7.hash(algorithm, bytes, "base64");
21559
21559
  };
21560
21560
  function caseSensitiveMatch(actualValue, expectedValue) {
21561
21561
  let actualValueLength = actualValue.length;
@@ -22586,13 +22586,13 @@ var require_fetch = __commonJS({
22586
22586
  function dispatch({ body }) {
22587
22587
  const url2 = requestCurrentURL(request);
22588
22588
  const agent = fetchParams.controller.dispatcher;
22589
- const path24 = url2.pathname + url2.search;
22589
+ const path26 = url2.pathname + url2.search;
22590
22590
  const hasTrailingQuestionMark = url2.search.length === 0 && url2.href[url2.href.length - url2.hash.length - 1] === "?";
22591
22591
  return dispatchWithProtocolPreference(body);
22592
22592
  function dispatchWithProtocolPreference(body2, allowH2) {
22593
22593
  return new Promise((resolve, reject2) => agent.dispatch(
22594
22594
  {
22595
- path: hasTrailingQuestionMark ? `${path24}?` : path24,
22595
+ path: hasTrailingQuestionMark ? `${path26}?` : path26,
22596
22596
  origin: url2.origin,
22597
22597
  method: request.method,
22598
22598
  body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body2,
@@ -23504,9 +23504,9 @@ var require_util4 = __commonJS({
23504
23504
  }
23505
23505
  }
23506
23506
  }
23507
- function validateCookiePath(path24) {
23508
- for (let i = 0; i < path24.length; ++i) {
23509
- const code = path24.charCodeAt(i);
23507
+ function validateCookiePath(path26) {
23508
+ for (let i = 0; i < path26.length; ++i) {
23509
+ const code = path26.charCodeAt(i);
23510
23510
  if (code < 32 || // exclude CTLs (0-31)
23511
23511
  code > 126 || // exclude non-ascii and DEL
23512
23512
  code === 59) {
@@ -24538,7 +24538,7 @@ var require_connection = __commonJS({
24538
24538
  var { WebsocketFrameSend } = require_frame();
24539
24539
  var assert2 = __require("node:assert");
24540
24540
  var { runtimeFeatures } = require_runtime_features();
24541
- var crypto6 = runtimeFeatures.has("crypto") ? __require("node:crypto") : null;
24541
+ var crypto7 = runtimeFeatures.has("crypto") ? __require("node:crypto") : null;
24542
24542
  var warningEmitted = false;
24543
24543
  function establishWebSocketConnection(url2, protocols, client, handler, options) {
24544
24544
  const requestURL = url2;
@@ -24558,7 +24558,7 @@ var require_connection = __commonJS({
24558
24558
  const headersList = getHeadersList(new Headers2(options.headers));
24559
24559
  request.headersList = headersList;
24560
24560
  }
24561
- const keyValue = crypto6.randomBytes(16).toString("base64");
24561
+ const keyValue = crypto7.randomBytes(16).toString("base64");
24562
24562
  request.headersList.append("sec-websocket-key", keyValue, true);
24563
24563
  request.headersList.append("sec-websocket-version", "13", true);
24564
24564
  for (const protocol of protocols) {
@@ -24598,7 +24598,7 @@ var require_connection = __commonJS({
24598
24598
  return;
24599
24599
  }
24600
24600
  const secWSAccept = response.headersList.get("Sec-WebSocket-Accept");
24601
- const digest = crypto6.hash("sha1", keyValue + uid, "base64");
24601
+ const digest = crypto7.hash("sha1", keyValue + uid, "base64");
24602
24602
  if (secWSAccept !== digest) {
24603
24603
  failWebsocketConnection(handler, 1002, "Incorrect hash received in Sec-WebSocket-Accept header.");
24604
24604
  return;
@@ -26878,11 +26878,11 @@ var require_undici = __commonJS({
26878
26878
  if (typeof opts.path !== "string") {
26879
26879
  throw new InvalidArgumentError("invalid opts.path");
26880
26880
  }
26881
- let path24 = opts.path;
26881
+ let path26 = opts.path;
26882
26882
  if (!opts.path.startsWith("/")) {
26883
- path24 = `/${path24}`;
26883
+ path26 = `/${path26}`;
26884
26884
  }
26885
- url2 = new URL(util.parseOrigin(url2).origin + path24);
26885
+ url2 = new URL(util.parseOrigin(url2).origin + path26);
26886
26886
  } else {
26887
26887
  if (!opts) {
26888
26888
  opts = typeof url2 === "object" ? url2 : {};
@@ -30187,8 +30187,8 @@ var require_utils2 = __commonJS({
30187
30187
  }
30188
30188
  return ind;
30189
30189
  }
30190
- function removeDotSegments(path24) {
30191
- let input = path24;
30190
+ function removeDotSegments(path26) {
30191
+ let input = path26;
30192
30192
  const output = [];
30193
30193
  let nextSlash = -1;
30194
30194
  let len = 0;
@@ -30440,8 +30440,8 @@ var require_schemes = __commonJS({
30440
30440
  wsComponent.secure = void 0;
30441
30441
  }
30442
30442
  if (wsComponent.resourceName) {
30443
- const [path24, query] = wsComponent.resourceName.split("?");
30444
- wsComponent.path = path24 && path24 !== "/" ? path24 : void 0;
30443
+ const [path26, query] = wsComponent.resourceName.split("?");
30444
+ wsComponent.path = path26 && path26 !== "/" ? path26 : void 0;
30445
30445
  wsComponent.query = query;
30446
30446
  wsComponent.resourceName = void 0;
30447
30447
  }
@@ -33840,12 +33840,12 @@ var require_dist = __commonJS({
33840
33840
  throw new Error(`Unknown format "${name}"`);
33841
33841
  return f;
33842
33842
  };
33843
- function addFormats(ajv, list, fs20, exportName) {
33843
+ function addFormats(ajv, list, fs22, exportName) {
33844
33844
  var _a3;
33845
33845
  var _b;
33846
33846
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
33847
33847
  for (const f of list)
33848
- ajv.addFormat(f, fs20[f]);
33848
+ ajv.addFormat(f, fs22[f]);
33849
33849
  }
33850
33850
  module.exports = exports = formatsPlugin;
33851
33851
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -33858,8 +33858,8 @@ var require_windows = __commonJS({
33858
33858
  "../../node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/windows.js"(exports, module) {
33859
33859
  module.exports = isexe;
33860
33860
  isexe.sync = sync;
33861
- var fs20 = __require("fs");
33862
- function checkPathExt(path24, options) {
33861
+ var fs22 = __require("fs");
33862
+ function checkPathExt(path26, options) {
33863
33863
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
33864
33864
  if (!pathext) {
33865
33865
  return true;
@@ -33870,25 +33870,25 @@ var require_windows = __commonJS({
33870
33870
  }
33871
33871
  for (var i = 0; i < pathext.length; i++) {
33872
33872
  var p = pathext[i].toLowerCase();
33873
- if (p && path24.substr(-p.length).toLowerCase() === p) {
33873
+ if (p && path26.substr(-p.length).toLowerCase() === p) {
33874
33874
  return true;
33875
33875
  }
33876
33876
  }
33877
33877
  return false;
33878
33878
  }
33879
- function checkStat(stat, path24, options) {
33879
+ function checkStat(stat, path26, options) {
33880
33880
  if (!stat.isSymbolicLink() && !stat.isFile()) {
33881
33881
  return false;
33882
33882
  }
33883
- return checkPathExt(path24, options);
33883
+ return checkPathExt(path26, options);
33884
33884
  }
33885
- function isexe(path24, options, cb) {
33886
- fs20.stat(path24, function(er, stat) {
33887
- cb(er, er ? false : checkStat(stat, path24, options));
33885
+ function isexe(path26, options, cb) {
33886
+ fs22.stat(path26, function(er, stat) {
33887
+ cb(er, er ? false : checkStat(stat, path26, options));
33888
33888
  });
33889
33889
  }
33890
- function sync(path24, options) {
33891
- return checkStat(fs20.statSync(path24), path24, options);
33890
+ function sync(path26, options) {
33891
+ return checkStat(fs22.statSync(path26), path26, options);
33892
33892
  }
33893
33893
  }
33894
33894
  });
@@ -33898,14 +33898,14 @@ var require_mode = __commonJS({
33898
33898
  "../../node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/mode.js"(exports, module) {
33899
33899
  module.exports = isexe;
33900
33900
  isexe.sync = sync;
33901
- var fs20 = __require("fs");
33902
- function isexe(path24, options, cb) {
33903
- fs20.stat(path24, function(er, stat) {
33901
+ var fs22 = __require("fs");
33902
+ function isexe(path26, options, cb) {
33903
+ fs22.stat(path26, function(er, stat) {
33904
33904
  cb(er, er ? false : checkStat(stat, options));
33905
33905
  });
33906
33906
  }
33907
- function sync(path24, options) {
33908
- return checkStat(fs20.statSync(path24), options);
33907
+ function sync(path26, options) {
33908
+ return checkStat(fs22.statSync(path26), options);
33909
33909
  }
33910
33910
  function checkStat(stat, options) {
33911
33911
  return stat.isFile() && checkMode(stat, options);
@@ -33929,7 +33929,7 @@ var require_mode = __commonJS({
33929
33929
  // ../../node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/index.js
33930
33930
  var require_isexe = __commonJS({
33931
33931
  "../../node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/index.js"(exports, module) {
33932
- var fs20 = __require("fs");
33932
+ var fs22 = __require("fs");
33933
33933
  var core;
33934
33934
  if (process.platform === "win32" || global.TESTING_WINDOWS) {
33935
33935
  core = require_windows();
@@ -33938,7 +33938,7 @@ var require_isexe = __commonJS({
33938
33938
  }
33939
33939
  module.exports = isexe;
33940
33940
  isexe.sync = sync;
33941
- function isexe(path24, options, cb) {
33941
+ function isexe(path26, options, cb) {
33942
33942
  if (typeof options === "function") {
33943
33943
  cb = options;
33944
33944
  options = {};
@@ -33948,7 +33948,7 @@ var require_isexe = __commonJS({
33948
33948
  throw new TypeError("callback not provided");
33949
33949
  }
33950
33950
  return new Promise(function(resolve, reject2) {
33951
- isexe(path24, options || {}, function(er, is) {
33951
+ isexe(path26, options || {}, function(er, is) {
33952
33952
  if (er) {
33953
33953
  reject2(er);
33954
33954
  } else {
@@ -33957,7 +33957,7 @@ var require_isexe = __commonJS({
33957
33957
  });
33958
33958
  });
33959
33959
  }
33960
- core(path24, options || {}, function(er, is) {
33960
+ core(path26, options || {}, function(er, is) {
33961
33961
  if (er) {
33962
33962
  if (er.code === "EACCES" || options && options.ignoreErrors) {
33963
33963
  er = null;
@@ -33967,9 +33967,9 @@ var require_isexe = __commonJS({
33967
33967
  cb(er, is);
33968
33968
  });
33969
33969
  }
33970
- function sync(path24, options) {
33970
+ function sync(path26, options) {
33971
33971
  try {
33972
- return core.sync(path24, options || {});
33972
+ return core.sync(path26, options || {});
33973
33973
  } catch (er) {
33974
33974
  if (options && options.ignoreErrors || er.code === "EACCES") {
33975
33975
  return false;
@@ -33985,7 +33985,7 @@ var require_isexe = __commonJS({
33985
33985
  var require_which = __commonJS({
33986
33986
  "../../node_modules/.pnpm/which@2.0.2/node_modules/which/which.js"(exports, module) {
33987
33987
  var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
33988
- var path24 = __require("path");
33988
+ var path26 = __require("path");
33989
33989
  var COLON = isWindows ? ";" : ":";
33990
33990
  var isexe = require_isexe();
33991
33991
  var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" });
@@ -34023,7 +34023,7 @@ var require_which = __commonJS({
34023
34023
  return opt.all && found.length ? resolve(found) : reject2(getNotFoundError(cmd));
34024
34024
  const ppRaw = pathEnv[i];
34025
34025
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
34026
- const pCmd = path24.join(pathPart, cmd);
34026
+ const pCmd = path26.join(pathPart, cmd);
34027
34027
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
34028
34028
  resolve(subStep(p, i, 0));
34029
34029
  });
@@ -34050,7 +34050,7 @@ var require_which = __commonJS({
34050
34050
  for (let i = 0; i < pathEnv.length; i++) {
34051
34051
  const ppRaw = pathEnv[i];
34052
34052
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
34053
- const pCmd = path24.join(pathPart, cmd);
34053
+ const pCmd = path26.join(pathPart, cmd);
34054
34054
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
34055
34055
  for (let j = 0; j < pathExt.length; j++) {
34056
34056
  const cur = p + pathExt[j];
@@ -34098,7 +34098,7 @@ var require_path_key = __commonJS({
34098
34098
  var require_resolveCommand = __commonJS({
34099
34099
  "../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/resolveCommand.js"(exports, module) {
34100
34100
  "use strict";
34101
- var path24 = __require("path");
34101
+ var path26 = __require("path");
34102
34102
  var which = require_which();
34103
34103
  var getPathKey = require_path_key();
34104
34104
  function resolveCommandAttempt(parsed, withoutPathExt) {
@@ -34116,7 +34116,7 @@ var require_resolveCommand = __commonJS({
34116
34116
  try {
34117
34117
  resolved = which.sync(parsed.command, {
34118
34118
  path: env[getPathKey({ env })],
34119
- pathExt: withoutPathExt ? path24.delimiter : void 0
34119
+ pathExt: withoutPathExt ? path26.delimiter : void 0
34120
34120
  });
34121
34121
  } catch (e) {
34122
34122
  } finally {
@@ -34125,7 +34125,7 @@ var require_resolveCommand = __commonJS({
34125
34125
  }
34126
34126
  }
34127
34127
  if (resolved) {
34128
- resolved = path24.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
34128
+ resolved = path26.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
34129
34129
  }
34130
34130
  return resolved;
34131
34131
  }
@@ -34179,8 +34179,8 @@ var require_shebang_command = __commonJS({
34179
34179
  if (!match) {
34180
34180
  return null;
34181
34181
  }
34182
- const [path24, argument] = match[0].replace(/#! ?/, "").split(" ");
34183
- const binary = path24.split("/").pop();
34182
+ const [path26, argument] = match[0].replace(/#! ?/, "").split(" ");
34183
+ const binary = path26.split("/").pop();
34184
34184
  if (binary === "env") {
34185
34185
  return argument;
34186
34186
  }
@@ -34193,16 +34193,16 @@ var require_shebang_command = __commonJS({
34193
34193
  var require_readShebang = __commonJS({
34194
34194
  "../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/readShebang.js"(exports, module) {
34195
34195
  "use strict";
34196
- var fs20 = __require("fs");
34196
+ var fs22 = __require("fs");
34197
34197
  var shebangCommand = require_shebang_command();
34198
34198
  function readShebang(command) {
34199
34199
  const size = 150;
34200
34200
  const buffer = Buffer.alloc(size);
34201
34201
  let fd;
34202
34202
  try {
34203
- fd = fs20.openSync(command, "r");
34204
- fs20.readSync(fd, buffer, 0, size, 0);
34205
- fs20.closeSync(fd);
34203
+ fd = fs22.openSync(command, "r");
34204
+ fs22.readSync(fd, buffer, 0, size, 0);
34205
+ fs22.closeSync(fd);
34206
34206
  } catch (e) {
34207
34207
  }
34208
34208
  return shebangCommand(buffer.toString());
@@ -34215,7 +34215,7 @@ var require_readShebang = __commonJS({
34215
34215
  var require_parse2 = __commonJS({
34216
34216
  "../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/parse.js"(exports, module) {
34217
34217
  "use strict";
34218
- var path24 = __require("path");
34218
+ var path26 = __require("path");
34219
34219
  var resolveCommand = require_resolveCommand();
34220
34220
  var escape2 = require_escape();
34221
34221
  var readShebang = require_readShebang();
@@ -34240,7 +34240,7 @@ var require_parse2 = __commonJS({
34240
34240
  const needsShell = !isExecutableRegExp.test(commandFile);
34241
34241
  if (parsed.options.forceShell || needsShell) {
34242
34242
  const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
34243
- parsed.command = path24.normalize(parsed.command);
34243
+ parsed.command = path26.normalize(parsed.command);
34244
34244
  parsed.command = escape2.command(parsed.command);
34245
34245
  parsed.args = parsed.args.map((arg) => escape2.argument(arg, needsDoubleEscapeMetaChars));
34246
34246
  const shellCommand = [parsed.command].concat(parsed.args).join(" ");
@@ -34455,201 +34455,11 @@ var require_content_type = __commonJS({
34455
34455
  });
34456
34456
 
34457
34457
  // src/server.ts
34458
- import fs19 from "node:fs/promises";
34458
+ import fs21 from "node:fs/promises";
34459
34459
  import {
34460
34460
  createServer
34461
34461
  } from "node:http";
34462
- import path23 from "node:path";
34463
-
34464
- // src/identity.ts
34465
- import crypto from "node:crypto";
34466
- var SingleUserIdentity = class _SingleUserIdentity {
34467
- describe = "single user (local)";
34468
- static PRINCIPAL = { id: "local", displayName: "Local user" };
34469
- /** Long-lived, minted per server run, only ever sent in an `Authorization` header. */
34470
- sessionToken = crypto.randomBytes(32).toString("base64url");
34471
- /**
34472
- * Single-use and short-lived, because it travels in the launch URL's fragment where it
34473
- * can end up in shell history or a terminal scrollback (§14).
34474
- */
34475
- handoffToken = crypto.randomBytes(32).toString("base64url");
34476
- handoffExpiresAt = Date.now() + 1e4;
34477
- get launchToken() {
34478
- if (this.handoffToken === void 0) throw new Error("handoff token already consumed");
34479
- return this.handoffToken;
34480
- }
34481
- /**
34482
- * Exchanges the handoff token for the session token, once.
34483
- *
34484
- * Cleared on the first attempt whether or not it matched: a wrong guess is either a bug
34485
- * or an attack, and in both cases the right answer is that this token is now spent.
34486
- */
34487
- redeemHandoff(presented) {
34488
- const expected = this.handoffToken;
34489
- const expiresAt = this.handoffExpiresAt;
34490
- this.handoffToken = void 0;
34491
- if (expected === void 0 || Date.now() > expiresAt) return void 0;
34492
- return timingSafeEquals(presented, expected) ? this.sessionToken : void 0;
34493
- }
34494
- async authenticate(request) {
34495
- const header = request.headers.authorization;
34496
- if (header === void 0 || !header.startsWith("Bearer ")) return void 0;
34497
- return timingSafeEquals(header.slice("Bearer ".length), this.sessionToken) ? _SingleUserIdentity.PRINCIPAL : void 0;
34498
- }
34499
- };
34500
- function timingSafeEquals(a, b) {
34501
- const left = Buffer.from(a);
34502
- const right = Buffer.from(b);
34503
- if (left.length !== right.length) return false;
34504
- return crypto.timingSafeEqual(left, right);
34505
- }
34506
- function storageKeyFor(principal) {
34507
- return crypto.createHash("sha256").update(principal.id).digest("hex").slice(0, 32);
34508
- }
34509
-
34510
- // src/roles.ts
34511
- var ADMIN_ONLY_MESSAGES = [
34512
- // Credentials and where inference goes (invariant 5: `profiles`, `activeProfileId`).
34513
- "saveProfile",
34514
- "deleteProfile",
34515
- "duplicateProfile",
34516
- "setActiveProfile",
34517
- "testConnection",
34518
- "importConfig",
34519
- "exportConfig",
34520
- // Processes this machine will spawn.
34521
- "saveMcpServer",
34522
- "saveMcpServers",
34523
- "deleteMcpServer",
34524
- "duplicateMcpServer",
34525
- "restartMcpServer",
34526
- "connectMcpServer",
34527
- "setMcpServerEnabled",
34528
- "setMcpToolPermission",
34529
- // Names an interpreter and a tools directory — `python.uvPath` is on invariant 5 for this.
34530
- "setPython",
34531
- "deletePythonTool",
34532
- "approvePythonTool",
34533
- // Names an executable that costs money to run.
34534
- "setExpert",
34535
- "assessJunior",
34536
- "clearAssessment",
34537
- // TLS trust and client identity for every outbound connection.
34538
- "saveNetwork",
34539
- // Where the corpus is sent, and what is embedded into it.
34540
- "saveSearchConnection",
34541
- "deleteSearchConnection",
34542
- "setActiveSearchConnection",
34543
- "saveEmbedder",
34544
- "setDispatcher",
34545
- "startIndexing",
34546
- "indexDocs",
34547
- "clearDocsIndex",
34548
- "syncVectorStore",
34549
- // Reading beyond the workspace, and where skills come from.
34550
- "setReadRoots",
34551
- "saveSkillDirs",
34552
- "deleteSkillFile",
34553
- // Unattended execution with a pre-granted tool list.
34554
- "saveSchedule",
34555
- "deleteSchedule",
34556
- "setScheduleEnabled",
34557
- "runScheduleNow",
34558
- "duplicateSchedule",
34559
- // Approvals are stored per workspace but govern what runs without asking.
34560
- "setAutoApprove",
34561
- "revokeAllowedTool",
34562
- "revokeAllowedCommand"
34563
- ];
34564
- var ADMIN_ONLY = new Set(ADMIN_ONLY_MESSAGES);
34565
- var PERSONAL_SETTINGS = /* @__PURE__ */ new Set([
34566
- "setMode",
34567
- "setAccentColor",
34568
- "setExpertColor",
34569
- "setMaxIterations",
34570
- "setTaskExpertLimits"
34571
- ]);
34572
- function isAdminOnly(messageType) {
34573
- if (PERSONAL_SETTINGS.has(messageType)) return false;
34574
- if (ADMIN_ONLY.has(messageType)) return true;
34575
- return /^(save|set|delete|duplicate|clear|restart|connect|revoke|import|export)/.test(messageType);
34576
- }
34577
- var SINGLE_USER_POLICY = {
34578
- shared: false,
34579
- roleFor: () => "admin"
34580
- };
34581
- function refusalFor(messageType) {
34582
- return `"${messageType}" changes configuration that the administrator owns on a shared server, so it was not applied. Everything about your own session \u2014 chatting, editing, the mode and appearance \u2014 is unaffected. Ask whoever runs this server if a setting needs changing.`;
34583
- }
34584
-
34585
- // src/security.ts
34586
- var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
34587
- function checkRequest(request, policy, options) {
34588
- const host = request.headers.host;
34589
- if (host === void 0 || !policy.allowedHosts.includes(host.toLowerCase())) {
34590
- return {
34591
- status: 421,
34592
- reason: `Host "${host ?? "(absent)"}" is not one this server answers to. This is what blocks DNS rebinding.`
34593
- };
34594
- }
34595
- const origin = request.headers.origin;
34596
- if (origin !== void 0 && !policy.allowedOrigins.includes(origin.toLowerCase())) {
34597
- return { status: 403, reason: `Origin "${origin}" is not allowed.` };
34598
- }
34599
- const fetchSite = request.headers["sec-fetch-site"];
34600
- if (typeof fetchSite === "string" && fetchSite !== "same-origin" && fetchSite !== "none") {
34601
- return { status: 403, reason: `Cross-site request (Sec-Fetch-Site: ${fetchSite}) is not allowed.` };
34602
- }
34603
- const method = (request.method ?? "GET").toUpperCase();
34604
- if (options.requireOrigin && !SAFE_METHODS.has(method) && origin === void 0) {
34605
- return { status: 403, reason: `Missing Origin header on a ${method}.` };
34606
- }
34607
- return void 0;
34608
- }
34609
- function securityHeaders() {
34610
- return {
34611
- "Content-Security-Policy": [
34612
- "default-src 'none'",
34613
- "script-src 'self'",
34614
- // The UI styles through the CSSOM rather than inline attributes, but the browser
34615
- // build also needs a stylesheet for the page shell.
34616
- "style-src 'self' 'unsafe-inline'",
34617
- "img-src 'self' data:",
34618
- "font-src 'self'",
34619
- "connect-src 'self'",
34620
- "frame-ancestors 'none'",
34621
- "base-uri 'none'",
34622
- "form-action 'none'"
34623
- ].join("; "),
34624
- "X-Content-Type-Options": "nosniff",
34625
- "Referrer-Policy": "no-referrer",
34626
- // Nothing here needs a camera, a microphone or a location.
34627
- "Permissions-Policy": "camera=(), microphone=(), geolocation=(), interest-cohort=()",
34628
- "Cache-Control": "no-store"
34629
- // Deliberately no Access-Control-Allow-Origin: no other origin may read these replies.
34630
- };
34631
- }
34632
- function reject(response, rejected) {
34633
- response.writeHead(rejected.status, { "Content-Type": "text/plain", ...securityHeaders() });
34634
- response.end(rejected.reason);
34635
- }
34636
- async function readJsonBody(request, maxBytes = 32 * 1024 * 1024) {
34637
- const chunks = [];
34638
- let total = 0;
34639
- for await (const chunk of request) {
34640
- const buffer = chunk;
34641
- total += buffer.length;
34642
- if (total > maxBytes) throw new Error("Request body too large.");
34643
- chunks.push(buffer);
34644
- }
34645
- if (total === 0) return void 0;
34646
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
34647
- }
34648
-
34649
- // src/session.ts
34650
- import { watch as fsWatch } from "node:fs";
34651
- import fs18 from "node:fs/promises";
34652
- import path22 from "node:path";
34462
+ import path25 from "node:path";
34653
34463
 
34654
34464
  // ../../packages/core/dist/platform/http.js
34655
34465
  var import_undici = __toESM(require_undici(), 1);
@@ -35583,10 +35393,10 @@ function mergeDefs(...defs) {
35583
35393
  function cloneDef(schema) {
35584
35394
  return mergeDefs(schema._zod.def);
35585
35395
  }
35586
- function getElementAtPath(obj, path24) {
35587
- if (!path24)
35396
+ function getElementAtPath(obj, path26) {
35397
+ if (!path26)
35588
35398
  return obj;
35589
- return path24.reduce((acc, key) => acc?.[key], obj);
35399
+ return path26.reduce((acc, key) => acc?.[key], obj);
35590
35400
  }
35591
35401
  function promiseAllObject(promisesObj) {
35592
35402
  const keys = Object.keys(promisesObj);
@@ -35995,11 +35805,11 @@ function explicitlyAborted(x, startIndex = 0) {
35995
35805
  }
35996
35806
  return false;
35997
35807
  }
35998
- function prefixIssues(path24, issues) {
35808
+ function prefixIssues(path26, issues) {
35999
35809
  return issues.map((iss) => {
36000
35810
  var _a3;
36001
35811
  (_a3 = iss).path ?? (_a3.path = []);
36002
- iss.path.unshift(path24);
35812
+ iss.path.unshift(path26);
36003
35813
  return iss;
36004
35814
  });
36005
35815
  }
@@ -36146,16 +35956,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
36146
35956
  }
36147
35957
  function formatError(error51, mapper = (issue2) => issue2.message) {
36148
35958
  const fieldErrors = { _errors: [] };
36149
- const processError = (error52, path24 = []) => {
35959
+ const processError = (error52, path26 = []) => {
36150
35960
  for (const issue2 of error52.issues) {
36151
35961
  if (issue2.code === "invalid_union" && issue2.errors.length) {
36152
- issue2.errors.map((issues) => processError({ issues }, [...path24, ...issue2.path]));
35962
+ issue2.errors.map((issues) => processError({ issues }, [...path26, ...issue2.path]));
36153
35963
  } else if (issue2.code === "invalid_key") {
36154
- processError({ issues: issue2.issues }, [...path24, ...issue2.path]);
35964
+ processError({ issues: issue2.issues }, [...path26, ...issue2.path]);
36155
35965
  } else if (issue2.code === "invalid_element") {
36156
- processError({ issues: issue2.issues }, [...path24, ...issue2.path]);
35966
+ processError({ issues: issue2.issues }, [...path26, ...issue2.path]);
36157
35967
  } else {
36158
- const fullpath = [...path24, ...issue2.path];
35968
+ const fullpath = [...path26, ...issue2.path];
36159
35969
  if (fullpath.length === 0) {
36160
35970
  fieldErrors._errors.push(mapper(issue2));
36161
35971
  } else {
@@ -36182,17 +35992,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
36182
35992
  }
36183
35993
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
36184
35994
  const result = { errors: [] };
36185
- const processError = (error52, path24 = []) => {
35995
+ const processError = (error52, path26 = []) => {
36186
35996
  var _a3, _b;
36187
35997
  for (const issue2 of error52.issues) {
36188
35998
  if (issue2.code === "invalid_union" && issue2.errors.length) {
36189
- issue2.errors.map((issues) => processError({ issues }, [...path24, ...issue2.path]));
35999
+ issue2.errors.map((issues) => processError({ issues }, [...path26, ...issue2.path]));
36190
36000
  } else if (issue2.code === "invalid_key") {
36191
- processError({ issues: issue2.issues }, [...path24, ...issue2.path]);
36001
+ processError({ issues: issue2.issues }, [...path26, ...issue2.path]);
36192
36002
  } else if (issue2.code === "invalid_element") {
36193
- processError({ issues: issue2.issues }, [...path24, ...issue2.path]);
36003
+ processError({ issues: issue2.issues }, [...path26, ...issue2.path]);
36194
36004
  } else {
36195
- const fullpath = [...path24, ...issue2.path];
36005
+ const fullpath = [...path26, ...issue2.path];
36196
36006
  if (fullpath.length === 0) {
36197
36007
  result.errors.push(mapper(issue2));
36198
36008
  continue;
@@ -36224,8 +36034,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
36224
36034
  }
36225
36035
  function toDotPath(_path) {
36226
36036
  const segs = [];
36227
- const path24 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
36228
- for (const seg of path24) {
36037
+ const path26 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
36038
+ for (const seg of path26) {
36229
36039
  if (typeof seg === "number")
36230
36040
  segs.push(`[${seg}]`);
36231
36041
  else if (typeof seg === "symbol")
@@ -48917,13 +48727,13 @@ function resolveRef(ref, ctx) {
48917
48727
  if (!ref.startsWith("#")) {
48918
48728
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
48919
48729
  }
48920
- const path24 = ref.slice(1).split("/").filter(Boolean);
48921
- if (path24.length === 0) {
48730
+ const path26 = ref.slice(1).split("/").filter(Boolean);
48731
+ if (path26.length === 0) {
48922
48732
  return ctx.rootSchema;
48923
48733
  }
48924
48734
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
48925
- if (path24[0] === defsKey) {
48926
- const key = path24[1];
48735
+ if (path26[0] === defsKey) {
48736
+ const key = path26[1];
48927
48737
  if (!key || !ctx.defs[key]) {
48928
48738
  throw new Error(`Reference not found: ${ref}`);
48929
48739
  }
@@ -49367,6 +49177,12 @@ function resolveToolPermission(toolName, namespacedName, disabledTools, alwaysAl
49367
49177
  function namespacedToolName(serverName, toolName) {
49368
49178
  return `${serverName}__${toolName}`;
49369
49179
  }
49180
+ function parseNamespacedToolName(name) {
49181
+ const index = name.indexOf("__");
49182
+ if (index <= 0)
49183
+ return void 0;
49184
+ return { serverName: name.slice(0, index), toolName: name.slice(index + 2) };
49185
+ }
49370
49186
  var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "npx.cmd", "pnpm", "pnpm.cmd", "pnpx", "bunx", "uvx", "yarn", "yarn.cmd"]);
49371
49187
  function isPackageRunnerCommand(command) {
49372
49188
  const base = command.split(/[\\/]/).pop()?.toLowerCase() ?? "";
@@ -49408,6 +49224,20 @@ var scheduleSchema = external_exports.object({
49408
49224
  * Control tools are always available regardless; they perform no work.
49409
49225
  */
49410
49226
  allowedTools: external_exports.array(external_exports.string()),
49227
+ /**
49228
+ * Which skills this run is told about, by name.
49229
+ *
49230
+ * **Absent means all of them**, which is what every schedule written before this existed
49231
+ * means, and the only reading that cannot silently take knowledge away from a job that was
49232
+ * working. An empty array is a real choice — "this run needs none" — and is honoured.
49233
+ *
49234
+ * Why a list rather than the retrieval the chat uses: a scheduled run's tools are an
49235
+ * allowlist the user ticked, and it may well not include `search_docs`, so telling the run
49236
+ * that notes exist and to go and search for them can leave it with nothing to search with.
49237
+ * Choosing the relevant ones up front is also simply better for a job that does the same
49238
+ * thing every night — it knows in advance which conventions apply, where the chat cannot.
49239
+ */
49240
+ allowedSkills: external_exports.array(external_exports.string()).optional(),
49411
49241
  /**
49412
49242
  * When the timer will next run this, in epoch ms.
49413
49243
  *
@@ -49443,6 +49273,12 @@ var scheduleSchema = external_exports.object({
49443
49273
  var MAX_REMEMBERED_RUNS = 20;
49444
49274
  var schedulesSchema = external_exports.record(external_exports.string(), scheduleSchema);
49445
49275
  var ALWAYS_AVAILABLE_TO_SCHEDULES = ["attempt_completion", "notify"];
49276
+ function skillsForSchedule(skills, allowed) {
49277
+ if (allowed === void 0)
49278
+ return [...skills];
49279
+ const wanted = new Set(allowed);
49280
+ return skills.filter((skill) => wanted.has(skill.name));
49281
+ }
49446
49282
 
49447
49283
  // ../../packages/core/dist/providers/types.js
49448
49284
  var wireFormatSchema = external_exports.enum(["openai", "anthropic", "gemini"]);
@@ -49621,7 +49457,45 @@ var expertConfigSchema = external_exports.object({
49621
49457
  * The backstop for when the CLI reports no cost — a spend limit cannot count what it is
49622
49458
  * not told the price of, and an unpriced consultation still costs money.
49623
49459
  */
49624
- maxConsultations: external_exports.number().int().min(0)
49460
+ maxConsultations: external_exports.number().int().min(0),
49461
+ /**
49462
+ * Whether this plan reports a per-consultation cost.
49463
+ *
49464
+ * Learned rather than configured, and learned from real consultations rather than from a
49465
+ * probe — asking the CLI "do you report cost?" means making a call, and the first call in a
49466
+ * session is the expensive one. So it is recorded the first time a consultation comes back
49467
+ * with or without `total_cost_usd`.
49468
+ *
49469
+ * Absent means not yet known. It matters because a spend cap cannot bind on a plan that
49470
+ * reports no cost: `usd` stays zero, the limit is never reached, and the only control that
49471
+ * actually holds is the consultation count. A cap that silently never fires is worse than no
49472
+ * cap, because it is believed.
49473
+ */
49474
+ reportsCost: external_exports.boolean(),
49475
+ /**
49476
+ * Refresh the expert's cache while a task is open, rather than paying a cold start later.
49477
+ *
49478
+ * The cache is one hour and that TTL is Anthropic's, not ours. A trivial resumed consultation
49479
+ * before it lapses costs about a fiftieth of the cold start it avoids.
49480
+ *
49481
+ * Off by default, and it must stay that way: it spends with nobody at the screen, which is
49482
+ * the one property this product is careful about everywhere else. Its cost is counted in the
49483
+ * meter like anything else.
49484
+ */
49485
+ keepAlive: external_exports.boolean(),
49486
+ /**
49487
+ * What a consultation costs on this plan, measured rather than assumed.
49488
+ *
49489
+ * The published figures came from one plan on one day. An enterprise agreement, a
49490
+ * subscription or a gateway can each report something different — and those numbers are what
49491
+ * the budget is set from and what the expert is told when it plans to fit.
49492
+ */
49493
+ pricing: external_exports.object({
49494
+ coldUsd: external_exports.number().min(0).optional(),
49495
+ resumedUsd: external_exports.number().min(0).optional(),
49496
+ measuredAt: external_exports.number(),
49497
+ reportsCost: external_exports.boolean()
49498
+ })
49625
49499
  }).partial();
49626
49500
  var vectorStoreKindSchema = external_exports.enum(["opensearch", "qdrant", "chroma"]);
49627
49501
  var vectorStoreSchema = external_exports.object({
@@ -49710,14 +49584,30 @@ var skillsConfigSchema = external_exports.object({
49710
49584
  }).partial();
49711
49585
  var retrievalConfigSchema = external_exports.object({
49712
49586
  /**
49713
- * Off by default, and that is a real default rather than caution.
49587
+ * **On by default since 0.33.0**, at the user's request: looking a tool up first is the
49588
+ * behaviour they want, and a corporate install with several MCP servers is the case this
49589
+ * product is actually deployed into.
49714
49590
  *
49715
- * The dispatcher trades a smaller prompt for less reliable tool calls: models are
49716
- * measurably better at native tool-calling than at naming a tool inside `call_tool`.
49717
- * It earns its place when a large MCP catalogue genuinely dominates the context window,
49718
- * which is a minority of installs.
49591
+ * The cost it trades against is real and unchanged models are measurably better at
49592
+ * native tool-calling than at naming a tool inside `call_tool`. Two things keep that from
49593
+ * biting a small install: nothing is hidden unless there is something to hide (a workspace
49594
+ * with no MCP or Python tools registers no dispatcher tools at all, so it pays nothing),
49595
+ * and the switch is one click away in Settings → Search, which reports exactly how many
49596
+ * tools it is hiding.
49719
49597
  */
49720
49598
  dispatcher: external_exports.boolean(),
49599
+ /**
49600
+ * The same treatment for skills: their names and descriptions leave the prompt and are
49601
+ * found with `search_docs` instead.
49602
+ *
49603
+ * On by default, and paired with `dispatcher` rather than independent of it in practice —
49604
+ * but a separate key because the trade is different. A tool's schema is large and its name
49605
+ * is guessable from the task; a skill's summary is one line and is the *only* thing that
49606
+ * makes the model aware the skill exists at all. So hiding skills saves less and risks
49607
+ * more, which is why a count and a standing instruction to search stay in the prompt even
49608
+ * when the list does not — see `renderSkillsHintForPrompt`.
49609
+ */
49610
+ skills: external_exports.boolean(),
49721
49611
  /**
49722
49612
  * Where the documentation corpus is indexed. Absent means `search_docs` still works,
49723
49613
  * matching names and descriptions from the live registry instead of by meaning — see
@@ -49725,6 +49615,12 @@ var retrievalConfigSchema = external_exports.object({
49725
49615
  */
49726
49616
  docsIndex: external_exports.string()
49727
49617
  }).partial();
49618
+ function dispatcherEnabled(retrieval) {
49619
+ return retrieval?.dispatcher !== false;
49620
+ }
49621
+ function skillRetrievalEnabled(retrieval) {
49622
+ return dispatcherEnabled(retrieval) && retrieval?.skills !== false;
49623
+ }
49728
49624
  var embedderConfigSchema = external_exports.object({
49729
49625
  profileId: external_exports.string().min(1),
49730
49626
  model: external_exports.string().min(1),
@@ -49791,6 +49687,18 @@ var configSchema = external_exports.object({
49791
49687
  */
49792
49688
  schedules: schedulesSchema,
49793
49689
  activeProfileId: external_exports.string(),
49690
+ /**
49691
+ * The profile that writes Python tool source, when it should not be the chat model.
49692
+ *
49693
+ * A cheap model is fine at deciding a tool is needed and describing it, and much worse at
49694
+ * writing the file. Naming a profile here splits the two: the chat model sends a
49695
+ * specification and this one produces the source, which goes through the ordinary approval
49696
+ * prompt showing the real bytes.
49697
+ *
49698
+ * Absent means the chat model writes it, which is the behaviour every release so far has
49699
+ * had. User-scope only for the same reason as `profiles`: it names where inference goes.
49700
+ */
49701
+ programmingProfileId: external_exports.string(),
49794
49702
  certDir: external_exports.string(),
49795
49703
  python: pythonConfigSchema,
49796
49704
  /**
@@ -49860,10 +49768,291 @@ function parseConfig(raw) {
49860
49768
  return result.data;
49861
49769
  }
49862
49770
 
49771
+ // ../../packages/core/dist/session/variables.js
49772
+ var sessionVariableSchema = external_exports.object({
49773
+ name: external_exports.string().min(1),
49774
+ value: external_exports.string(),
49775
+ /** Shown beside the value. For "which one of these is the staging URL". */
49776
+ description: external_exports.string().optional()
49777
+ });
49778
+ var sessionVariablesSchema = external_exports.array(sessionVariableSchema);
49779
+ var VALID_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
49780
+ function isValidVariableName(name) {
49781
+ return VALID_NAME.test(name);
49782
+ }
49783
+ function resolveSessionVariables(adminVariables, userVariables) {
49784
+ const byName = /* @__PURE__ */ new Map();
49785
+ for (const variable of userVariables) {
49786
+ byName.set(variable.name, { ...variable, scope: "user" });
49787
+ }
49788
+ for (const variable of adminVariables) {
49789
+ const displaced = byName.get(variable.name);
49790
+ byName.set(variable.name, {
49791
+ ...variable,
49792
+ scope: "admin",
49793
+ ...displaced !== void 0 ? { overriddenUserValue: displaced.value } : {}
49794
+ });
49795
+ }
49796
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
49797
+ }
49798
+ function toEnvironment(variables) {
49799
+ const env = {};
49800
+ for (const variable of variables) {
49801
+ if (!isValidVariableName(variable.name))
49802
+ continue;
49803
+ env[variable.name] = variable.value;
49804
+ }
49805
+ return env;
49806
+ }
49807
+
49808
+ // ../../packages/core/dist/python/codeGenerator.js
49809
+ function buildCodeGenerationPrompt(request) {
49810
+ const lines = [
49811
+ "Write one complete Python file implementing the tool described below.",
49812
+ "",
49813
+ "Requirements, all load-bearing:",
49814
+ "- Define a function named `run`. It is the entry point and nothing else is called.",
49815
+ "- Annotate every parameter and the return type. The tool\u2019s schema is derived from those",
49816
+ " hints, so an unannotated parameter cannot be passed by the caller.",
49817
+ "- Write a module docstring. It becomes the tool description the model reads when choosing",
49818
+ " this tool, so say what it does, not how.",
49819
+ "- Document parameters in a Google-style `Args:` block.",
49820
+ "- Declare any third-party dependency in a PEP 723 inline block. Standard library needs none.",
49821
+ "",
49822
+ "**Return the file and nothing else.** No explanation, no fenced code block, no preamble.",
49823
+ "Anything that is not Python will be written to the file verbatim and fail to parse.",
49824
+ "",
49825
+ `Tool name: ${request.toolName}`,
49826
+ "",
49827
+ "What it must do:",
49828
+ request.specification
49829
+ ];
49830
+ if (request.existingSource !== void 0 && request.existingSource.length > 0) {
49831
+ lines.push("", "This tool already exists. Change what the requirement asks for and leave the rest alone \u2014", "return the whole file, including the parts you did not touch.", "", "Current file:", request.existingSource);
49832
+ }
49833
+ return lines.join("\n");
49834
+ }
49835
+ function unwrapFencedSource(text) {
49836
+ const trimmed = text.trim();
49837
+ if (!trimmed.startsWith("```"))
49838
+ return text;
49839
+ const firstNewline = trimmed.indexOf("\n");
49840
+ if (firstNewline === -1)
49841
+ return text;
49842
+ const opening = trimmed.slice(0, firstNewline).trim();
49843
+ if (!/^```[a-zA-Z0-9]*$/.test(opening))
49844
+ return text;
49845
+ if (!trimmed.endsWith("```"))
49846
+ return text;
49847
+ return trimmed.slice(firstNewline + 1, trimmed.length - 3).replace(/\s+$/, "") + "\n";
49848
+ }
49849
+
49850
+ // ../../packages/core/dist/review/types.js
49851
+ function describeSubmission(request) {
49852
+ const what = request.kind === "python-tool" ? "tool" : "skill";
49853
+ return [
49854
+ `Submitted "${request.name}" for review. It is not saved and not callable yet.`,
49855
+ "",
49856
+ `An administrator has to read the ${what} and approve it before it can run. This is not an`,
49857
+ "error and there is nothing to retry \u2014 submitting again would only add a second copy to the",
49858
+ "queue. Tell the user it is waiting for approval and carry on with whatever else the task",
49859
+ "needs."
49860
+ ].join("\n");
49861
+ }
49862
+
49863
+ // ../../packages/core/dist/expert/pricing.js
49864
+ var PRICING_PROBE = "Reply with the single word: OK";
49865
+ function pricingForPrompt(pricing) {
49866
+ if (pricing === void 0 || !pricing.reportsCost)
49867
+ return void 0;
49868
+ const cold = pricing.coldUsd;
49869
+ const resumed = pricing.resumedUsd;
49870
+ if (cold === void 0 || resumed === void 0)
49871
+ return void 0;
49872
+ return `Measured on this deployment: the first consultation of a task costs about ${money(cold)}, and each one after it about ${money(resumed)} because it resumes the same session. Plan accordingly \u2014 make the first one carry the task, and do not repeat context afterwards.`;
49873
+ }
49874
+ function money(value) {
49875
+ return value >= 0.01 ? `$${value.toFixed(2)}` : `$${value.toFixed(4)}`;
49876
+ }
49877
+
49878
+ // ../../packages/core/dist/guide/steps.js
49879
+ var GUIDE_STEPS = [
49880
+ {
49881
+ id: "orientation",
49882
+ title: "Where everything is",
49883
+ opensPanel: true,
49884
+ completionEvents: ["onCommand:lightCode.openPanel"],
49885
+ altText: "The VS Code window: the Light Code icon in the activity bar, the chat panel, and the new-task, history, settings and guide buttons in its header.",
49886
+ body: [
49887
+ "Light Code lives in one sidebar panel. The chat is the whole product; the gear opens eleven settings tabs, and the question mark reopens this guide whenever you want it.",
49888
+ "The numbers in the picture are the four things worth knowing before anything else."
49889
+ ]
49890
+ },
49891
+ {
49892
+ id: "providers",
49893
+ title: "Providers - point it at a model",
49894
+ tab: "providers",
49895
+ completionEvents: ["onContext:lightCode.hasProvider"],
49896
+ altText: "The Providers tab, showing the profile list and the fields for editing one: preset, label, base URL, authentication, API key, model and Test connection.",
49897
+ body: [
49898
+ "Nothing ships configured. There are no default endpoints, so a fresh install contacts nothing until you fill this in.",
49899
+ "**Preset** prefills a base URL and wire format - OpenAI-compatible, Anthropic, Gemini, DeepSeek - and every field stays editable for a gateway that fronts one differently. **Authentication** is a separate axis: an API key, or Apigee client-certificate mTLS with a token grant. **Model** is fetched from your gateway and always typeable, because many return nothing. Keep one profile per gateway and switch between them from the chat header.",
49900
+ "**Test connection** is the field worth using first: it loads certificates, gets a token, lists models, and tells you which of the three failed."
49901
+ ]
49902
+ },
49903
+ {
49904
+ id: "network",
49905
+ title: "Network - certificates, once, for everything",
49906
+ tab: "network",
49907
+ completionEvents: ["onStepSelected"],
49908
+ altText: "The Network tab, showing certificate directory, CA certificate, client certificate and key, PFX bundle, passphrase, and the verify-TLS toggle.",
49909
+ body: [
49910
+ "If your company intercepts TLS or issues client certificates, this is the only place you set that up. It applies to every connection: the gateway, the token endpoint, MCP over HTTP, the vector store and the embedder.",
49911
+ "**CA certificate** is added to the public roots rather than replacing them, so trusting your corporate root does not cost you every other host. **Certificate and key** - or a **PFX bundle**, which is what Windows PKI usually issues - identify you. Any single connection can override either.",
49912
+ "**Verify TLS certificates** can be turned off, and the panel says plainly what that costs: an interceptor can read and change the traffic, API key included. Add the CA instead."
49913
+ ]
49914
+ },
49915
+ {
49916
+ id: "chat",
49917
+ title: "The chat - ask for something real",
49918
+ opensPanel: true,
49919
+ completionEvents: ["onContext:lightCode.hasChatted"],
49920
+ altText: "The chat header, with the mode selector, the expert budget, and the four header buttons labelled; below it, the composer.",
49921
+ body: [
49922
+ "Type a request. It reads files, searches, edits and runs commands, one step at a time, and stops when it is done or when it needs you.",
49923
+ "**@** names a file directly. Paste a screenshot, or drop a Word, Excel, PDF or HTML file in. Long output is truncated with a handle it can re-read, so a huge log does not eat the window - the bar above the composer shows what has.",
49924
+ "**Mode** picks what it may do: Code edits and runs, Ask is read-only, Junior brings the expert in. **History** keeps every past task, and reopening one restores the whole transcript."
49925
+ ]
49926
+ },
49927
+ {
49928
+ id: "approvals",
49929
+ title: "Approvals - nothing happens without you",
49930
+ tab: "approvals",
49931
+ completionEvents: ["onStepSelected"],
49932
+ altText: "The Approvals tab, showing four auto-approve toggles all off, the always-allowed command and tool lists, extra readable folders, and the maximum-steps setting.",
49933
+ body: [
49934
+ "Every tool call is shown before it runs, as ground truth: the real command, the computed diff, the actual source. Never the model's description of what it means to do. **Deny** is a real answer - it goes back as a result and the turn continues.",
49935
+ "This tab is where standing permission is granted and, more importantly, taken back. The four toggles skip the prompt by category and **all ship off**. Below them are the grants you made in the chat: always-allowed commands, always-allowed MCP tools, and folders outside the workspace it may read.",
49936
+ "Command matching is **exact, byte for byte**. Allowing `npm test` never allows `npm test && rm -rf /`.",
49937
+ "Before its first edit to a task it snapshots the workspace, so you can roll the whole thing back."
49938
+ ]
49939
+ },
49940
+ {
49941
+ id: "mcp",
49942
+ title: "MCP - connect the servers you already run",
49943
+ tab: "mcp",
49944
+ completionEvents: ["onStepSelected"],
49945
+ altText: "The MCP tab, showing two servers with health, per-tool Always/Ask/Never controls, and the JSON configuration box.",
49946
+ body: [
49947
+ "Standard `mcpServers` configuration, so a config from another client pastes in unchanged. stdio or HTTP, inferred from whether you gave a command or a URL.",
49948
+ "Servers connect when the panel opens and show health, so a mistyped command is visible immediately rather than the first time something needs it. Every tool is namespaced `server__tool`, and each one has its own **Always / Ask / Never** - one server can expose forty.",
49949
+ "Secrets go in as `${secret:NAME}` and are resolved from the OS keychain at spawn time, never written into the file."
49950
+ ]
49951
+ },
49952
+ {
49953
+ id: "python",
49954
+ title: "Python - let it write its own tools",
49955
+ tab: "python",
49956
+ completionEvents: ["onStepSelected"],
49957
+ altText: "The Python tab, showing the enable toggle, uv path, environment choice, tools folder, package index, timeout, and a created tool with its content-hash approval note.",
49958
+ body: [
49959
+ "It can write a Python tool mid-conversation and call it from the next message. Dependencies are declared in the file and installed with `uv`; the schema comes from your type hints, so there is no metadata to keep in step.",
49960
+ "**Python environment** prefers your project's own venv, because that is where your internal libraries already are. **Package index** can point at an internal mirror, or refuse the network entirely.",
49961
+ "This is the sharpest surface in the product, so it is off by default and creating a tool **always** prompts with the full source - no toggle skips it. Approval pins a hash of exactly what you saw; a file edited outside is refused and reported, and tools live in `.lightcode/tools/` so they land in git and get reviewed."
49962
+ ]
49963
+ },
49964
+ {
49965
+ id: "skills",
49966
+ title: "Skills - teach it your conventions",
49967
+ tab: "skills",
49968
+ completionEvents: ["onStepSelected"],
49969
+ altText: "The Skills tab, showing two skills, a note that they are found by searching rather than listing, the writable skills folder, extra read-only folders, the problems list, and the approval note.",
49970
+ body: [
49971
+ "A skill is a markdown file with a name and a description. The body is never in the prompt - it is read with `read_file` when a task actually calls for it, so a skill can be as long as you like.",
49972
+ "By default the summaries are not in the prompt either: the assistant searches for a relevant note with `search_docs`, the same way it finds tools. What stays is a count and an instruction to look, so it still knows notes exist - a description nobody sees is a note nobody reads. Switch it off in **Search** if you would rather every summary sat in the prompt.",
49973
+ 'This is the answer to "it does not know about our internal libraries". It offers to write one when you explain something durable, and offers to correct one when something contradicts it - a stale skill is worse than a missing one.',
49974
+ "You get a writable folder plus any number of read-only ones, such as a shared team folder, with PATH-style precedence and shadowing reported rather than silently applied. Writing a skill needs approval too: it is prose that steers every future turn."
49975
+ ]
49976
+ },
49977
+ {
49978
+ id: "search",
49979
+ title: "Search - find things by meaning",
49980
+ tab: "search",
49981
+ completionEvents: ["onStepSelected"],
49982
+ altText: "The Search tab, showing the backend choice, connection fields, embedding profile, the index button, the two look-things-up toggles for tools and skills, index copying, and query limits.",
49983
+ body: [
49984
+ "Indexing is optional, ships disabled, and is **the largest thing Light Code ever sends anywhere**: it uploads the contents of your workspace to the embedding endpoint you name. It says so, and where to, before the first upload.",
49985
+ "**Qdrant** and **Chroma** run locally if you would rather nothing left the machine; **OpenSearch** is usually the one your company already has. Embeddings reuse a provider profile, so there is no second set of credentials. You can **copy an index between backends**, so changing your mind later does not orphan what you indexed.",
49986
+ "**Looking things up rather than listing them is the default.** MCP and Python tool schemas, and skill summaries, stay out of the prompt; the assistant finds them with `search_docs` and calls them through `call_tool`. Nothing is registered when there is nothing to hide, so a workspace with no MCP servers and no skills pays nothing for it. The tab shows how many things it is hiding, and either half can be switched off - models do call a tool listed in the prompt slightly more reliably than one named through a dispatcher."
49987
+ ]
49988
+ },
49989
+ {
49990
+ id: "tools",
49991
+ title: "Tools - everything it can call",
49992
+ tab: "tools",
49993
+ completionEvents: ["onStepSelected"],
49994
+ altText: "The Tools tab, showing the search box and the catalogue grouped into built-in, MCP and Python tools, with the looked-up badge explained.",
49995
+ body: [
49996
+ "One read-only list of every tool available right now: the built-in nine, everything your MCP servers expose, and the Python tools it has written. Search matches descriptions as well as names, so you can look for what you want done rather than what it is called.",
49997
+ "A **looked up** badge means the tool is kept out of the system prompt to save space - the default for MCP and Python tools. It is still callable: the assistant searches for it and calls it by name. A shorter prompt is not a shorter tool list; withholding a capability is what Approvals and modes are for."
49998
+ ]
49999
+ },
50000
+ {
50001
+ id: "expert",
50002
+ title: "Expert - spend less on the hard parts",
50003
+ tab: "expert",
50004
+ completionEvents: ["onStepSelected"],
50005
+ altText: "The Expert tab, showing the enable toggle, command and model, the per-task spend and consultation limits, cost estimate, skill assessment, and the read-only tool restriction.",
50006
+ body: [
50007
+ "In **Junior mode** a cheap model does the work and consults Claude, through the Claude CLI, on the parts that need it. The expert plans, sets checkpoints, and reviews each one as the junior finishes it.",
50008
+ "It is read-only by construction - Read, Grep and Glob, never edit or execute - so a second agent can never act outside the approval gate. It keeps one session per task, which makes the first consultation the expensive one and every later one about nineteen times cheaper.",
50009
+ "**Budget per task** caps both spend and number of consultations, and the same control sits in the chat header so you can raise it mid-task. The expert is told what is left and plans to fit, gives you a cost estimate up front, and can assess how your primary model is doing."
50010
+ ]
50011
+ },
50012
+ {
50013
+ id: "schedules",
50014
+ title: "Schedules - let it run on its own",
50015
+ tab: "schedules",
50016
+ completionEvents: ["onStepSelected"],
50017
+ altText: "The Schedules tab, showing a schedule name, prompt and interval, the file-permission, tool and skill filters for unattended runs, and the run history.",
50018
+ body: [
50019
+ "A prompt on a timer. Runs in the background without touching the chat you are in, and keeps running with the panel closed.",
50020
+ "Nobody is present to approve anything, so an unattended run does not inherit your auto-approve settings. Permission is granted **per schedule**: files are read-only unless you say otherwise, and you pick exactly which tools it may call. Creating Python tools or skills is never available to a schedule at all - model-authored code with no one watching is the one thing that stays out of reach.",
50021
+ "**A schedule names the skills it needs** rather than searching for them, under *What it should know*. Its tool list may not include `search_docs`, and a run that comes up empty has nobody to notice. All skills are included until you narrow it.",
50022
+ "Every run is logged with its full transcript, and `notify` raises a toast when a run has something to say."
50023
+ ]
50024
+ },
50025
+ {
50026
+ id: "appearance",
50027
+ title: "Appearance - make it yours",
50028
+ tab: "appearance",
50029
+ completionEvents: ["onStepSelected"],
50030
+ altText: "The Appearance tab, showing the accent colour swatches, the expert colour swatches, and the reduced-motion toggle.",
50031
+ body: [
50032
+ "The panel follows your editor theme. Two colours are yours to set: the **accent**, used for anything actionable, and the **expert** colour, which marks authorship - text in it is Claude's words rather than your primary model's.",
50033
+ "Text on either is computed rather than fixed, so it stays readable whatever you pick. Motion follows your OS reduced-motion setting, and can be turned off here regardless."
50034
+ ]
50035
+ },
50036
+ {
50037
+ id: "privacy",
50038
+ title: "What it does not do",
50039
+ completionEvents: ["onStepSelected"],
50040
+ altText: "A diagram of what leaves the machine: your gateway and MCP servers, plus the vector store and embedder only if Search is enabled; then the four things Light Code never does, and a warning that nothing is sandboxed.",
50041
+ body: [
50042
+ "No telemetry. No update checks. No default endpoints - a fresh install contacts nothing. No remote assets in the panel.",
50043
+ "The only hosts it ever reaches are the ones you configured: your gateway, your MCP servers, and - only if you turn Search on - your vector store and embedding endpoint.",
50044
+ "Two things are stated plainly rather than glossed. **Indexing is the largest egress in the product**: enabling it sends your workspace to the embedder. And **nothing is sandboxed** - commands, Python tools and MCP servers run as you, with your permissions, and Light Code does not protect you from another process running as the same user. Approval is the real boundary, which is why it is per-invocation and why every toggle ships off.",
50045
+ "Source, issues and the full security section: [github.com/chosengenerationdev/light-code](https://github.com/chosengenerationdev/light-code)"
50046
+ ]
50047
+ }
50048
+ ];
50049
+
49863
50050
  // ../../packages/core/dist/config/scopes.js
49864
50051
  var USER_SCOPE_ONLY_KEYS = [
49865
50052
  "profiles",
49866
50053
  "activeProfileId",
50054
+ // Names where inference goes, exactly as the other two do.
50055
+ "programmingProfileId",
49867
50056
  "certDir",
49868
50057
  // The whole block, not just uvPath: toolsDir and venvPath also name where code is found
49869
50058
  // and run from, and dynamicTools decides whether model-authored code runs at all.
@@ -50407,7 +50596,7 @@ function describeTlsError(error51) {
50407
50596
  }
50408
50597
 
50409
50598
  // ../../packages/core/dist/providers/auth/certs.js
50410
- import crypto2 from "node:crypto";
50599
+ import crypto from "node:crypto";
50411
50600
  import fs4 from "node:fs/promises";
50412
50601
  import path5 from "node:path";
50413
50602
  var CertError = class extends Error {
@@ -50444,12 +50633,12 @@ function assertKeyMatchesCert(cert, key, passphrase) {
50444
50633
  let publicKey;
50445
50634
  let privateKey;
50446
50635
  try {
50447
- publicKey = new crypto2.X509Certificate(cert).publicKey;
50636
+ publicKey = new crypto.X509Certificate(cert).publicKey;
50448
50637
  } catch (error51) {
50449
50638
  throw new CertError(`The certificate could not be parsed: ${error51 instanceof Error ? error51.message : String(error51)}`);
50450
50639
  }
50451
50640
  try {
50452
- privateKey = crypto2.createPrivateKey(passphrase !== void 0 ? { key, passphrase } : { key });
50641
+ privateKey = crypto.createPrivateKey(passphrase !== void 0 ? { key, passphrase } : { key });
50453
50642
  } catch (error51) {
50454
50643
  const message = error51 instanceof Error ? error51.message : String(error51);
50455
50644
  if (/bad decrypt|bad password|passphrase/i.test(message)) {
@@ -50459,14 +50648,14 @@ function assertKeyMatchesCert(cert, key, passphrase) {
50459
50648
  }
50460
50649
  const probe2 = Buffer.from("light-code-key-match-probe");
50461
50650
  try {
50462
- const signature = crypto2.sign(null, probe2, privateKey);
50463
- if (!crypto2.verify(null, probe2, publicKey, signature)) {
50651
+ const signature = crypto.sign(null, probe2, privateKey);
50652
+ if (!crypto.verify(null, probe2, publicKey, signature)) {
50464
50653
  throw new CertError("The private key does not match the certificate.");
50465
50654
  }
50466
50655
  } catch (error51) {
50467
50656
  if (error51 instanceof CertError)
50468
50657
  throw error51;
50469
- const derived = crypto2.createPublicKey(privateKey).export({ type: "spki", format: "der" });
50658
+ const derived = crypto.createPublicKey(privateKey).export({ type: "spki", format: "der" });
50470
50659
  const expected = publicKey.export({ type: "spki", format: "der" });
50471
50660
  if (!derived.equals(expected)) {
50472
50661
  throw new CertError("The private key does not match the certificate.");
@@ -50498,7 +50687,7 @@ async function loadCerts(config2) {
50498
50687
  loaded.cert = cert;
50499
50688
  loaded.key = key;
50500
50689
  try {
50501
- loaded.notAfter = new Date(new crypto2.X509Certificate(cert).validTo);
50690
+ loaded.notAfter = new Date(new crypto.X509Certificate(cert).validTo);
50502
50691
  } catch {
50503
50692
  }
50504
50693
  return loaded;
@@ -51598,8 +51787,8 @@ var SUPERSEDED_MARKER = "[Superseded: this file was read again later in the conv
51598
51787
  function readFilePath(argumentsJson) {
51599
51788
  try {
51600
51789
  const parsed = JSON.parse(argumentsJson.length > 0 ? argumentsJson : "{}");
51601
- const path24 = parsed.path;
51602
- return typeof path24 === "string" && path24.length > 0 ? path24 : void 0;
51790
+ const path26 = parsed.path;
51791
+ return typeof path26 === "string" && path26.length > 0 ? path26 : void 0;
51603
51792
  } catch {
51604
51793
  return void 0;
51605
51794
  }
@@ -51612,8 +51801,8 @@ function dropSupersededReads(messages) {
51612
51801
  for (const toolCall of message.toolCalls ?? []) {
51613
51802
  if (toolCall.name !== "read_file")
51614
51803
  continue;
51615
- const path24 = readFilePath(toolCall.arguments);
51616
- if (path24 === void 0)
51804
+ const path26 = readFilePath(toolCall.arguments);
51805
+ if (path26 === void 0)
51617
51806
  continue;
51618
51807
  keyByCallId.set(toolCall.id, toolCall.arguments);
51619
51808
  }
@@ -52003,8 +52192,20 @@ function buildSystemPrompt(workspaceRoot, options = {}) {
52003
52192
  if (options.skills !== void 0 && options.skills.length > 0) {
52004
52193
  lines.push("", options.skills);
52005
52194
  }
52195
+ if (options.pythonToolsDisabled === true) {
52196
+ lines.push(
52197
+ "",
52198
+ "Python tools:",
52199
+ "- You cannot create runnable tools right now \u2014 the feature is switched off in Settings",
52200
+ " \u2192 Python.",
52201
+ // One line, unwrapped: it is the instruction that matters and a test asserts it verbatim.
52202
+ "- Do not write a script and call it a tool.",
52203
+ '- If the user asks for a "tool", say it is switched off and let them choose: enable it in',
52204
+ " Settings \u2192 Python, or have you write an ordinary script instead."
52205
+ );
52206
+ }
52006
52207
  if (options.canWriteSkills === true) {
52007
- lines.push("", "Recording what you learn:", "- When the user explains something durable about their environment \u2014 an internal", " library and how to use it, a house convention, the shape of an in-house API, a", " gotcha specific to this codebase \u2014 offer to record it with write_skill. Ask first;", " do not write one unprompted.", '- "Durable" means it would be true again next week and useful to a future', " conversation. A one-off instruction for the current task is not a skill.", "- Before writing a new skill, check the list above: if one already covers the", " subject, read it and update that instead of creating a near-duplicate.", "- When you learn something *corrects* an existing skill, say so and offer to update", " it. A stale skill is worse than a missing one, because it is trusted.", "- Write for a reader who has none of this conversation: name the package, the import", ' path, the function, and show a short example. Avoid "as discussed" and "the usual".', "- The description line is the only part always in context, so make it say what", " subject the skill covers \u2014 it is a trigger for reading, not a summary.");
52208
+ lines.push("", "Recording what you learn:", "- When the user explains something durable about their environment \u2014 an internal", " library and how to use it, a house convention, the shape of an in-house API, a", " gotcha specific to this codebase \u2014 offer to record it with write_skill. Ask first;", " do not write one unprompted.", '- "Durable" means it would be true again next week and useful to a future', " conversation. A one-off instruction for the current task is not a skill.", options.skillsSearchable === true ? "- Before writing a new skill, search for one with search_docs: if a note already covers the subject, read it and update that instead of creating a near-duplicate." : "- Before writing a new skill, check the list above: if one already covers the subject, read it and update that instead of creating a near-duplicate.", "- When you learn something *corrects* an existing skill, say so and offer to update", " it. A stale skill is worse than a missing one, because it is trusted.", "- Write for a reader who has none of this conversation: name the package, the import", ' path, the function, and show a short example. Avoid "as discussed" and "the usual".', options.skillsSearchable === true ? "- The description line is what search matches on, so make it say what subject the skill covers in the words someone would search for \u2014 it is a trigger, not a summary." : "- The description line is the only part always in context, so make it say what subject the skill covers \u2014 it is a trigger for reading, not a summary.");
52008
52209
  }
52009
52210
  if (options.expertAvailable === true) {
52010
52211
  lines.push("", "Expert consultation:", "- A stronger model, Claude, is available through the ask_expert tool. You CAN talk to", " it. Never tell the user you have no way to reach another model \u2014 you do.", '- **If the user asks you to consult it, do so.** "Ask Claude", "check with the', ' expert", "what does Claude think" and anything similar are direct instructions.', " It is their money and their decision; do not talk them out of it or decide the", " question is too simple to be worth asking.", "- Otherwise, judge it yourself. It costs real money per call, so on your own", " initiative use it for: planning a change spanning several files, diagnosing a bug", " you have already failed to fix once, choosing between designs with long-lived", " consequences, or reviewing something subtle before committing to it.", "- On your own initiative, do not use it for anything you could answer by reading a", " file, for routine edits, or for restating something already established here.", "- If you decide against consulting it, say that you chose not to and why. Do not say", " you are unable to.", "- The expert can read and search this workspace but cannot edit or run anything. It", " cannot see this conversation, so put the context it needs in your question.", "- You remain responsible for the work. Treat its answer as advice from a colleague:", " verify it against the actual code, and say so if you disagree.");
@@ -52499,7 +52700,7 @@ function buildExpertBriefing(input) {
52499
52700
  }
52500
52701
 
52501
52702
  // ../../packages/core/dist/expert/budget.js
52502
- function money(value) {
52703
+ function money2(value) {
52503
52704
  return `$${value.toFixed(value < 1 ? 4 : 2)}`;
52504
52705
  }
52505
52706
  function checkExpertBudget(spend, limits) {
@@ -52514,7 +52715,7 @@ function checkExpertBudget(spend, limits) {
52514
52715
  if (maxSpend > 0 && spend.usd >= maxSpend) {
52515
52716
  return {
52516
52717
  allowed: false,
52517
- message: `The expert spending limit for this task has been reached (${money(spend.usd)} of ${money(maxSpend)}). Continue on your own: use what the expert has already told you, read the code directly, and say plainly if you are stuck rather than guessing. The user can raise the limit in Settings \u2192 Expert, or start a new task to reset it.` + (spend.unpriced > 0 ? ` Note ${String(spend.unpriced)} consultation${spend.unpriced === 1 ? "" : "s"} reported no cost, so the real total is higher than the figure above.` : "")
52718
+ message: `The expert spending limit for this task has been reached (${money2(spend.usd)} of ${money2(maxSpend)}). Continue on your own: use what the expert has already told you, read the code directly, and say plainly if you are stuck rather than guessing. The user can raise the limit in Settings \u2192 Expert, or start a new task to reset it.` + (spend.unpriced > 0 ? ` Note ${String(spend.unpriced)} consultation${spend.unpriced === 1 ? "" : "s"} reported no cost, so the real total is higher than the figure above.` : "")
52518
52719
  };
52519
52720
  }
52520
52721
  return { allowed: true };
@@ -52531,7 +52732,7 @@ function expertBudgetUsage(spend, limits) {
52531
52732
  return void 0;
52532
52733
  return Math.min(1, Math.max(...fractions));
52533
52734
  }
52534
- function describeExpertBudget(spend, limits) {
52735
+ function describeExpertBudget(spend, limits, pricing) {
52535
52736
  const parts = [];
52536
52737
  const maxConsultations = limits.maxConsultations ?? 0;
52537
52738
  if (maxConsultations > 0) {
@@ -52540,11 +52741,14 @@ function describeExpertBudget(spend, limits) {
52540
52741
  }
52541
52742
  const maxSpend = limits.maxSpendUsd ?? 0;
52542
52743
  if (maxSpend > 0) {
52543
- parts.push(`${money(Math.max(0, maxSpend - spend.usd))} of ${money(maxSpend)} left`);
52744
+ parts.push(`${money2(Math.max(0, maxSpend - spend.usd))} of ${money2(maxSpend)} left`);
52544
52745
  }
52545
52746
  if (parts.length === 0)
52546
- return void 0;
52547
- return `Budget for this task: ${parts.join(", ")}. Plan the number of checkpoints to fit \u2014 when it runs out the junior finishes alone.`;
52747
+ return pricing;
52748
+ return [
52749
+ `Budget for this task: ${parts.join(", ")}. Plan the number of checkpoints to fit \u2014 when it runs out the junior finishes alone.`,
52750
+ pricing
52751
+ ].filter((line) => line !== void 0).join(" ");
52548
52752
  }
52549
52753
 
52550
52754
  // ../../packages/core/dist/rag/vectorStore.js
@@ -52597,13 +52801,13 @@ var OpenSearchClient = class {
52597
52801
  * `_bulk`, `_delete_by_query`, index creation — is refused here rather than merely
52598
52802
  * unused, so no future edit or crafted argument can turn a read client into a writer.
52599
52803
  */
52600
- async request(path24, options = {}) {
52804
+ async request(path26, options = {}) {
52601
52805
  const method = options.method ?? "GET";
52602
- const isSearchPost = method === "POST" && /\/_search(\?|$)/.test(path24);
52806
+ const isSearchPost = method === "POST" && /\/_search(\?|$)/.test(path26);
52603
52807
  if (method !== "GET" && !isSearchPost) {
52604
- throw new OpenSearchError(`Refusing ${method} ${path24}: this client is read-only. Indexing goes through the indexer, which the user starts from Settings.`);
52808
+ throw new OpenSearchError(`Refusing ${method} ${path26}: this client is read-only. Indexing goes through the indexer, which the user starts from Settings.`);
52605
52809
  }
52606
- const url2 = `${this.base}${path24}`;
52810
+ const url2 = `${this.base}${path26}`;
52607
52811
  const request = {
52608
52812
  method,
52609
52813
  headers: this.headers()
@@ -52757,11 +52961,11 @@ function collectFields(properties, prefix, out) {
52757
52961
  return;
52758
52962
  for (const [name, raw] of Object.entries(properties)) {
52759
52963
  const field = raw;
52760
- const path24 = prefix.length > 0 ? `${prefix}.${name}` : name;
52964
+ const path26 = prefix.length > 0 ? `${prefix}.${name}` : name;
52761
52965
  if (typeof field.type === "string")
52762
- out[path24] = field.type;
52966
+ out[path26] = field.type;
52763
52967
  if (field.properties !== void 0)
52764
- collectFields(field.properties, path24, out);
52968
+ collectFields(field.properties, path26, out);
52765
52969
  }
52766
52970
  }
52767
52971
  function describeStatus(status, url2, body) {
@@ -52780,23 +52984,23 @@ function describeStatus(status, url2, body) {
52780
52984
  var TEXT_TYPES = /* @__PURE__ */ new Set(["text", "match_only_text", "search_as_you_type", "wildcard"]);
52781
52985
  var KEYWORD_TYPES = /* @__PURE__ */ new Set(["keyword", "constant_keyword"]);
52782
52986
  var NOISE_FIELDS = /* @__PURE__ */ new Set(["@version", "ecs", "tags", "stream", "input", "agent", "host", "event"]);
52783
- function leafName(path24) {
52784
- const parts = path24.split(".");
52785
- return parts[parts.length - 1] ?? path24;
52987
+ function leafName(path26) {
52988
+ const parts = path26.split(".");
52989
+ return parts[parts.length - 1] ?? path26;
52786
52990
  }
52787
52991
  function selectQueryFields(mapping, limit = 25) {
52788
52992
  const text = [];
52789
52993
  const keyword = [];
52790
- for (const [path24, type] of Object.entries(mapping)) {
52791
- if (NOISE_FIELDS.has(leafName(path24)) || NOISE_FIELDS.has(path24.split(".")[0] ?? ""))
52994
+ for (const [path26, type] of Object.entries(mapping)) {
52995
+ if (NOISE_FIELDS.has(leafName(path26)) || NOISE_FIELDS.has(path26.split(".")[0] ?? ""))
52792
52996
  continue;
52793
52997
  if (TEXT_TYPES.has(type)) {
52794
- text.push(path24);
52998
+ text.push(path26);
52795
52999
  } else if (KEYWORD_TYPES.has(type)) {
52796
- const parent = path24.replace(/\.keyword$/, "");
52797
- if (path24.endsWith(".keyword") && TEXT_TYPES.has(mapping[parent] ?? ""))
53000
+ const parent = path26.replace(/\.keyword$/, "");
53001
+ if (path26.endsWith(".keyword") && TEXT_TYPES.has(mapping[parent] ?? ""))
52798
53002
  continue;
52799
- keyword.push(path24);
53003
+ keyword.push(path26);
52800
53004
  }
52801
53005
  }
52802
53006
  const byDepth = (a, b) => a.split(".").length - b.split(".").length || a.localeCompare(b);
@@ -52938,8 +53142,8 @@ var OpenSearchIndexWriter = class {
52938
53142
  }
52939
53143
  return headers;
52940
53144
  }
52941
- async request(path24, method, body, signal) {
52942
- const url2 = `${this.connection.url.replace(/\/+$/, "")}${path24}`;
53145
+ async request(path26, method, body, signal) {
53146
+ const url2 = `${this.connection.url.replace(/\/+$/, "")}${path26}`;
52943
53147
  const request = { method, headers: this.headers() };
52944
53148
  if (body !== void 0) {
52945
53149
  if (typeof body === "string") {
@@ -53094,13 +53298,13 @@ var OpenSearchIndexWriter = class {
53094
53298
  for (const hit of hits) {
53095
53299
  const source = hit._source ?? {};
53096
53300
  const vector = source.vector;
53097
- const path24 = source.path;
53098
- if (typeof path24 !== "string" || !Array.isArray(vector))
53301
+ const path26 = source.path;
53302
+ if (typeof path26 !== "string" || !Array.isArray(vector))
53099
53303
  continue;
53100
53304
  documents.push({
53101
53305
  id: hit._id ?? "",
53102
53306
  text: typeof source.text === "string" ? source.text : "",
53103
- path: path24,
53307
+ path: path26,
53104
53308
  startLine: typeof source.startLine === "number" ? source.startLine : 1,
53105
53309
  endLine: typeof source.endLine === "number" ? source.endLine : 1,
53106
53310
  vector
@@ -53174,8 +53378,8 @@ var RestTransport = class {
53174
53378
  * threw would push every caller into catching and re-inspecting an error to find out
53175
53379
  * whether it was really an error. `expectOk` is there for the cases that are.
53176
53380
  */
53177
- async send(path24, method, body, signal) {
53178
- const url2 = `${this.connection.url.replace(/\/+$/, "")}${path24}`;
53381
+ async send(path26, method, body, signal) {
53382
+ const url2 = `${this.connection.url.replace(/\/+$/, "")}${path26}`;
53179
53383
  const request = { method, headers: this.headers() };
53180
53384
  if (body !== void 0)
53181
53385
  request.body = JSON.stringify(body);
@@ -53201,11 +53405,11 @@ var RestTransport = class {
53201
53405
  return { status: response.status, body: parsed };
53202
53406
  }
53203
53407
  /** Sends, and throws unless the status is 2xx. */
53204
- async expectOk(path24, method, body, signal) {
53205
- const result = await this.send(path24, method, body, signal);
53408
+ async expectOk(path26, method, body, signal) {
53409
+ const result = await this.send(path26, method, body, signal);
53206
53410
  if (result.status < 200 || result.status >= 300) {
53207
53411
  const detail = typeof result.body === "string" ? result.body : JSON.stringify(result.body ?? "");
53208
- throw new VectorStoreError(`${method} ${path24} on ${this.label} returned HTTP ${String(result.status)}. ${detail.slice(0, 300)}`, result.status);
53412
+ throw new VectorStoreError(`${method} ${path26} on ${this.label} returned HTTP ${String(result.status)}. ${detail.slice(0, 300)}`, result.status);
53209
53413
  }
53210
53414
  return result.body;
53211
53415
  }
@@ -53299,10 +53503,10 @@ var ChromaSearcher = class extends ChromaBase {
53299
53503
  const matches = [];
53300
53504
  for (let index = 0; index < ids.length; index++) {
53301
53505
  const metadata = metadatas[index] ?? {};
53302
- const path24 = typeof metadata.path === "string" ? metadata.path : void 0;
53303
- if (path24 === void 0)
53506
+ const path26 = typeof metadata.path === "string" ? metadata.path : void 0;
53507
+ if (path26 === void 0)
53304
53508
  continue;
53305
- if (filtering && !path24.startsWith(prefix))
53509
+ if (filtering && !path26.startsWith(prefix))
53306
53510
  continue;
53307
53511
  const distance = distances[index];
53308
53512
  const match = {
@@ -53314,7 +53518,7 @@ var ChromaSearcher = class extends ChromaBase {
53314
53518
  */
53315
53519
  score: typeof distance === "number" ? 1 / (1 + Math.max(0, distance)) : 0,
53316
53520
  text: documents[index] ?? (typeof metadata.text === "string" ? metadata.text : ""),
53317
- path: path24
53521
+ path: path26
53318
53522
  };
53319
53523
  if (typeof metadata.startLine === "number")
53320
53524
  match.startLine = metadata.startLine;
@@ -53419,13 +53623,13 @@ var ChromaIndexWriter = class extends ChromaBase {
53419
53623
  for (let index = 0; index < ids.length; index++) {
53420
53624
  const metadata = result.metadatas?.[index] ?? {};
53421
53625
  const vector = result.embeddings?.[index];
53422
- const path24 = typeof metadata.path === "string" ? metadata.path : void 0;
53423
- if (path24 === void 0 || !Array.isArray(vector))
53626
+ const path26 = typeof metadata.path === "string" ? metadata.path : void 0;
53627
+ if (path26 === void 0 || !Array.isArray(vector))
53424
53628
  continue;
53425
53629
  documents.push({
53426
53630
  id: ids[index] ?? "",
53427
53631
  text: result.documents?.[index] ?? "",
53428
- path: path24,
53632
+ path: path26,
53429
53633
  startLine: typeof metadata.startLine === "number" ? metadata.startLine : 1,
53430
53634
  endLine: typeof metadata.endLine === "number" ? metadata.endLine : 1,
53431
53635
  vector
@@ -53442,9 +53646,9 @@ var ChromaIndexWriter = class extends ChromaBase {
53442
53646
  const result = await this.rest.expectOk(`${this.base}/collections/${found.id}/get`, "POST", { include: ["metadatas"], limit }, options.signal);
53443
53647
  const paths = /* @__PURE__ */ new Set();
53444
53648
  for (const metadata of result.metadatas ?? []) {
53445
- const path24 = metadata?.path;
53446
- if (typeof path24 === "string")
53447
- paths.add(path24);
53649
+ const path26 = metadata?.path;
53650
+ if (typeof path26 === "string")
53651
+ paths.add(path26);
53448
53652
  }
53449
53653
  return [...paths];
53450
53654
  }
@@ -53471,14 +53675,14 @@ var MARKER_ID = "5f6d2a41-0000-5000-8000-6c69676874c0";
53471
53675
  var MARKER_MARK = "light-code";
53472
53676
  function toMatch(point) {
53473
53677
  const payload = point.payload ?? {};
53474
- const path24 = typeof payload.path === "string" ? payload.path : void 0;
53475
- if (path24 === void 0)
53678
+ const path26 = typeof payload.path === "string" ? payload.path : void 0;
53679
+ if (path26 === void 0)
53476
53680
  return void 0;
53477
53681
  const match = {
53478
53682
  id: typeof payload.chunkId === "string" ? payload.chunkId : point.id,
53479
53683
  score: typeof point.score === "number" ? point.score : 0,
53480
53684
  text: typeof payload.text === "string" ? payload.text : "",
53481
- path: path24
53685
+ path: path26
53482
53686
  };
53483
53687
  if (typeof payload.startLine === "number")
53484
53688
  match.startLine = payload.startLine;
@@ -53641,13 +53845,13 @@ var QdrantIndexWriter = class extends QdrantBase {
53641
53845
  const documents = [];
53642
53846
  for (const point of result.body.result?.points ?? []) {
53643
53847
  const payload = point.payload ?? {};
53644
- const path24 = typeof payload.path === "string" ? payload.path : void 0;
53645
- if (path24 === void 0 || !Array.isArray(point.vector))
53848
+ const path26 = typeof payload.path === "string" ? payload.path : void 0;
53849
+ if (path26 === void 0 || !Array.isArray(point.vector))
53646
53850
  continue;
53647
53851
  documents.push({
53648
53852
  id: typeof payload.chunkId === "string" ? payload.chunkId : point.id,
53649
53853
  text: typeof payload.text === "string" ? payload.text : "",
53650
- path: path24,
53854
+ path: path26,
53651
53855
  startLine: typeof payload.startLine === "number" ? payload.startLine : 1,
53652
53856
  endLine: typeof payload.endLine === "number" ? payload.endLine : 1,
53653
53857
  vector: point.vector
@@ -53674,9 +53878,9 @@ var QdrantIndexWriter = class extends QdrantBase {
53674
53878
  throw new VectorStoreError(`Could not list "${collection}" (HTTP ${String(result.status)}).`, result.status);
53675
53879
  }
53676
53880
  for (const point of result.body.result?.points ?? []) {
53677
- const path24 = point.payload?.path;
53678
- if (typeof path24 === "string")
53679
- paths.add(path24);
53881
+ const path26 = point.payload?.path;
53882
+ if (typeof path26 === "string")
53883
+ paths.add(path26);
53680
53884
  }
53681
53885
  offset = result.body.result?.next_page_offset;
53682
53886
  if (offset === void 0 || offset === null)
@@ -54830,14 +55034,14 @@ function formatBytes(size) {
54830
55034
  return `${(size / (1 << 10)).toFixed(1)}KB`;
54831
55035
  return `${String(size)}B`;
54832
55036
  }
54833
- async function readTail(fs20, path24, size, count) {
55037
+ async function readTail(fs22, path26, size, count) {
54834
55038
  let span = Math.min(size, CHUNK);
54835
55039
  let text;
54836
55040
  let start;
54837
55041
  for (; ; ) {
54838
55042
  start = Math.max(0, size - span);
54839
55043
  const decoder = new StringDecoder("utf8");
54840
- text = decoder.write(await fs20.readBytesSlice(path24, start, size)) + decoder.end();
55044
+ text = decoder.write(await fs22.readBytesSlice(path26, start, size)) + decoder.end();
54841
55045
  const enough = text.split("\n").length > count;
54842
55046
  if (enough || start === 0 || span >= size)
54843
55047
  break;
@@ -54855,7 +55059,7 @@ async function readTail(fs20, path24, size, count) {
54855
55059
  hasMoreAfter: false
54856
55060
  };
54857
55061
  }
54858
- async function readLineWindow(fs20, path24, size, from, count) {
55062
+ async function readLineWindow(fs22, path26, size, from, count) {
54859
55063
  const decoder = new StringDecoder("utf8");
54860
55064
  const lines = [];
54861
55065
  let pending = "";
@@ -54870,7 +55074,7 @@ async function readLineWindow(fs20, path24, size, from, count) {
54870
55074
  };
54871
55075
  scan: while (position < size) {
54872
55076
  const end = Math.min(size, position + CHUNK);
54873
- pending += decoder.write(await fs20.readBytesSlice(path24, position, end));
55077
+ pending += decoder.write(await fs22.readBytesSlice(path26, position, end));
54874
55078
  position = end;
54875
55079
  const parts = pending.split(/\r\n|\r|\n/);
54876
55080
  pending = parts.pop() ?? "";
@@ -54893,13 +55097,13 @@ async function readLineWindow(fs20, path24, size, from, count) {
54893
55097
  hasMoreAfter: !reachedEnd || lineNumber > from + lines.length
54894
55098
  };
54895
55099
  }
54896
- async function countLines(fs20, path24, size) {
55100
+ async function countLines(fs22, path26, size) {
54897
55101
  let newlines = 0;
54898
55102
  let position = 0;
54899
55103
  let lastByte = -1;
54900
55104
  while (position < size) {
54901
55105
  const end = Math.min(size, position + CHUNK);
54902
- const buffer = await fs20.readBytesSlice(path24, position, end);
55106
+ const buffer = await fs22.readBytesSlice(path26, position, end);
54903
55107
  for (const byte of buffer)
54904
55108
  if (byte === 10)
54905
55109
  newlines += 1;
@@ -54972,7 +55176,7 @@ function chunkFile(content, options = {}) {
54972
55176
  }
54973
55177
 
54974
55178
  // ../../packages/core/dist/rag/indexer.js
54975
- import crypto3 from "node:crypto";
55179
+ import crypto2 from "node:crypto";
54976
55180
  import fs5 from "node:fs/promises";
54977
55181
  import path8 from "node:path";
54978
55182
  var ALWAYS_SKIP = /* @__PURE__ */ new Set([
@@ -55067,7 +55271,7 @@ var SKIP_FILENAMES = /* @__PURE__ */ new Set([
55067
55271
  ".env"
55068
55272
  ]);
55069
55273
  function hashContent(content) {
55070
- return crypto3.createHash("sha256").update(content).digest("hex").slice(0, 32);
55274
+ return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 32);
55071
55275
  }
55072
55276
  function chunkSignatureFor(options) {
55073
55277
  return JSON.stringify([options?.windowLines ?? null, options?.overlapLines ?? null, options?.maxChars ?? null]);
@@ -58971,12 +59175,12 @@ function createFetchWithInit(baseFetch = fetch, baseInit) {
58971
59175
  }
58972
59176
 
58973
59177
  // ../../node_modules/.pnpm/pkce-challenge@5.0.1/node_modules/pkce-challenge/dist/index.node.js
58974
- var crypto4;
58975
- crypto4 = globalThis.crypto?.webcrypto ?? // Node.js [18-16] REPL
59178
+ var crypto3;
59179
+ crypto3 = globalThis.crypto?.webcrypto ?? // Node.js [18-16] REPL
58976
59180
  globalThis.crypto ?? // Node.js >18
58977
59181
  import("node:crypto").then((m) => m.webcrypto);
58978
59182
  async function getRandomValues(size) {
58979
- return (await crypto4).getRandomValues(new Uint8Array(size));
59183
+ return (await crypto3).getRandomValues(new Uint8Array(size));
58980
59184
  }
58981
59185
  async function random(size) {
58982
59186
  const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
@@ -58996,7 +59200,7 @@ async function generateVerifier(length) {
58996
59200
  return await random(length);
58997
59201
  }
58998
59202
  async function generateChallenge(code_verifier) {
58999
- const buffer = await (await crypto4).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
59203
+ const buffer = await (await crypto3).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
59000
59204
  return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "");
59001
59205
  }
59002
59206
  async function pkceChallenge(length) {
@@ -61120,6 +61324,24 @@ async function loadSkills(dirs) {
61120
61324
  skills.sort((a, b) => a.name.localeCompare(b.name));
61121
61325
  return { skills, issues };
61122
61326
  }
61327
+ function renderSkillsHintForPrompt(count) {
61328
+ if (count === 0)
61329
+ return "";
61330
+ const plural = count === 1 ? "note has" : "notes have";
61331
+ return [
61332
+ "## Skills",
61333
+ "",
61334
+ `${String(count)} ${plural} been recorded for this workspace: house conventions, internal`,
61335
+ "libraries, and gotchas specific to this codebase. They are not listed here.",
61336
+ "",
61337
+ "- Before working on an unfamiliar part of this workspace, or whenever the user mentions",
61338
+ " something internal you do not recognise, call search_docs to look for a relevant note.",
61339
+ '- Search by subject, in your own words \u2014 "how we call internal HTTP services", not a',
61340
+ " guessed file name.",
61341
+ "- A hit gives you the summary and a path. Read the file for the full text before acting",
61342
+ " on the subject."
61343
+ ].join("\n");
61344
+ }
61123
61345
  function renderSkillsForPrompt(skills) {
61124
61346
  if (skills.length === 0)
61125
61347
  return "";
@@ -61178,8 +61400,19 @@ function createWriteSkillTool(context) {
61178
61400
  async execute(params) {
61179
61401
  try {
61180
61402
  const filePath = await resolveSkillPath(context.skillsDir, params.name);
61181
- const existed = (await readIfPresent(filePath)).length > 0;
61182
- await fs9.writeFile(filePath, renderSkill(params.name, params.description, params.body), "utf8");
61403
+ const before = await readIfPresent(filePath);
61404
+ const existed = before.length > 0;
61405
+ const rendered = renderSkill(params.name, params.description, params.body);
61406
+ if (context.submitForReview !== void 0) {
61407
+ return {
61408
+ content: await context.submitForReview({
61409
+ name: params.name,
61410
+ content: rendered,
61411
+ existingContent: before
61412
+ })
61413
+ };
61414
+ }
61415
+ await fs9.writeFile(filePath, rendered, "utf8");
61183
61416
  await context.onChanged();
61184
61417
  return {
61185
61418
  content: `${existed ? "Updated" : "Recorded"} the skill "${params.name}" at ${filePath}.
@@ -61220,12 +61453,12 @@ import fs12 from "node:fs/promises";
61220
61453
  import path15 from "node:path";
61221
61454
 
61222
61455
  // ../../packages/core/dist/python/registry.js
61223
- import crypto5 from "node:crypto";
61456
+ import crypto4 from "node:crypto";
61224
61457
  import fs10 from "node:fs/promises";
61225
61458
  import path13 from "node:path";
61226
61459
  var REGISTRY_FILE = ".registry.json";
61227
61460
  function hashSource(source) {
61228
- return crypto5.createHash("sha256").update(source.replace(/\r\n/g, "\n")).digest("hex");
61461
+ return crypto4.createHash("sha256").update(source.replace(/\r\n/g, "\n")).digest("hex");
61229
61462
  }
61230
61463
  function isValidToolName(name) {
61231
61464
  return /^[a-z][a-z0-9_]{0,63}$/.test(name);
@@ -61381,6 +61614,10 @@ var createParams = external_exports.object({
61381
61614
  name: external_exports.string().describe("Tool name: lowercase letters, digits and underscores. Becomes py__<name> and <name>.py."),
61382
61615
  source: external_exports.string().describe("The complete Python file. Must define `run`. Use type hints \u2014 the parameter schema is derived from them. The module docstring becomes the tool description; document parameters in a Google-style Args: block. Declare dependencies in a PEP 723 inline block if you need any.")
61383
61616
  });
61617
+ var specifyParams = external_exports.object({
61618
+ name: external_exports.string().describe("Tool name: lowercase letters, digits and underscores. Becomes py__<name> and <name>.py."),
61619
+ specification: external_exports.string().describe("What the tool must do, in prose. A model configured for code writes the file from this; you do not write Python here. Say what it takes, what it returns, and any library or endpoint it must use. The user approves the generated source before anything runs.")
61620
+ });
61384
61621
  var deleteParams2 = external_exports.object({
61385
61622
  name: external_exports.string().describe("The tool to remove.")
61386
61623
  });
@@ -61399,11 +61636,34 @@ async function readIfPresent2(filePath) {
61399
61636
  }
61400
61637
  }
61401
61638
  function makeWriteTool(context, options) {
61639
+ const generator = context.generateSource;
61640
+ const pending = /* @__PURE__ */ new Map();
61641
+ const sourceFor = async (params) => {
61642
+ if (generator === void 0 || params.specification === void 0) {
61643
+ return { source: params.source };
61644
+ }
61645
+ const key = `${params.name}::${params.specification}`;
61646
+ let inFlight = pending.get(key);
61647
+ if (inFlight === void 0) {
61648
+ const toolPath = resolveToolPath(context.toolsDir, params.name);
61649
+ inFlight = (async () => {
61650
+ const before = await readIfPresent2(await toolPath);
61651
+ const generated = await generator({
61652
+ toolName: params.name,
61653
+ specification: params.specification ?? "",
61654
+ ...before.length > 0 ? { existingSource: before } : {}
61655
+ });
61656
+ return { source: unwrapFencedSource(generated.source), producedBy: generated.producedBy };
61657
+ })();
61658
+ pending.set(key, inFlight);
61659
+ }
61660
+ return inFlight;
61661
+ };
61402
61662
  return {
61403
61663
  name: options.name,
61404
61664
  group: "edit",
61405
61665
  description: options.description,
61406
- parametersSchema: createParams,
61666
+ parametersSchema: generator !== void 0 ? specifyParams : createParams,
61407
61667
  /**
61408
61668
  * A real diff of the real file: its current content against exactly the bytes that
61409
61669
  * will be written. Not a summary and not the model's account of what it wrote
@@ -61413,7 +61673,14 @@ function makeWriteTool(context, options) {
61413
61673
  async preview(params) {
61414
61674
  const filePath = await resolveToolPath(context.toolsDir, params.name);
61415
61675
  const before = await readIfPresent2(filePath);
61416
- return { kind: "diff", path: filePath, before, after: params.source };
61676
+ const { source, producedBy } = await sourceFor(params);
61677
+ return {
61678
+ kind: "diff",
61679
+ path: filePath,
61680
+ before,
61681
+ after: source,
61682
+ ...producedBy !== void 0 ? { note: `Written by ${producedBy}` } : {}
61683
+ };
61417
61684
  },
61418
61685
  async execute(params) {
61419
61686
  try {
@@ -61428,15 +61695,26 @@ function makeWriteTool(context, options) {
61428
61695
  isError: true
61429
61696
  };
61430
61697
  }
61698
+ const { source, producedBy } = await sourceFor(params);
61699
+ if (context.submitForReview !== void 0) {
61700
+ return {
61701
+ content: await context.submitForReview({
61702
+ name: params.name,
61703
+ content: source,
61704
+ existingContent: before,
61705
+ ...producedBy !== void 0 ? { producedBy } : {}
61706
+ })
61707
+ };
61708
+ }
61431
61709
  await fs11.mkdir(context.toolsDir, { recursive: true });
61432
- await fs11.writeFile(filePath, params.source, "utf8");
61710
+ await fs11.writeFile(filePath, source, "utf8");
61433
61711
  const restore = async () => {
61434
61712
  if (before.length > 0)
61435
61713
  await fs11.writeFile(filePath, before, "utf8");
61436
61714
  else
61437
61715
  await fs11.rm(filePath, { force: true });
61438
61716
  };
61439
- const declared = parseInlineDependencies(params.source);
61717
+ const declared = parseInlineDependencies(source);
61440
61718
  if (declared.length > 0) {
61441
61719
  if (context.installDeps === void 0) {
61442
61720
  await restore();
@@ -61464,7 +61742,7 @@ ${message}
61464
61742
 
61465
61743
  ${traceback ?? ""}`.trim(), isError: true };
61466
61744
  }
61467
- await approveTool(context.toolsDir, params.name, params.source, described);
61745
+ await approveTool(context.toolsDir, params.name, source, described);
61468
61746
  await context.onChanged();
61469
61747
  return {
61470
61748
  content: `Saved and registered as py__${params.name}.
@@ -61628,7 +61906,7 @@ var PythonManager = class {
61628
61906
  return;
61629
61907
  }
61630
61908
  try {
61631
- const env = minimalPythonEnv();
61909
+ const env = minimalPythonEnv(this.options.sessionEnv?.() ?? {});
61632
61910
  let interpreter;
61633
61911
  if (config2.venvPath !== void 0 && config2.venvPath.trim().length > 0) {
61634
61912
  this.venvPath = config2.venvPath.trim();
@@ -61725,8 +62003,10 @@ var PythonManager = class {
61725
62003
  return [];
61726
62004
  const worker = this.worker;
61727
62005
  const uv = this.uv;
62006
+ const generated = this.options.generateSource?.();
61728
62007
  const context = {
61729
62008
  toolsDir: this.toolsDir,
62009
+ ...generated !== void 0 ? { generateSource: generated } : {},
61730
62010
  worker,
61731
62011
  onChanged: () => this.refresh(),
61732
62012
  ...uv !== void 0 ? {
@@ -61737,7 +62017,7 @@ var PythonManager = class {
61737
62017
  ...this.indexUrl !== void 0 ? { indexUrl: this.indexUrl } : {},
61738
62018
  extraIndexUrls: this.extraIndexUrls,
61739
62019
  offline: this.offline,
61740
- env: minimalPythonEnv()
62020
+ env: minimalPythonEnv(this.options.sessionEnv?.() ?? {})
61741
62021
  })
61742
62022
  } : {}
61743
62023
  };
@@ -62143,6 +62423,31 @@ function renderDocsMatches(options, matches) {
62143
62423
  }).filter((rendered) => rendered !== void 0).join("\n\n");
62144
62424
  }
62145
62425
 
62426
+ // ../../packages/core/dist/agent/unfinished.js
62427
+ var MAX_PREAMBLE_LENGTH = 400;
62428
+ var FORWARD_LOOKING = /^(let me\b|let's\b|i'?ll\b|i will\b|i'?m going to\b|going to\b|now i\b|next,? i\b|first,? i\b|starting\b|beginning\b)/i;
62429
+ var HANDING_BACK = /^(let me know\b|let us know\b|i'?ll be happy\b|i'?ll wait\b|i'?ll stand by\b|let me know if\b|i'?ll leave\b)/i;
62430
+ function lastSentence(text) {
62431
+ const trimmed = text.trim();
62432
+ const parts = trimmed.split(/(?<=[.!?])\s+/);
62433
+ return (parts[parts.length - 1] ?? trimmed).trim();
62434
+ }
62435
+ function looksUnfinished(text) {
62436
+ const trimmed = text.trim();
62437
+ if (trimmed.length === 0 || trimmed.length > MAX_PREAMBLE_LENGTH)
62438
+ return false;
62439
+ if (trimmed.endsWith("?"))
62440
+ return false;
62441
+ if (trimmed.endsWith(":"))
62442
+ return true;
62443
+ const last = lastSentence(trimmed);
62444
+ if (HANDING_BACK.test(last))
62445
+ return false;
62446
+ return FORWARD_LOOKING.test(last);
62447
+ }
62448
+ var CONTINUE_PROMPT = "You described what you were about to do but did not call a tool, so nothing happened. If you meant to act, call the tool now. If you were already finished, call attempt_completion with a summary instead.";
62449
+ var MAX_CONTINUE_NUDGES = 1;
62450
+
62146
62451
  // ../../packages/core/dist/agent/truncate.js
62147
62452
  import { randomUUID as randomUUID2 } from "node:crypto";
62148
62453
  import fs13 from "node:fs/promises";
@@ -62383,6 +62688,7 @@ async function runAgentTurn(provider, conversation, userMessage, toolRegistry, t
62383
62688
  conversation.addUserMessage(userMessage, options.images);
62384
62689
  const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
62385
62690
  const mistakeCounts = /* @__PURE__ */ new Map();
62691
+ let continueNudges = 0;
62386
62692
  const mode = options.mode ?? CODE_MODE;
62387
62693
  const tools = toToolDefinitions(toolsForMode(toolRegistry, mode));
62388
62694
  let checkpointTaken = false;
@@ -62416,12 +62722,18 @@ async function runAgentTurn(provider, conversation, userMessage, toolRegistry, t
62416
62722
  return;
62417
62723
  }
62418
62724
  if (toolCall === void 0) {
62419
- if (assistantText.length > 0) {
62420
- conversation.addAssistantMessage(assistantText);
62421
- events.onDone();
62422
- } else {
62725
+ if (assistantText.length === 0) {
62423
62726
  events.onError("The provider finished without returning any text. Check the base URL and model name, and that the endpoint supports streaming chat completions.");
62727
+ return;
62424
62728
  }
62729
+ conversation.addAssistantMessage(assistantText);
62730
+ if (continueNudges < MAX_CONTINUE_NUDGES && looksUnfinished(assistantText)) {
62731
+ continueNudges++;
62732
+ conversation.addUserMessage(CONTINUE_PROMPT);
62733
+ events.onNudgedToContinue?.();
62734
+ continue;
62735
+ }
62736
+ events.onDone();
62425
62737
  return;
62426
62738
  }
62427
62739
  conversation.addAssistantMessage(assistantText, [toolCall]);
@@ -62536,17 +62848,17 @@ var WebviewApprovalGate = class {
62536
62848
  // ../../packages/core/dist/platform/node/filesystem.js
62537
62849
  import fs14 from "node:fs/promises";
62538
62850
  var NodeFileSystem = class {
62539
- async readFile(path24) {
62540
- return fs14.readFile(path24, "utf8");
62851
+ async readFile(path26) {
62852
+ return fs14.readFile(path26, "utf8");
62541
62853
  }
62542
- async readBytes(path24) {
62543
- return fs14.readFile(path24);
62854
+ async readBytes(path26) {
62855
+ return fs14.readFile(path26);
62544
62856
  }
62545
- async readBytesSlice(path24, start, end) {
62857
+ async readBytesSlice(path26, start, end) {
62546
62858
  const length = Math.max(0, end - start);
62547
62859
  if (length === 0)
62548
62860
  return Buffer.alloc(0);
62549
- const handle = await fs14.open(path24, "r");
62861
+ const handle = await fs14.open(path26, "r");
62550
62862
  try {
62551
62863
  const buffer = Buffer.alloc(length);
62552
62864
  const { bytesRead } = await handle.read(buffer, 0, length, start);
@@ -62555,11 +62867,11 @@ var NodeFileSystem = class {
62555
62867
  await handle.close();
62556
62868
  }
62557
62869
  }
62558
- async writeFile(path24, contents) {
62559
- await fs14.writeFile(path24, contents, "utf8");
62870
+ async writeFile(path26, contents) {
62871
+ await fs14.writeFile(path26, contents, "utf8");
62560
62872
  }
62561
- async stat(path24) {
62562
- const stat = await fs14.lstat(path24);
62873
+ async stat(path26) {
62874
+ const stat = await fs14.lstat(path26);
62563
62875
  return {
62564
62876
  size: stat.size,
62565
62877
  mtimeMs: stat.mtimeMs,
@@ -62568,8 +62880,8 @@ var NodeFileSystem = class {
62568
62880
  isSymbolicLink: stat.isSymbolicLink()
62569
62881
  };
62570
62882
  }
62571
- async readdir(path24) {
62572
- const entries = await fs14.readdir(path24, { withFileTypes: true });
62883
+ async readdir(path26) {
62884
+ const entries = await fs14.readdir(path26, { withFileTypes: true });
62573
62885
  return entries.map((entry) => ({
62574
62886
  name: entry.name,
62575
62887
  isFile: entry.isFile(),
@@ -62577,16 +62889,16 @@ var NodeFileSystem = class {
62577
62889
  isSymbolicLink: entry.isSymbolicLink()
62578
62890
  }));
62579
62891
  }
62580
- async exists(path24) {
62892
+ async exists(path26) {
62581
62893
  try {
62582
- await fs14.access(path24);
62894
+ await fs14.access(path26);
62583
62895
  return true;
62584
62896
  } catch {
62585
62897
  return false;
62586
62898
  }
62587
62899
  }
62588
- async mkdir(path24) {
62589
- await fs14.mkdir(path24, { recursive: true });
62900
+ async mkdir(path26) {
62901
+ await fs14.mkdir(path26, { recursive: true });
62590
62902
  }
62591
62903
  };
62592
62904
 
@@ -62864,11 +63176,25 @@ function wireChatBridge(services) {
62864
63176
  workspaceRoot,
62865
63177
  storageDir,
62866
63178
  logger,
63179
+ // Read at worker spawn, so a changed variable applies to the next worker rather than being
63180
+ // frozen at construction. Added to the allowlist in minimalPythonEnv, never a way past it.
63181
+ ...services.sessionEnv !== void 0 ? { sessionEnv: services.sessionEnv } : {},
63182
+ ...services.submitForReview !== void 0 ? {
63183
+ submitForReview: (request) => services.submitForReview?.({ kind: "python-tool", ...request }) ?? Promise.resolve("")
63184
+ } : {},
63185
+ /*
63186
+ * A resolver, not a generator: the tool's *parameters* change shape depending on whether one
63187
+ * is configured — specification versus source — so the answer is needed when the tool list is
63188
+ * built, not when it is called. Refreshed by `loadSettings`, which runs before every turn, so
63189
+ * changing the profile mid-session takes effect on the next message.
63190
+ */
63191
+ generateSource: () => cachedCodeGenerator,
62867
63192
  // A tool created, updated or deleted during a chat changes both the Python tab and the
62868
63193
  // documentation corpus. `postPython` refreshes the tab and schedules the reindex.
62869
63194
  onToolsChanged: () => {
62870
63195
  void postPython();
62871
63196
  void postSchedules();
63197
+ void postTools();
62872
63198
  }
62873
63199
  });
62874
63200
  const defaultSkillsDir = workspaceRoot !== void 0 ? path18.join(workspaceRoot, ".lightcode", "skills") : void 0;
@@ -63070,11 +63396,12 @@ function wireChatBridge(services) {
63070
63396
  }
63071
63397
  const pendingPathApprovals = /* @__PURE__ */ new Map();
63072
63398
  const searchLog = new SearchLog(50, () => post({ type: "searchLog", entries: [...searchLog.list()] }));
63073
- let expertSpend = { usd: 0, consultations: 0, unpriced: 0 };
63399
+ let expertSpend = { usd: 0, consultations: 0, unpriced: 0, keepAlives: 0 };
63074
63400
  let expertSessionId;
63075
63401
  function resetExpertSpend() {
63076
- expertSpend = { usd: 0, consultations: 0, unpriced: 0 };
63402
+ expertSpend = { usd: 0, consultations: 0, unpriced: 0, keepAlives: 0 };
63077
63403
  expertSessionId = void 0;
63404
+ stopKeepAlive();
63078
63405
  taskExpertLimits = void 0;
63079
63406
  taskExpertEstimate = void 0;
63080
63407
  postExpertSpend();
@@ -63100,16 +63427,40 @@ function wireChatBridge(services) {
63100
63427
  else
63101
63428
  expertSpend.unpriced += 1;
63102
63429
  postExpertSpend();
63430
+ if (info.isError)
63431
+ return;
63432
+ const learned = info.costUsd !== void 0;
63433
+ if (cachedReportsCost === learned)
63434
+ return;
63435
+ cachedReportsCost = learned;
63436
+ void configManager.load().then(async ({ config: config2 }) => {
63437
+ await configManager.save("user", { ...config2, expert: { ...config2.expert, reportsCost: learned } });
63438
+ await postExpert();
63439
+ }).catch(() => {
63440
+ });
63103
63441
  }
63104
63442
  let cachedModeId;
63443
+ let cachedCodeGenerator;
63444
+ let cachedReportsCost;
63445
+ let measuringStep;
63446
+ let cachedPricing;
63447
+ let cachedKeepAlive = false;
63448
+ let cachedProgrammingProfileId;
63105
63449
  async function loadSettings() {
63106
63450
  const { config: config2 } = await configManager.load();
63107
63451
  cachedApprovals = config2.approvals?.[approvalsKey] ?? {};
63452
+ cachedCodeGenerator = codeGeneratorFor(config2);
63453
+ cachedProgrammingProfileId = config2.programmingProfileId;
63108
63454
  cachedModeId = config2.modeId;
63109
63455
  cachedMaxIterations = config2.maxIterations ?? 25;
63110
63456
  cachedAccentColor = config2.ui?.accentColor ?? "#22C55E";
63111
63457
  cachedExpertColor = config2.ui?.expertColor ?? "#D97757";
63112
63458
  cachedAssessment = config2.expert?.assessment;
63459
+ cachedReportsCost = config2.expert?.reportsCost;
63460
+ cachedPricing = config2.expert?.pricing;
63461
+ cachedKeepAlive = config2.expert?.keepAlive === true;
63462
+ if (!cachedKeepAlive)
63463
+ stopKeepAlive();
63113
63464
  cachedExpertLimits = {
63114
63465
  ...config2.expert?.maxSpendUsd !== void 0 ? { maxSpendUsd: config2.expert.maxSpendUsd } : {},
63115
63466
  ...config2.expert?.maxConsultations !== void 0 ? { maxConsultations: config2.expert.maxConsultations } : {}
@@ -63130,9 +63481,18 @@ function wireChatBridge(services) {
63130
63481
  maxIterations: cachedMaxIterations,
63131
63482
  accentColor: cachedAccentColor,
63132
63483
  expertColor: cachedExpertColor,
63133
- readRoots: cachedReadRoots
63484
+ readRoots: cachedReadRoots,
63485
+ ...cachedProgrammingProfileId !== void 0 ? { programmingProfileId: cachedProgrammingProfileId } : {},
63486
+ ...hostCapabilities()
63134
63487
  });
63135
63488
  }
63489
+ function hostCapabilities() {
63490
+ return {
63491
+ nativeGuide: ui.openWalkthrough !== void 0,
63492
+ allowProgrammingProfile: services.allowProgrammingProfile === true,
63493
+ ...services.guideMediaBase !== void 0 ? { guideMediaBase: services.guideMediaBase } : {}
63494
+ };
63495
+ }
63136
63496
  const userGate = new WebviewApprovalGate(post);
63137
63497
  const approvalGate = new PolicyApprovalGate(userGate, () => cachedApprovals);
63138
63498
  let mcpJson = '{\n "mcpServers": {}\n}';
@@ -63145,6 +63505,7 @@ function wireChatBridge(services) {
63145
63505
  onStateChanged: () => {
63146
63506
  postMcp();
63147
63507
  void postSchedules();
63508
+ void postTools();
63148
63509
  scheduleDocsReindex("MCP tools changed");
63149
63510
  }
63150
63511
  }, logger, () => cachedApprovals.allowedTools ?? []);
@@ -63162,7 +63523,7 @@ function wireChatBridge(services) {
63162
63523
  platform: process.platform === "win32" ? "win32" : "posix"
63163
63524
  });
63164
63525
  }
63165
- function currentToolRegistry(expert, search, codebase, docs, dispatcher = false) {
63526
+ function currentToolRegistry(expert, search, codebase, docs, dispatcher = false, hideSkills = false) {
63166
63527
  const combined = new ToolRegistry();
63167
63528
  for (const tool of builtinTools.list())
63168
63529
  combined.register(tool);
@@ -63192,7 +63553,13 @@ function wireChatBridge(services) {
63192
63553
  }
63193
63554
  }));
63194
63555
  if (skillsDir !== void 0) {
63195
- const context = { skillsDir, onChanged: refreshSkills };
63556
+ const context = {
63557
+ skillsDir,
63558
+ onChanged: refreshSkills,
63559
+ ...services.submitForReview !== void 0 ? {
63560
+ submitForReview: (request) => services.submitForReview?.({ kind: "skill", ...request }) ?? Promise.resolve("")
63561
+ } : {}
63562
+ };
63196
63563
  combined.register(createWriteSkillTool(context));
63197
63564
  combined.register(createDeleteSkillTool(context));
63198
63565
  }
@@ -63208,8 +63575,12 @@ function wireChatBridge(services) {
63208
63575
  if (codebase !== void 0) {
63209
63576
  combined.register(createSearchCodebaseTool({ ...codebase, observer: searchLog }));
63210
63577
  }
63211
- if (dispatcher) {
63578
+ const hasHiddenTools = dispatcher && combined.dispatchOnlyList().length > 0;
63579
+ const hasHiddenSkills = hideSkills && skills.length > 0;
63580
+ if (hasHiddenTools) {
63212
63581
  combined.register(createCallToolTool());
63582
+ }
63583
+ if (dispatcher && (hasHiddenTools || hasHiddenSkills)) {
63213
63584
  combined.register(createForgetDocsTool());
63214
63585
  combined.register(createSearchDocsTool({
63215
63586
  // Resolved per call, so a tool registered later in this same function is still
@@ -63228,7 +63599,11 @@ function wireChatBridge(services) {
63228
63599
  // Read at call time, not captured: the user can raise the limit mid-task and the very
63229
63600
  // next consultation should honour it, without starting a new task to pick it up.
63230
63601
  budget: () => checkExpertBudget(expertSpend, effectiveExpertLimits()),
63231
- budgetSummary: () => describeExpertBudget(expertSpend, effectiveExpertLimits()),
63602
+ /*
63603
+ * The measured cost goes with the budget, so the expert plans in this deployment's
63604
+ * units rather than from what it believes consultations cost in general.
63605
+ */
63606
+ budgetSummary: () => describeExpertBudget(expertSpend, effectiveExpertLimits(), pricingForPrompt(cachedPricing)),
63232
63607
  onEstimate: (estimate) => {
63233
63608
  taskExpertEstimate = estimate;
63234
63609
  postExpertSpend();
@@ -63237,6 +63612,8 @@ function wireChatBridge(services) {
63237
63612
  get: () => expertSessionId,
63238
63613
  set: (sessionId) => {
63239
63614
  expertSessionId = sessionId;
63615
+ if (sessionId !== void 0 && cachedKeepAlive)
63616
+ ensureKeepAlive();
63240
63617
  }
63241
63618
  },
63242
63619
  /*
@@ -63419,12 +63796,26 @@ function wireChatBridge(services) {
63419
63796
  await refreshSkills();
63420
63797
  const activeMode = findMode(config2.modeId);
63421
63798
  const scheduledGuidance = schedule === void 0 ? void 0 : scheduledRunGuidance(schedule, filterToolsForSchedule(currentToolRegistry(void 0, void 0, void 0, void 0, false).list(), schedule).map((tool) => tool.name).filter((name) => name !== "attempt_completion"));
63799
+ const skillsSearchable = skillRetrievalEnabled(config2.retrieval) && schedule === void 0;
63800
+ const turnSkills = schedule === void 0 ? skills : skillsForSchedule(skills, schedule.allowedSkills);
63422
63801
  const desiredPrompt = buildSystemPrompt(workspaceRoot, {
63423
63802
  model: profile.model,
63424
63803
  providerLabel: profile.label,
63425
63804
  expertAvailable: expertCliInfo !== void 0,
63426
- skills: renderSkillsForPrompt(skills),
63805
+ /*
63806
+ * Either the whole list or a count and an instruction to search — never both, and
63807
+ * never neither. `renderSkillsHintForPrompt` explains why the count stays.
63808
+ */
63809
+ skills: skillsSearchable ? renderSkillsHintForPrompt(skills.length) : renderSkillsForPrompt(turnSkills),
63810
+ skillsSearchable,
63427
63811
  canWriteSkills: skillsDir !== void 0,
63812
+ /*
63813
+ * Read from config rather than from the registry, because the prompt is built before the
63814
+ * registry is. Only the *off* case is claimed: "on but uv is missing" leaves the model
63815
+ * equally toolless, but the Python tab reports that with the actual reason, and telling
63816
+ * the user to switch on something already switched on would be worse than saying nothing.
63817
+ */
63818
+ pythonToolsDisabled: config2.python?.dynamicTools !== "on",
63428
63819
  /*
63429
63820
  * Junior mode's instructions are worse than useless without the expert to delegate
63430
63821
  * to: the model would be told to consult something it has no tool for. The picker
@@ -63453,6 +63844,9 @@ function wireChatBridge(services) {
63453
63844
  denylist,
63454
63845
  readFiles,
63455
63846
  readRoots: cachedReadRoots,
63847
+ // Resolved per turn by the host, so an edit applies to the next command rather than
63848
+ // needing a new session. Absent in the extension, where there is nothing to resolve.
63849
+ ...services.sessionEnv !== void 0 ? { sessionEnv: services.sessionEnv() } : {},
63456
63850
  /*
63457
63851
  * Omitted for a scheduled run: there is nobody to answer, and a run that could grant
63458
63852
  * itself new filesystem access would defeat the point of its allowlist.
@@ -63515,7 +63909,8 @@ function wireChatBridge(services) {
63515
63909
  * behind a search that could never find them.
63516
63910
  */
63517
63911
  search !== void 0 && embedder !== void 0 && docsIndex !== void 0 ? { searcher: search.searcher, embedder, index: docsIndex } : void 0,
63518
- config2.retrieval?.dispatcher === true
63912
+ dispatcherEnabled(config2.retrieval),
63913
+ skillsSearchable
63519
63914
  );
63520
63915
  const turnRegistry = schedule !== void 0 ? registryForSchedule(fullRegistry.list(), schedule) : fullRegistry;
63521
63916
  if (schedule !== void 0) {
@@ -63526,6 +63921,9 @@ function wireChatBridge(services) {
63526
63921
  post({ type: "contextUsage", usage: { ...breakdown, supersededCount, compactedCount } });
63527
63922
  },
63528
63923
  onCompacted: (summarisedCount) => post({ type: "compacted", summarisedCount }),
63924
+ onNudgedToContinue: () => {
63925
+ logger.warn("the model described an action without calling a tool; asked it to continue");
63926
+ },
63529
63927
  onQueuedMessageConsumed: (text2) => {
63530
63928
  post({ type: "queuedMessageConsumed", text: text2 });
63531
63929
  cumulativeText = "";
@@ -63783,18 +64181,29 @@ function wireChatBridge(services) {
63783
64181
  logger.warn(`could not check the expert CLI: ${reason}`);
63784
64182
  const settings = await configManager.load().then((loaded) => loaded.config.expert, () => void 0);
63785
64183
  post({
63786
- type: "expert",
63787
- enabled: settings?.enabled === true,
64184
+ ...expertMessageFrom(settings),
63788
64185
  available: false,
63789
64186
  path: settings?.path ?? expertCliPath ?? "claude",
63790
- reason: `Could not check whether the Claude CLI is available: ${reason}`,
63791
- ...settings?.model !== void 0 ? { model: settings.model } : {},
63792
- maxSpendUsd: settings?.maxSpendUsd ?? 0,
63793
- maxConsultations: settings?.maxConsultations ?? 0,
63794
- ...settings?.assessment !== void 0 ? { assessment: settings.assessment } : {}
64187
+ reason: `Could not check whether the Claude CLI is available: ${reason}`
63795
64188
  });
63796
64189
  }
63797
64190
  }
64191
+ function expertMessageFrom(settings) {
64192
+ return {
64193
+ type: "expert",
64194
+ enabled: settings?.enabled === true,
64195
+ available: false,
64196
+ path: settings?.path ?? expertCliPath ?? "claude",
64197
+ maxSpendUsd: settings?.maxSpendUsd ?? 0,
64198
+ maxConsultations: settings?.maxConsultations ?? 0,
64199
+ keepAlive: settings?.keepAlive === true,
64200
+ ...settings?.model !== void 0 ? { model: settings.model } : {},
64201
+ ...settings?.assessment !== void 0 ? { assessment: settings.assessment } : {},
64202
+ ...settings?.reportsCost !== void 0 ? { reportsCost: settings.reportsCost } : {},
64203
+ ...settings?.pricing !== void 0 ? { pricing: settings.pricing } : {},
64204
+ ...measuringStep !== void 0 ? { measuringStep } : {}
64205
+ };
64206
+ }
63798
64207
  async function postExpertInner(redetect) {
63799
64208
  const { config: config2 } = await configManager.load();
63800
64209
  const configured = config2.expert?.path ?? "claude";
@@ -63802,19 +64211,125 @@ function wireChatBridge(services) {
63802
64211
  expertCli = detected;
63803
64212
  expertCliPath = configured;
63804
64213
  post({
63805
- type: "expert",
63806
- enabled: config2.expert?.enabled === true,
64214
+ // Everything from settings comes from one place, so the two paths cannot drift again.
64215
+ ...expertMessageFrom(config2.expert),
63807
64216
  available: detected.available,
63808
64217
  path: configured,
63809
64218
  ...detected.version !== void 0 ? { version: detected.version } : {},
63810
64219
  ...detected.reason !== void 0 ? { reason: detected.reason } : {},
63811
- ...config2.expert?.model !== void 0 ? { model: config2.expert.model } : {},
63812
- maxSpendUsd: config2.expert?.maxSpendUsd ?? 0,
63813
- maxConsultations: config2.expert?.maxConsultations ?? 0,
63814
- ...config2.expert?.assessment !== void 0 ? { assessment: config2.expert.assessment } : {},
63815
64220
  ...assessmentStep === void 0 ? {} : { assessing: true, assessmentStep }
63816
64221
  });
63817
64222
  }
64223
+ const KEEP_ALIVE_MS = 50 * 60 * 1e3;
64224
+ let keepAliveTimer;
64225
+ function stopKeepAlive() {
64226
+ if (keepAliveTimer === void 0)
64227
+ return;
64228
+ clearInterval(keepAliveTimer);
64229
+ keepAliveTimer = void 0;
64230
+ }
64231
+ function ensureKeepAlive() {
64232
+ if (keepAliveTimer !== void 0)
64233
+ return;
64234
+ keepAliveTimer = setInterval(() => {
64235
+ void runKeepAlive();
64236
+ }, KEEP_ALIVE_MS);
64237
+ keepAliveTimer.unref?.();
64238
+ }
64239
+ async function runKeepAlive() {
64240
+ const session = expertSessionId;
64241
+ if (session === void 0) {
64242
+ stopKeepAlive();
64243
+ return;
64244
+ }
64245
+ try {
64246
+ const { config: config2 } = await configManager.load();
64247
+ if (config2.expert?.keepAlive !== true) {
64248
+ stopKeepAlive();
64249
+ return;
64250
+ }
64251
+ const verdict = checkExpertBudget(expertSpend, effectiveExpertLimits());
64252
+ if (!verdict.allowed) {
64253
+ logger.info("expert keep-alive stopped: the budget for this task is spent");
64254
+ stopKeepAlive();
64255
+ return;
64256
+ }
64257
+ const cli = await resolveExpert(config2);
64258
+ if (cli === void 0) {
64259
+ stopKeepAlive();
64260
+ return;
64261
+ }
64262
+ const answer = await consultExpert(cli, {
64263
+ question: PRICING_PROBE,
64264
+ cwd: workspaceRoot ?? process.cwd(),
64265
+ ...config2.expert?.model !== void 0 ? { model: config2.expert.model } : {},
64266
+ resumeSessionId: session
64267
+ }, logger);
64268
+ expertSpend.keepAlives += 1;
64269
+ if (answer.costUsd !== void 0)
64270
+ expertSpend.usd += answer.costUsd;
64271
+ if (answer.sessionId !== void 0)
64272
+ expertSessionId = answer.sessionId;
64273
+ postExpertSpend();
64274
+ logger.info("expert keep-alive refreshed the session cache");
64275
+ } catch (error51) {
64276
+ logger.warn(`expert keep-alive failed: ${String(error51)}`);
64277
+ }
64278
+ }
64279
+ async function handleMeasureExpertCost() {
64280
+ if (measuringStep !== void 0) {
64281
+ post({ type: "error", message: `Already measuring \u2014 ${measuringStep}` });
64282
+ return;
64283
+ }
64284
+ measuringStep = "Starting\u2026";
64285
+ logger.info("measuring what an expert consultation costs");
64286
+ await postExpert({ redetect: false });
64287
+ try {
64288
+ const { config: config2 } = await configManager.load();
64289
+ const cli = await resolveExpert(config2);
64290
+ if (cli === void 0) {
64291
+ post({
64292
+ type: "error",
64293
+ message: "The Claude CLI could not be found, so there is nothing to measure. Check the path in this tab."
64294
+ });
64295
+ return;
64296
+ }
64297
+ let sessionId;
64298
+ const samples = [];
64299
+ for (const [index, label] of ["first consultation", "follow-up in the same session"].entries()) {
64300
+ measuringStep = `Measuring the ${label} (${String(index + 1)}/2)\u2026`;
64301
+ await postExpert({ redetect: false });
64302
+ const answer = await consultExpert(cli, {
64303
+ question: PRICING_PROBE,
64304
+ cwd: workspaceRoot ?? process.cwd(),
64305
+ ...config2.expert?.model !== void 0 ? { model: config2.expert.model } : {},
64306
+ // Cold on the first pass, resumed on the second. That pair is the measurement.
64307
+ ...sessionId !== void 0 ? { resumeSessionId: sessionId } : {}
64308
+ }, logger);
64309
+ samples.push(answer.costUsd);
64310
+ sessionId = answer.sessionId ?? sessionId;
64311
+ }
64312
+ const [cold, resumed] = samples;
64313
+ const reportsCost = cold !== void 0 || resumed !== void 0;
64314
+ const pricing = {
64315
+ measuredAt: Date.now(),
64316
+ reportsCost,
64317
+ ...cold !== void 0 ? { coldUsd: cold } : {},
64318
+ ...resumed !== void 0 ? { resumedUsd: resumed } : {}
64319
+ };
64320
+ const { config: current } = await configManager.load();
64321
+ await configManager.save("user", {
64322
+ ...current,
64323
+ expert: { ...current.expert, pricing, reportsCost }
64324
+ });
64325
+ logger.info(reportsCost ? `expert pricing measured: cold ${String(cold)} / resumed ${String(resumed)}` : "expert pricing measured: this plan reports no cost per consultation");
64326
+ } catch (error51) {
64327
+ post({ type: "error", message: `Could not measure the expert's cost: ${String(error51)}` });
64328
+ } finally {
64329
+ measuringStep = void 0;
64330
+ await postExpert({ redetect: false });
64331
+ }
64332
+ }
63818
64333
  async function handleAssessJunior() {
63819
64334
  if (assessmentStep !== void 0)
63820
64335
  return;
@@ -64014,15 +64529,22 @@ function wireChatBridge(services) {
64014
64529
  }
64015
64530
  }
64016
64531
  let indexingAbort;
64532
+ async function saveRetrieval(patch) {
64533
+ const { config: config2 } = await configManager.load();
64534
+ await configManager.save("user", { retrieval: { ...config2.retrieval, ...patch } });
64535
+ }
64017
64536
  async function postDispatcher() {
64018
64537
  const { config: config2 } = await configManager.load();
64019
- const enabled = config2.retrieval?.dispatcher === true;
64020
- const hidden = currentToolRegistry(void 0, void 0, void 0, void 0, true).dispatchOnlyList().length;
64538
+ await refreshSkills();
64539
+ const enabled = dispatcherEnabled(config2.retrieval);
64540
+ const hidden = currentToolRegistry(void 0, void 0, void 0, void 0, true, true).dispatchOnlyList().length;
64021
64541
  const index = docsIndexName(config2);
64022
64542
  post({
64023
64543
  type: "dispatcher",
64024
64544
  enabled,
64025
64545
  hiddenTools: hidden,
64546
+ skills: skillRetrievalEnabled(config2.retrieval),
64547
+ hiddenSkills: skills.length,
64026
64548
  ...index !== void 0 ? { docsIndex: index } : {}
64027
64549
  });
64028
64550
  }
@@ -64634,6 +65156,30 @@ function wireChatBridge(services) {
64634
65156
  post({ type: "error", message: error51 instanceof Error ? error51.message : String(error51) });
64635
65157
  }
64636
65158
  }
65159
+ function codeGeneratorFor(config2) {
65160
+ if (services.allowProgrammingProfile !== true)
65161
+ return void 0;
65162
+ const id = config2.programmingProfileId;
65163
+ if (id === void 0 || id.length === 0)
65164
+ return void 0;
65165
+ const profile = config2.profiles?.find((candidate) => candidate.id === id);
65166
+ if (profile === void 0) {
65167
+ logger.warn(`programming provider "${id}" is configured but no such profile exists; the chat model will write tool source`);
65168
+ return void 0;
65169
+ }
65170
+ return async (request) => {
65171
+ const provider = createChatProvider(profile, httpClient, authStrategyFor(config2, profile), logger);
65172
+ let text = "";
65173
+ for await (const chunk of provider.streamChat([{ role: "user", content: buildCodeGenerationPrompt(request) }], {
65174
+ // No tools offered: it is being asked for a file, and offering tools invites it to use one.
65175
+ ...request.signal !== void 0 ? { signal: request.signal } : {}
65176
+ })) {
65177
+ if (chunk.type === "text")
65178
+ text += chunk.text;
65179
+ }
65180
+ return { source: text, producedBy: profile.label };
65181
+ };
65182
+ }
64637
65183
  async function postSettings() {
64638
65184
  await loadSettings();
64639
65185
  post({
@@ -64643,7 +65189,8 @@ function wireChatBridge(services) {
64643
65189
  maxIterations: cachedMaxIterations,
64644
65190
  accentColor: cachedAccentColor,
64645
65191
  expertColor: cachedExpertColor,
64646
- readRoots: cachedReadRoots
65192
+ readRoots: cachedReadRoots,
65193
+ ...hostCapabilities()
64647
65194
  });
64648
65195
  }
64649
65196
  async function handleAlwaysAllow(id, scope) {
@@ -64930,6 +65477,16 @@ function wireChatBridge(services) {
64930
65477
  void handleSetMode(message.modeId);
64931
65478
  } else if (message.type === "setMaxIterations") {
64932
65479
  void configManager.save("user", { maxIterations: message.value }).then(() => postSettings()).catch((error51) => post({ type: "error", message: String(error51) }));
65480
+ } else if (message.type === "setProgrammingProfile") {
65481
+ void configManager.load().then(async ({ config: config2 }) => {
65482
+ const next = { ...config2 };
65483
+ if (message.id.length === 0)
65484
+ delete next.programmingProfileId;
65485
+ else
65486
+ next.programmingProfileId = message.id;
65487
+ await configManager.save("user", next);
65488
+ await postSettings();
65489
+ }).catch((error51) => post({ type: "error", message: String(error51) }));
64933
65490
  } else if (message.type === "setReadRoots") {
64934
65491
  void configManager.save("user", {
64935
65492
  filesystem: { readRoots: message.roots.map((root) => root.trim()).filter((root) => root.length > 0) }
@@ -64997,11 +65554,17 @@ function wireChatBridge(services) {
64997
65554
  } else if (message.type === "clearSearchLog") {
64998
65555
  searchLog.clear();
64999
65556
  } else if (message.type === "setDispatcher") {
65000
- void configManager.save("user", { retrieval: { dispatcher: message.enabled } }).then(() => {
65557
+ void saveRetrieval({ dispatcher: message.enabled }).then(() => {
65001
65558
  void postDispatcher();
65002
65559
  if (message.enabled)
65003
65560
  scheduleDocsReindex("dispatcher enabled");
65004
65561
  }).catch((error51) => post({ type: "error", message: String(error51) }));
65562
+ } else if (message.type === "setSkillRetrieval") {
65563
+ void saveRetrieval({ skills: message.enabled }).then(() => {
65564
+ void postDispatcher();
65565
+ if (message.enabled)
65566
+ scheduleDocsReindex("skill retrieval enabled");
65567
+ }).catch((error51) => post({ type: "error", message: String(error51) }));
65005
65568
  } else if (message.type === "startIndexing") {
65006
65569
  void handleStartIndexing();
65007
65570
  } else if (message.type === "cancelIndexing") {
@@ -65010,6 +65573,10 @@ function wireChatBridge(services) {
65010
65573
  void handleSaveEmbedder(message.profileId, message.model, message.dimensions, message.indexName, message.indexPrefix);
65011
65574
  } else if (message.type === "requestEmbedderModels") {
65012
65575
  void handleRequestEmbedderModels(message.profileId);
65576
+ } else if (message.type === "openWalkthrough") {
65577
+ void ui.openWalkthrough?.();
65578
+ } else if (message.type === "requestTools") {
65579
+ void postTools();
65013
65580
  } else if (message.type === "requestSchedules") {
65014
65581
  void postSchedules();
65015
65582
  } else if (message.type === "saveSchedule") {
@@ -65026,6 +65593,21 @@ function wireChatBridge(services) {
65026
65593
  ...message.maxConsultations !== void 0 ? { maxConsultations: message.maxConsultations } : {}
65027
65594
  };
65028
65595
  postExpertSpend();
65596
+ } else if (message.type === "setExpertKeepAlive") {
65597
+ void configManager.load().then(async ({ config: config2 }) => {
65598
+ await configManager.save("user", { ...config2, expert: { ...config2.expert, keepAlive: message.enabled } });
65599
+ await postExpert({ redetect: false });
65600
+ }).catch((error51) => post({ type: "error", message: String(error51) }));
65601
+ } else if (message.type === "measureExpertCost") {
65602
+ void handleMeasureExpertCost();
65603
+ } else if (message.type === "clearExpertPricing") {
65604
+ void configManager.load().then(async ({ config: config2 }) => {
65605
+ const expert = { ...config2.expert };
65606
+ delete expert.pricing;
65607
+ delete expert.reportsCost;
65608
+ await configManager.save("user", { ...config2, expert });
65609
+ await postExpert({ redetect: false });
65610
+ }).catch((error51) => post({ type: "error", message: String(error51) }));
65029
65611
  } else if (message.type === "assessJunior") {
65030
65612
  void handleAssessJunior();
65031
65613
  } else if (message.type === "clearAssessment") {
@@ -65163,16 +65745,42 @@ ${entry.content}`);
65163
65745
  function allToolsForPicker() {
65164
65746
  return currentToolRegistry(void 0, void 0, void 0, void 0, false).list().filter((tool) => !NEVER_AVAILABLE_TO_SCHEDULES.includes(tool.name)).map((tool) => ({ name: tool.name, description: tool.description, group: tool.group })).sort((a, b) => a.name.localeCompare(b.name));
65165
65747
  }
65748
+ async function postTools() {
65749
+ const { config: config2 } = await configManager.load();
65750
+ const dispatcher = config2.retrieval?.dispatcher === true;
65751
+ const registry2 = currentToolRegistry(void 0, void 0, void 0, void 0, dispatcher);
65752
+ const advertised = new Set(registry2.promptList().map((tool) => tool.name));
65753
+ const pythonNames = new Set(python.tools().map((tool) => tool.name));
65754
+ const mcpNames = new Set(mcp.enabledTools().map((tool) => tool.name));
65755
+ post({
65756
+ type: "tools",
65757
+ dispatcher,
65758
+ tools: registry2.list().map((tool) => {
65759
+ const server = mcpNames.has(tool.name) ? parseNamespacedToolName(tool.name)?.serverName : void 0;
65760
+ const source = pythonNames.has(tool.name) ? "python" : mcpNames.has(tool.name) ? "mcp" : "built-in";
65761
+ return {
65762
+ name: tool.name,
65763
+ description: tool.description,
65764
+ group: tool.group,
65765
+ source,
65766
+ ...server !== void 0 ? { server } : {},
65767
+ advertised: advertised.has(tool.name)
65768
+ };
65769
+ }).sort((a, b) => a.name.localeCompare(b.name))
65770
+ });
65771
+ }
65166
65772
  async function loadSchedules() {
65167
65773
  const { config: config2 } = await configManager.load();
65168
65774
  return config2.schedules ?? {};
65169
65775
  }
65170
65776
  async function postSchedules() {
65777
+ await refreshSkills();
65171
65778
  const schedules = await loadSchedules();
65172
65779
  post({
65173
65780
  type: "schedules",
65174
65781
  schedules: Object.values(schedules).sort((a, b) => a.name.localeCompare(b.name)),
65175
65782
  tools: allToolsForPicker(),
65783
+ skills: skills.map((skill) => ({ name: skill.name, description: skill.description })),
65176
65784
  ...runningScheduleId !== void 0 ? { runningId: runningScheduleId } : {},
65177
65785
  scheduler: {
65178
65786
  running: scheduleTimer !== void 0,
@@ -65469,6 +66077,7 @@ ${entry.content}`);
65469
66077
  clearTimeout(docsReindexTimer);
65470
66078
  if (scheduleTimer !== void 0)
65471
66079
  clearInterval(scheduleTimer);
66080
+ stopKeepAlive();
65472
66081
  unsubscribe();
65473
66082
  }
65474
66083
  };
@@ -65927,7 +66536,10 @@ var executeCommandTool = {
65927
66536
  parametersSchema: paramsSchema10,
65928
66537
  async execute(params, context) {
65929
66538
  const cwd = params.cwd !== void 0 ? params.cwd : context.workspaceRoot;
65930
- const proc = context.terminal.run(params.command, { cwd });
66539
+ const proc = context.terminal.run(params.command, {
66540
+ cwd,
66541
+ ...context.sessionEnv !== void 0 ? { env: context.sessionEnv } : {}
66542
+ });
65931
66543
  let output = "";
65932
66544
  let truncated = false;
65933
66545
  proc.onData((chunk) => {
@@ -66064,10 +66676,10 @@ function readSmall(raw, params) {
66064
66676
  const end = params.limit !== void 0 ? start + params.limit : lines.length;
66065
66677
  return number4(lines.slice(start, end), start + 1);
66066
66678
  }
66067
- async function readLarge(fs20, realPath, params, size) {
66679
+ async function readLarge(fs22, realPath, params, size) {
66068
66680
  const human = formatBytes(size);
66069
66681
  if (params.tail !== void 0) {
66070
- const part = await readTail(fs20, realPath, size, params.tail);
66682
+ const part = await readTail(fs22, realPath, size, params.tail);
66071
66683
  return [
66072
66684
  `${human} file \u2014 last ${String(part.lines.length)} lines.`,
66073
66685
  /*
@@ -66082,7 +66694,7 @@ async function readLarge(fs20, realPath, params, size) {
66082
66694
  }
66083
66695
  if (params.offset !== void 0) {
66084
66696
  const limit = params.limit ?? DEFAULT_LARGE_LIMIT;
66085
- const part = await readLineWindow(fs20, realPath, size, params.offset, limit);
66697
+ const part = await readLineWindow(fs22, realPath, size, params.offset, limit);
66086
66698
  const shown = part.lines.length;
66087
66699
  return [
66088
66700
  `${human} file \u2014 lines ${String(params.offset)}\u2013${String(params.offset + shown - 1)}${part.hasMoreAfter ? ", more follows" : " (end of file)"}.`,
@@ -66090,7 +66702,7 @@ async function readLarge(fs20, realPath, params, size) {
66090
66702
  number4(part.lines, params.offset)
66091
66703
  ].join("\n");
66092
66704
  }
66093
- const total = await countLines(fs20, realPath, size);
66705
+ const total = await countLines(fs22, realPath, size);
66094
66706
  return [
66095
66707
  `${realPathName(realPath)} is ${human} (${total.toLocaleString()} lines) \u2014 too large to read at once.`,
66096
66708
  "",
@@ -66279,6 +66891,221 @@ function createDefaultToolRegistry() {
66279
66891
  return registry2;
66280
66892
  }
66281
66893
 
66894
+ // src/identity.ts
66895
+ import crypto5 from "node:crypto";
66896
+ var SingleUserIdentity = class _SingleUserIdentity {
66897
+ describe = "single user (local)";
66898
+ static PRINCIPAL = { id: "local", displayName: "Local user" };
66899
+ /** Long-lived, minted per server run, only ever sent in an `Authorization` header. */
66900
+ sessionToken = crypto5.randomBytes(32).toString("base64url");
66901
+ /**
66902
+ * Single-use and short-lived, because it travels in the launch URL's fragment where it
66903
+ * can end up in shell history or a terminal scrollback (§14).
66904
+ */
66905
+ handoffToken = crypto5.randomBytes(32).toString("base64url");
66906
+ handoffExpiresAt = Date.now() + 1e4;
66907
+ get launchToken() {
66908
+ if (this.handoffToken === void 0) throw new Error("handoff token already consumed");
66909
+ return this.handoffToken;
66910
+ }
66911
+ /**
66912
+ * Exchanges the handoff token for the session token, once.
66913
+ *
66914
+ * Cleared on the first attempt whether or not it matched: a wrong guess is either a bug
66915
+ * or an attack, and in both cases the right answer is that this token is now spent.
66916
+ */
66917
+ redeemHandoff(presented) {
66918
+ const expected = this.handoffToken;
66919
+ const expiresAt = this.handoffExpiresAt;
66920
+ this.handoffToken = void 0;
66921
+ if (expected === void 0 || Date.now() > expiresAt) return void 0;
66922
+ return timingSafeEquals(presented, expected) ? this.sessionToken : void 0;
66923
+ }
66924
+ async authenticate(request) {
66925
+ const header = request.headers.authorization;
66926
+ if (header === void 0 || !header.startsWith("Bearer ")) return void 0;
66927
+ return timingSafeEquals(header.slice("Bearer ".length), this.sessionToken) ? _SingleUserIdentity.PRINCIPAL : void 0;
66928
+ }
66929
+ };
66930
+ function timingSafeEquals(a, b) {
66931
+ const left = Buffer.from(a);
66932
+ const right = Buffer.from(b);
66933
+ if (left.length !== right.length) return false;
66934
+ return crypto5.timingSafeEqual(left, right);
66935
+ }
66936
+ function storageKeyFor(principal) {
66937
+ return crypto5.createHash("sha256").update(principal.id).digest("hex").slice(0, 32);
66938
+ }
66939
+
66940
+ // src/roles.ts
66941
+ var ADMIN_ONLY_MESSAGES = [
66942
+ /*
66943
+ * The *shared* provider set, and the default a new user inherits.
66944
+ *
66945
+ * A user's own profiles are theirs — see PERSONAL_SETTINGS. That is a reversal, made
66946
+ * deliberately: the original rule froze all of `profiles` because a second user was treated as
66947
+ * the same threat as a hostile repository. The threat that reasoning is about is one user
66948
+ * repointing *another's* gateway, and a per-user profile cannot do that — someone bringing
66949
+ * their own key is spending their own money against a host they chose.
66950
+ */
66951
+ "saveSharedProfile",
66952
+ "deleteSharedProfile",
66953
+ "setDefaultProfile",
66954
+ // Writes a whole profile list, so it is not the same act as exporting one.
66955
+ "importConfig",
66956
+ // Processes this machine will spawn.
66957
+ "saveMcpServer",
66958
+ "saveMcpServers",
66959
+ "deleteMcpServer",
66960
+ "duplicateMcpServer",
66961
+ "restartMcpServer",
66962
+ "connectMcpServer",
66963
+ "setMcpServerEnabled",
66964
+ "setMcpToolPermission",
66965
+ // Names an interpreter and a tools directory — `python.uvPath` is on invariant 5 for this.
66966
+ "setPython",
66967
+ "deletePythonTool",
66968
+ "approvePythonTool",
66969
+ // Names an executable that costs money to run.
66970
+ "setExpert",
66971
+ "assessJunior",
66972
+ "clearAssessment",
66973
+ // TLS trust and client identity for every outbound connection.
66974
+ "saveNetwork",
66975
+ // Where the corpus is sent, and what is embedded into it.
66976
+ "saveSearchConnection",
66977
+ "deleteSearchConnection",
66978
+ "setActiveSearchConnection",
66979
+ "saveEmbedder",
66980
+ "setDispatcher",
66981
+ "startIndexing",
66982
+ "indexDocs",
66983
+ "clearDocsIndex",
66984
+ "syncVectorStore",
66985
+ // Reading beyond the workspace, and where skills come from.
66986
+ "setReadRoots",
66987
+ "saveSkillDirs",
66988
+ "deleteSkillFile",
66989
+ // Unattended execution with a pre-granted tool list.
66990
+ // Session variables an administrator sets for everyone. A user saving their own is
66991
+ // `saveUserVariables`, which is deliberately not here — it is theirs.
66992
+ // Approving model-authored code is the whole point of the queue.
66993
+ "decideReview",
66994
+ "saveAdminVariables",
66995
+ "saveAdminIds",
66996
+ "saveSchedule",
66997
+ "deleteSchedule",
66998
+ "setScheduleEnabled",
66999
+ "runScheduleNow",
67000
+ "duplicateSchedule",
67001
+ // Approvals are stored per workspace but govern what runs without asking.
67002
+ "setAutoApprove",
67003
+ "revokeAllowedTool",
67004
+ "revokeAllowedCommand"
67005
+ ];
67006
+ var ADMIN_ONLY = new Set(ADMIN_ONLY_MESSAGES);
67007
+ var PERSONAL_SETTINGS = /* @__PURE__ */ new Set([
67008
+ // A user's own session variables. Caught by the unknown-mutating-verb rule, which is the
67009
+ // safety net working — the net is meant to be wrong in this direction, and this is where the
67010
+ // exception gets made deliberately rather than by weakening the rule.
67011
+ "saveUserVariables",
67012
+ /*
67013
+ * A user's own provider profiles, including their own API key.
67014
+ *
67015
+ * They cannot reach the shared ones: the config store strips a shared profile from anything
67016
+ * written to a user's file, so that boundary is storage rather than this list. Test Connection
67017
+ * is theirs too — a diagnostic against a profile they can already use, and refusing it would
67018
+ * leave someone unable to find out why their own key does not work.
67019
+ */
67020
+ "saveProfile",
67021
+ "deleteProfile",
67022
+ "duplicateProfile",
67023
+ "setActiveProfile",
67024
+ "testConnection",
67025
+ "exportConfig",
67026
+ "setMode",
67027
+ "setAccentColor",
67028
+ "setExpertColor",
67029
+ "setMaxIterations",
67030
+ "setTaskExpertLimits"
67031
+ ]);
67032
+ function isAdminOnly(messageType) {
67033
+ if (PERSONAL_SETTINGS.has(messageType)) return false;
67034
+ if (ADMIN_ONLY.has(messageType)) return true;
67035
+ return /^(save|set|delete|duplicate|clear|restart|connect|revoke|import|export)/.test(messageType);
67036
+ }
67037
+ var SINGLE_USER_POLICY = {
67038
+ shared: false,
67039
+ roleFor: () => "admin"
67040
+ };
67041
+ function refusalFor(messageType) {
67042
+ return `"${messageType}" changes configuration that the administrator owns on a shared server, so it was not applied. Everything about your own session \u2014 chatting, editing, the mode and appearance \u2014 is unaffected. Ask whoever runs this server if a setting needs changing.`;
67043
+ }
67044
+
67045
+ // src/security.ts
67046
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
67047
+ function checkRequest(request, policy, options) {
67048
+ const host = request.headers.host;
67049
+ if (host === void 0 || !policy.allowedHosts.includes(host.toLowerCase())) {
67050
+ return {
67051
+ status: 421,
67052
+ reason: `Host "${host ?? "(absent)"}" is not one this server answers to. This is what blocks DNS rebinding.`
67053
+ };
67054
+ }
67055
+ const origin = request.headers.origin;
67056
+ if (origin !== void 0 && !policy.allowedOrigins.includes(origin.toLowerCase())) {
67057
+ return { status: 403, reason: `Origin "${origin}" is not allowed.` };
67058
+ }
67059
+ const fetchSite = request.headers["sec-fetch-site"];
67060
+ if (typeof fetchSite === "string" && fetchSite !== "same-origin" && fetchSite !== "none") {
67061
+ return { status: 403, reason: `Cross-site request (Sec-Fetch-Site: ${fetchSite}) is not allowed.` };
67062
+ }
67063
+ const method = (request.method ?? "GET").toUpperCase();
67064
+ if (options.requireOrigin && !SAFE_METHODS.has(method) && origin === void 0) {
67065
+ return { status: 403, reason: `Missing Origin header on a ${method}.` };
67066
+ }
67067
+ return void 0;
67068
+ }
67069
+ function securityHeaders() {
67070
+ return {
67071
+ "Content-Security-Policy": [
67072
+ "default-src 'none'",
67073
+ "script-src 'self'",
67074
+ // The UI styles through the CSSOM rather than inline attributes, but the browser
67075
+ // build also needs a stylesheet for the page shell.
67076
+ "style-src 'self' 'unsafe-inline'",
67077
+ "img-src 'self' data:",
67078
+ "font-src 'self'",
67079
+ "connect-src 'self'",
67080
+ "frame-ancestors 'none'",
67081
+ "base-uri 'none'",
67082
+ "form-action 'none'"
67083
+ ].join("; "),
67084
+ "X-Content-Type-Options": "nosniff",
67085
+ "Referrer-Policy": "no-referrer",
67086
+ // Nothing here needs a camera, a microphone or a location.
67087
+ "Permissions-Policy": "camera=(), microphone=(), geolocation=(), interest-cohort=()",
67088
+ "Cache-Control": "no-store"
67089
+ // Deliberately no Access-Control-Allow-Origin: no other origin may read these replies.
67090
+ };
67091
+ }
67092
+ function reject(response, rejected) {
67093
+ response.writeHead(rejected.status, { "Content-Type": "text/plain", ...securityHeaders() });
67094
+ response.end(rejected.reason);
67095
+ }
67096
+ async function readJsonBody(request, maxBytes = 32 * 1024 * 1024) {
67097
+ const chunks = [];
67098
+ let total = 0;
67099
+ for await (const chunk of request) {
67100
+ const buffer = chunk;
67101
+ total += buffer.length;
67102
+ if (total > maxBytes) throw new Error("Request body too large.");
67103
+ chunks.push(buffer);
67104
+ }
67105
+ if (total === 0) return void 0;
67106
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
67107
+ }
67108
+
66282
67109
  // src/fileSecretStore.ts
66283
67110
  import fs17 from "node:fs/promises";
66284
67111
  import path21 from "node:path";
@@ -66337,7 +67164,233 @@ var FileSecretStore = class {
66337
67164
  }
66338
67165
  };
66339
67166
 
67167
+ // src/reviewQueue.ts
67168
+ import crypto6 from "node:crypto";
67169
+ import fs18 from "node:fs/promises";
67170
+ import path22 from "node:path";
67171
+ var ReviewQueue = class {
67172
+ constructor(filePath) {
67173
+ this.filePath = filePath;
67174
+ }
67175
+ filePath;
67176
+ cache;
67177
+ async load() {
67178
+ if (this.cache !== void 0) return this.cache;
67179
+ try {
67180
+ const parsed = JSON.parse(await fs18.readFile(this.filePath, "utf8"));
67181
+ this.cache = Array.isArray(parsed) ? parsed : [];
67182
+ } catch {
67183
+ this.cache = [];
67184
+ }
67185
+ return this.cache;
67186
+ }
67187
+ async persist(items) {
67188
+ this.cache = items;
67189
+ await fs18.mkdir(path22.dirname(this.filePath), { recursive: true });
67190
+ const temporary = `${this.filePath}.tmp`;
67191
+ await fs18.writeFile(temporary, JSON.stringify(items, null, 2), { encoding: "utf8", mode: 384 });
67192
+ await fs18.rename(temporary, this.filePath);
67193
+ }
67194
+ async list() {
67195
+ return [...await this.load()];
67196
+ }
67197
+ async pending() {
67198
+ return (await this.load()).filter((item) => item.status === "pending");
67199
+ }
67200
+ async submit(request) {
67201
+ const items = await this.load();
67202
+ const superseded = items.findIndex(
67203
+ (item) => item.status === "pending" && item.kind === request.kind && item.name === request.name
67204
+ );
67205
+ const queued = {
67206
+ ...request,
67207
+ id: crypto6.randomUUID(),
67208
+ submittedAt: Date.now(),
67209
+ status: "pending"
67210
+ };
67211
+ if (superseded === -1) items.push(queued);
67212
+ else items[superseded] = queued;
67213
+ await this.persist(items);
67214
+ return queued;
67215
+ }
67216
+ async decide(id, decision) {
67217
+ const items = await this.load();
67218
+ const item = items.find((candidate) => candidate.id === id);
67219
+ if (item === void 0 || item.status !== "pending") return void 0;
67220
+ item.status = decision.approved ? "approved" : "rejected";
67221
+ item.decidedBy = decision.by;
67222
+ item.decidedAt = Date.now();
67223
+ if (decision.reason !== void 0 && decision.reason.length > 0) item.reason = decision.reason;
67224
+ await this.persist(items);
67225
+ return item;
67226
+ }
67227
+ /**
67228
+ * Drops decided items older than the cutoff.
67229
+ *
67230
+ * Kept for a while rather than deleted on decision: "who approved this and when" is the question
67231
+ * a review queue exists to be able to answer afterwards, and the audit log records the decision
67232
+ * but not the source that was read.
67233
+ */
67234
+ async prune(olderThanMs) {
67235
+ const cutoff = Date.now() - olderThanMs;
67236
+ const items = await this.load();
67237
+ const kept = items.filter((item) => item.status === "pending" || (item.decidedAt ?? 0) > cutoff);
67238
+ if (kept.length !== items.length) await this.persist(kept);
67239
+ }
67240
+ };
67241
+
67242
+ // src/sharedProfiles.ts
67243
+ var SHARED_PREFIX = "shared:";
67244
+ function isSharedProfileId(id) {
67245
+ return id.startsWith(SHARED_PREFIX);
67246
+ }
67247
+ function toSharedProfileId(id) {
67248
+ return `${SHARED_PREFIX}${id}`;
67249
+ }
67250
+ function isSharedSecretRef(ref) {
67251
+ return ref.startsWith(`profile:${SHARED_PREFIX}`);
67252
+ }
67253
+ function presentSharedProfiles(profiles) {
67254
+ return profiles.map((profile) => ({
67255
+ ...profile,
67256
+ id: toSharedProfileId(profile.id),
67257
+ ...profile.auth.type === "apiKey" && profile.auth.apiKeyRef !== void 0 ? { auth: { ...profile.auth, apiKeyRef: `profile:${toSharedProfileId(profile.id)}:apiKey` } } : {}
67258
+ }));
67259
+ }
67260
+ var SharedProfileConfigStore = class {
67261
+ constructor(inner, shared) {
67262
+ this.inner = inner;
67263
+ this.shared = shared;
67264
+ }
67265
+ inner;
67266
+ shared;
67267
+ async read(scope) {
67268
+ const raw = await this.inner.read(scope);
67269
+ if (scope !== "user") return raw;
67270
+ const shared = this.shared();
67271
+ const presented = presentSharedProfiles(shared.profiles);
67272
+ if (presented.length === 0) return raw;
67273
+ let parsed;
67274
+ try {
67275
+ parsed = raw === void 0 ? {} : JSON.parse(raw);
67276
+ } catch {
67277
+ return raw;
67278
+ }
67279
+ const own = Array.isArray(parsed["profiles"]) ? parsed["profiles"] : [];
67280
+ const merged = [...presented, ...own.filter((profile) => !isSharedProfileId(profile.id))];
67281
+ const activeId = typeof parsed["activeProfileId"] === "string" ? parsed["activeProfileId"] : void 0;
67282
+ const resolvedActive = activeId !== void 0 && merged.some((profile) => profile.id === activeId) ? activeId : shared.defaultProfileId !== void 0 && merged.some((profile) => profile.id === toSharedProfileId(shared.defaultProfileId ?? "")) ? toSharedProfileId(shared.defaultProfileId) : activeId;
67283
+ const ownProgramming = typeof parsed["programmingProfileId"] === "string" ? parsed["programmingProfileId"] : void 0;
67284
+ const sharedProgramming = shared.defaultProgrammingProfileId !== void 0 ? toSharedProfileId(shared.defaultProgrammingProfileId) : void 0;
67285
+ const resolvedProgramming = ownProgramming !== void 0 && merged.some((profile) => profile.id === ownProgramming) ? ownProgramming : sharedProgramming !== void 0 && merged.some((profile) => profile.id === sharedProgramming) ? sharedProgramming : ownProgramming;
67286
+ return JSON.stringify({
67287
+ ...parsed,
67288
+ profiles: merged,
67289
+ ...resolvedActive !== void 0 ? { activeProfileId: resolvedActive } : {},
67290
+ ...resolvedProgramming !== void 0 ? { programmingProfileId: resolvedProgramming } : {}
67291
+ });
67292
+ }
67293
+ async write(scope, contents) {
67294
+ if (scope !== "user") return this.inner.write(scope, contents);
67295
+ let parsed;
67296
+ try {
67297
+ parsed = JSON.parse(contents);
67298
+ } catch {
67299
+ return this.inner.write(scope, contents);
67300
+ }
67301
+ if (Array.isArray(parsed["profiles"])) {
67302
+ parsed["profiles"] = parsed["profiles"].filter(
67303
+ (profile) => !isSharedProfileId(profile.id)
67304
+ );
67305
+ }
67306
+ return this.inner.write(scope, JSON.stringify(parsed, null, 2));
67307
+ }
67308
+ watch(scope, onChange) {
67309
+ return this.inner.watch(scope, onChange);
67310
+ }
67311
+ };
67312
+ var RoutedSecretStore = class {
67313
+ constructor(own, shared) {
67314
+ this.own = own;
67315
+ this.shared = shared;
67316
+ }
67317
+ own;
67318
+ shared;
67319
+ storeFor(key) {
67320
+ return isSharedSecretRef(key) ? this.shared : this.own;
67321
+ }
67322
+ async get(key) {
67323
+ return this.storeFor(key).get(key);
67324
+ }
67325
+ async set(key, value) {
67326
+ return this.storeFor(key).set(key, value);
67327
+ }
67328
+ async delete(key) {
67329
+ return this.storeFor(key).delete(key);
67330
+ }
67331
+ /**
67332
+ * Clears the user's own only.
67333
+ *
67334
+ * "Clear all stored secrets" is offered to every user, and an administrator's key is not theirs
67335
+ * to destroy — one person tidying up would otherwise break the gateway for everybody. An
67336
+ * administrator clears the shared ones from the shared store.
67337
+ */
67338
+ async clear() {
67339
+ return this.own.clear();
67340
+ }
67341
+ backendName() {
67342
+ return this.own.backendName();
67343
+ }
67344
+ };
67345
+
67346
+ // src/userVariables.ts
67347
+ import fs19 from "node:fs/promises";
67348
+ import { readFileSync } from "node:fs";
67349
+ import path23 from "node:path";
67350
+ var UserVariableStore = class {
67351
+ constructor(filePath) {
67352
+ this.filePath = filePath;
67353
+ }
67354
+ filePath;
67355
+ /**
67356
+ * Synchronous, because it is read on the path that builds a command's environment and an
67357
+ * `await` there would make every tool call wait on a file. It is a few hundred bytes.
67358
+ */
67359
+ read() {
67360
+ return readVariablesFile(this.filePath);
67361
+ }
67362
+ async save(variables) {
67363
+ const parsed = sessionVariablesSchema.parse(variables);
67364
+ await fs19.mkdir(path23.dirname(this.filePath), { recursive: true });
67365
+ const temporary = `${this.filePath}.tmp`;
67366
+ await fs19.writeFile(temporary, JSON.stringify({ variables: parsed }, null, 2), {
67367
+ encoding: "utf8",
67368
+ mode: 384
67369
+ });
67370
+ await fs19.rename(temporary, this.filePath);
67371
+ return parsed;
67372
+ }
67373
+ };
67374
+ function userVariableStoreFor(dataDir, principal) {
67375
+ return new UserVariableStore(userVariablesPath(path23.join(dataDir, "users", storageKeyFor(principal))));
67376
+ }
67377
+ function userVariablesPath(userDir) {
67378
+ return path23.join(userDir, "variables.json");
67379
+ }
67380
+ function readVariablesFile(filePath) {
67381
+ try {
67382
+ const raw = JSON.parse(readFileSync(filePath, "utf8"));
67383
+ const parsed = sessionVariablesSchema.safeParse(raw["variables"]);
67384
+ return parsed.success ? parsed.data : [];
67385
+ } catch {
67386
+ return [];
67387
+ }
67388
+ }
67389
+
66340
67390
  // src/session.ts
67391
+ import { watch as fsWatch } from "node:fs";
67392
+ import fs20 from "node:fs/promises";
67393
+ import path24 from "node:path";
66341
67394
  var FileConfigStore = class {
66342
67395
  constructor(userConfigPath, workspaceRoot) {
66343
67396
  this.userConfigPath = userConfigPath;
@@ -66353,7 +67406,7 @@ var FileConfigStore = class {
66353
67406
  const filePath = this.pathFor(scope);
66354
67407
  if (filePath === void 0) return void 0;
66355
67408
  try {
66356
- return await fs18.readFile(filePath, "utf8");
67409
+ return await fs20.readFile(filePath, "utf8");
66357
67410
  } catch (error51) {
66358
67411
  if (error51.code === "ENOENT") return void 0;
66359
67412
  throw error51;
@@ -66362,8 +67415,8 @@ var FileConfigStore = class {
66362
67415
  async write(scope, contents) {
66363
67416
  const filePath = this.pathFor(scope);
66364
67417
  if (filePath === void 0) throw new Error(`Cannot write ${scope} config: no workspace is open`);
66365
- await fs18.mkdir(path22.dirname(filePath), { recursive: true });
66366
- await fs18.writeFile(filePath, contents, { encoding: "utf8", mode: 384 });
67418
+ await fs20.mkdir(path24.dirname(filePath), { recursive: true });
67419
+ await fs20.writeFile(filePath, contents, { encoding: "utf8", mode: 384 });
66367
67420
  }
66368
67421
  watch(scope, onChange) {
66369
67422
  const filePath = this.pathFor(scope);
@@ -66371,8 +67424,8 @@ var FileConfigStore = class {
66371
67424
  };
66372
67425
  let watcher;
66373
67426
  try {
66374
- watcher = fsWatch(path22.dirname(filePath), (_event, filename) => {
66375
- if (filename === path22.basename(filePath)) onChange();
67427
+ watcher = fsWatch(path24.dirname(filePath), (_event, filename) => {
67428
+ if (filename === path24.basename(filePath)) onChange();
66376
67429
  });
66377
67430
  } catch {
66378
67431
  }
@@ -66387,7 +67440,7 @@ var FileWorkspaceState = class {
66387
67440
  values = {};
66388
67441
  async load() {
66389
67442
  try {
66390
- const parsed = JSON.parse(await fs18.readFile(this.filePath, "utf8"));
67443
+ const parsed = JSON.parse(await fs20.readFile(this.filePath, "utf8"));
66391
67444
  if (typeof parsed === "object" && parsed !== null) this.values = parsed;
66392
67445
  } catch {
66393
67446
  this.values = {};
@@ -66399,8 +67452,8 @@ var FileWorkspaceState = class {
66399
67452
  async set(key, value) {
66400
67453
  if (value === void 0) delete this.values[key];
66401
67454
  else this.values[key] = value;
66402
- await fs18.mkdir(path22.dirname(this.filePath), { recursive: true });
66403
- await fs18.writeFile(this.filePath, JSON.stringify(this.values, null, 2), { encoding: "utf8", mode: 384 });
67455
+ await fs20.mkdir(path24.dirname(this.filePath), { recursive: true });
67456
+ await fs20.writeFile(this.filePath, JSON.stringify(this.values, null, 2), { encoding: "utf8", mode: 384 });
66404
67457
  }
66405
67458
  };
66406
67459
  function createBrowserUi(workspaceRoot, post) {
@@ -66436,14 +67489,14 @@ function createBrowserUi(workspaceRoot, post) {
66436
67489
  if (found.length >= limit || depth > 12) return;
66437
67490
  let entries;
66438
67491
  try {
66439
- entries = await fs18.readdir(dir, { withFileTypes: true });
67492
+ entries = await fs20.readdir(dir, { withFileTypes: true });
66440
67493
  } catch {
66441
67494
  return;
66442
67495
  }
66443
67496
  for (const entry of entries) {
66444
67497
  if (found.length >= limit) return;
66445
67498
  if (entry.name.startsWith(".") && entry.name !== ".env") continue;
66446
- const full = path22.join(dir, entry.name);
67499
+ const full = path24.join(dir, entry.name);
66447
67500
  if (entry.isDirectory()) {
66448
67501
  if (!skip.has(entry.name)) await walk(full, depth + 1);
66449
67502
  } else if (needle.length === 0 || entry.name.toLowerCase().includes(needle)) {
@@ -66457,20 +67510,50 @@ function createBrowserUi(workspaceRoot, post) {
66457
67510
  };
66458
67511
  }
66459
67512
  async function createSession(options) {
66460
- const userDir = path22.join(options.dataDir, "users", storageKeyFor(options.principal));
66461
- await fs18.mkdir(userDir, { recursive: true, mode: 448 });
66462
- const workspaceState = new FileWorkspaceState(path22.join(userDir, "workspace-state.json"));
67513
+ const userDir = path24.join(options.dataDir, "users", storageKeyFor(options.principal));
67514
+ await fs20.mkdir(userDir, { recursive: true, mode: 448 });
67515
+ const variableStore = new UserVariableStore(userVariablesPath(userDir));
67516
+ const userVariables = () => variableStore.read();
67517
+ const workspaceState = new FileWorkspaceState(path24.join(userDir, "workspace-state.json"));
66463
67518
  await workspaceState.load();
66464
67519
  const services = {
66465
67520
  transport: options.transport,
66466
- secrets: new FileSecretStore(path22.join(userDir, "secrets.json")),
66467
- configStore: new FileConfigStore(path22.join(userDir, "config.json"), options.workspaceRoot),
67521
+ /*
67522
+ * A shared profile's API key belongs to the administrator and lives beside the shared config;
67523
+ * everything else is this user's. Routed by the reference, which is all a secret store gets.
67524
+ */
67525
+ secrets: options.sharedSecrets === void 0 ? new FileSecretStore(path24.join(userDir, "secrets.json")) : new RoutedSecretStore(new FileSecretStore(path24.join(userDir, "secrets.json")), options.sharedSecrets),
67526
+ configStore: options.sharedProfiles === void 0 ? new FileConfigStore(path24.join(userDir, "config.json"), options.workspaceRoot) : new SharedProfileConfigStore(
67527
+ new FileConfigStore(path24.join(userDir, "config.json"), options.workspaceRoot),
67528
+ options.sharedProfiles
67529
+ ),
66468
67530
  workspaceState,
66469
67531
  ui: createBrowserUi(options.workspaceRoot, options.logSink),
66470
67532
  workspaceRoot: options.workspaceRoot,
66471
67533
  storageDir: userDir,
66472
67534
  ripgrepPath: options.ripgrepPath,
66473
- logSink: options.logSink
67535
+ logSink: options.logSink,
67536
+ /*
67537
+ * Served from this origin, which is what `img-src 'self'` in the CSP permits and the whole
67538
+ * reason the diagrams are copied into the client bundle rather than fetched. A relative base
67539
+ * also survives whatever port the server happened to bind.
67540
+ */
67541
+ guideMediaBase: "/guide",
67542
+ /*
67543
+ * Offered here and nowhere else. A shared server is where "a cheap model chats, a good one
67544
+ * writes the code" is worth configuring — and where an administrator can set a default for
67545
+ * people who have not chosen.
67546
+ */
67547
+ allowProgrammingProfile: true,
67548
+ ...options.submitForReview !== void 0 ? { submitForReview: options.submitForReview } : {},
67549
+ /*
67550
+ * Resolved per read, so both halves stay live — an administrator's edit and the user's own
67551
+ * each reach the next command rather than the next session.
67552
+ *
67553
+ * The administrator's win. That is a precedence rule and not a secrecy one: everything a
67554
+ * session spawns runs as the service account, so another user's agent can read these.
67555
+ */
67556
+ sessionEnv: () => toEnvironment(resolveSessionVariables(options.adminVariables?.() ?? [], userVariables()))
66474
67557
  };
66475
67558
  new Logger({ level: "debug", sink: options.logSink }).info(
66476
67559
  `session for ${options.principal.displayName} \u2192 ${userDir}`
@@ -66482,21 +67565,61 @@ async function createSession(options) {
66482
67565
  var CLIENT_ASSETS = {
66483
67566
  "/": "index.html",
66484
67567
  "/index.html": "index.html",
67568
+ /*
67569
+ * The administrator's URL. The same page — the client asks the server what it may do rather
67570
+ * than being a second bundle — but a distinct address, because that is what a proxy rule can
67571
+ * be written against.
67572
+ *
67573
+ * **Reaching it is assumed to be restricted upstream.** Light Code does not re-derive who may
67574
+ * be here; the proxy, the firewall or a separate listener decides. The consequence, stated
67575
+ * once so nobody has to infer it: anyone who can reach `/admin` directly is an administrator,
67576
+ * so exposing the port without the proxy in front exposes this with it.
67577
+ */
67578
+ "/admin": "index.html",
67579
+ "/admin/": "index.html",
66485
67580
  "/client.js": "client.js",
66486
- "/client.css": "client.css"
67581
+ "/client.css": "client.css",
67582
+ /*
67583
+ * The guide's diagrams, one entry per step and palette.
67584
+ *
67585
+ * Derived from `GUIDE_STEPS` rather than listed by hand, but still a *fixed table*: the keys
67586
+ * come from checked-in data, never from the request, so `serveAsset` keeps the property that
67587
+ * makes it safe — no part of the path is attacker-supplied and traversal is unreachable.
67588
+ */
67589
+ ...Object.fromEntries(
67590
+ GUIDE_STEPS.flatMap(
67591
+ (step) => ["light", "dark"].map((theme) => [
67592
+ `/guide/${step.id}-${theme}.svg`,
67593
+ `guide/${step.id}-${theme}.svg`
67594
+ ])
67595
+ )
67596
+ )
66487
67597
  };
66488
67598
  var CONTENT_TYPES = {
66489
67599
  ".html": "text/html; charset=utf-8",
66490
67600
  ".js": "text/javascript; charset=utf-8",
66491
- ".css": "text/css; charset=utf-8"
67601
+ ".css": "text/css; charset=utf-8",
67602
+ // Served as an image, and the CSP's `img-src 'self'` is what keeps it one: an SVG loaded
67603
+ // through <img> cannot run script, whatever it contains.
67604
+ ".svg": "image/svg+xml"
66492
67605
  };
66493
67606
  async function startServer(options) {
66494
67607
  const log = options.logSink ?? ((line) => process.stderr.write(`${line}
66495
67608
  `));
66496
67609
  const identity = options.identity ?? new SingleUserIdentity();
66497
67610
  const roles = options.roles ?? SINGLE_USER_POLICY;
67611
+ const sharedStore = options.sharedConfig;
67612
+ const sharedSecretStore = new FileSecretStore(path25.join(options.dataDir, "shared-secrets.json"));
67613
+ const reviews = new ReviewQueue(path25.join(options.dataDir, "reviews.json"));
67614
+ let sharedCache = { variables: [], adminIds: [], profiles: [] };
66498
67615
  const bindAddress = options.bindAddress ?? "127.0.0.1";
67616
+ if (sharedStore !== void 0) sharedCache = await sharedStore.load();
67617
+ const adminConnections = /* @__PURE__ */ new Set();
66499
67618
  const connections = /* @__PURE__ */ new Map();
67619
+ function isAdminSession(principal) {
67620
+ if (!roles.shared) return true;
67621
+ return adminConnections.has(principal.id) && roles.roleFor(principal) === "admin";
67622
+ }
66500
67623
  let policy = { allowedHosts: [], allowedOrigins: [] };
66501
67624
  async function openConnection(principal, response) {
66502
67625
  const listeners = /* @__PURE__ */ new Set();
@@ -66525,7 +67648,37 @@ async function startServer(options) {
66525
67648
  workspaceRoot: options.workspaceRoot,
66526
67649
  dataDir: options.dataDir,
66527
67650
  ripgrepPath: options.ripgrepPath,
66528
- logSink: log
67651
+ logSink: log,
67652
+ /*
67653
+ * Read at use, not captured: an administrator saving a variable must reach a session that
67654
+ * is already open. `SharedConfigStore` caches, so this is a map lookup rather than a read.
67655
+ */
67656
+ adminVariables: () => sharedCache.variables,
67657
+ /*
67658
+ * Only for someone who cannot approve their own work. An administrator keeps the ordinary
67659
+ * in-chat prompt — the same mechanism with the approver already at the screen — so this is
67660
+ * absent for them rather than a queue they would have to visit to approve themselves.
67661
+ */
67662
+ ...roles.shared && !isAdminSession(principal) ? {
67663
+ submitForReview: async (request) => {
67664
+ const queued = await reviews.submit({ ...request, authorId: principal.id, authorName: principal.displayName });
67665
+ log(`${principal.displayName} submitted ${request.kind} "${request.name}" for review`);
67666
+ await broadcastReviews();
67667
+ return describeSubmission(queued);
67668
+ }
67669
+ } : {},
67670
+ /*
67671
+ * Only in shared mode. Outside it there is one person and every profile is already theirs,
67672
+ * so wrapping the stores would add a prefix nobody needs and a second file nobody writes.
67673
+ */
67674
+ ...sharedStore !== void 0 ? {
67675
+ sharedProfiles: () => ({
67676
+ profiles: sharedCache.profiles,
67677
+ ...sharedCache.defaultProfileId !== void 0 ? { defaultProfileId: sharedCache.defaultProfileId } : {},
67678
+ ...sharedCache.defaultProgrammingProfileId !== void 0 ? { defaultProgrammingProfileId: sharedCache.defaultProgrammingProfileId } : {}
67679
+ }),
67680
+ sharedSecrets: sharedSecretStore
67681
+ } : {}
66529
67682
  });
66530
67683
  const originalDispose = connection.dispose;
66531
67684
  connection.dispose = () => {
@@ -66583,8 +67736,18 @@ async function startServer(options) {
66583
67736
  ...securityHeaders()
66584
67737
  });
66585
67738
  response.write(": connected\n\n");
67739
+ const viaAdminUrl = url2.searchParams.get("view") === "admin";
67740
+ if (viaAdminUrl) adminConnections.add(principal.id);
67741
+ else adminConnections.delete(principal.id);
66586
67742
  const connection = await openConnection(principal, response);
66587
67743
  connections.set(principal.id, connection);
67744
+ connection.transport.post({
67745
+ type: "hostRole",
67746
+ role: isAdminSession(principal) ? "admin" : "user",
67747
+ shared: roles.shared,
67748
+ displayName: principal.displayName,
67749
+ sharedProfileIds: sharedCache.profiles.map((profile) => toSharedProfileId(profile.id))
67750
+ });
66588
67751
  const heartbeat = setInterval(() => response.write(": ping\n\n"), 2e4);
66589
67752
  const cleanup = () => {
66590
67753
  clearInterval(heartbeat);
@@ -66602,18 +67765,154 @@ async function startServer(options) {
66602
67765
  }
66603
67766
  const body = await readJsonBody(request);
66604
67767
  const type = typeof body?.type === "string" ? body.type : "";
66605
- if (roles.shared && roles.roleFor(principal) !== "admin" && isAdminOnly(type)) {
67768
+ if (roles.shared && !isAdminSession(principal) && isAdminOnly(type)) {
66606
67769
  log(`refused "${type}" from ${principal.displayName} (${principal.id}): not an administrator`);
66607
67770
  connection.transport.post({ type: "error", message: refusalFor(type) });
66608
67771
  respondJson(response, 403, { ok: false });
66609
67772
  return;
66610
67773
  }
67774
+ if (await handleVariableMessage(principal, type, body, connection)) {
67775
+ respondJson(response, 202, { ok: true });
67776
+ return;
67777
+ }
66611
67778
  connection.deliver(body);
66612
67779
  respondJson(response, 202, { ok: true });
66613
67780
  return;
66614
67781
  }
66615
67782
  reject(response, { status: 404, reason: "Not found." });
66616
67783
  }
67784
+ async function postReviews(principal, connection) {
67785
+ const canDecide = isAdminSession(principal);
67786
+ const all = await reviews.list();
67787
+ const visible = canDecide ? all : all.filter((item) => item.authorId === principal.id);
67788
+ connection.transport.post({
67789
+ type: "reviews",
67790
+ canDecide,
67791
+ items: visible.sort((a, b) => b.submittedAt - a.submittedAt).map((item) => ({
67792
+ id: item.id,
67793
+ kind: item.kind,
67794
+ name: item.name,
67795
+ content: item.content,
67796
+ existingContent: item.existingContent,
67797
+ authorName: item.authorName,
67798
+ submittedAt: item.submittedAt,
67799
+ status: item.status,
67800
+ ...item.producedBy !== void 0 ? { producedBy: item.producedBy } : {},
67801
+ ...item.decidedBy !== void 0 ? { decidedBy: item.decidedBy } : {},
67802
+ ...item.reason !== void 0 ? { reason: item.reason } : {}
67803
+ }))
67804
+ });
67805
+ }
67806
+ async function broadcastReviews() {
67807
+ for (const [id, connection] of connections) {
67808
+ await postReviews({ id, displayName: id }, connection);
67809
+ }
67810
+ }
67811
+ async function applyApproval(item) {
67812
+ if (options.workspaceRoot === void 0) return "No workspace is open, so there is nowhere to write it.";
67813
+ try {
67814
+ if (item.kind === "skill") {
67815
+ const dir2 = path25.join(options.workspaceRoot, ".lightcode", "skills");
67816
+ await fs21.mkdir(dir2, { recursive: true });
67817
+ await fs21.writeFile(path25.join(dir2, `${item.name}.md`), item.content, "utf8");
67818
+ return void 0;
67819
+ }
67820
+ const dir = path25.join(options.workspaceRoot, ".lightcode", "tools");
67821
+ await fs21.mkdir(dir, { recursive: true });
67822
+ await fs21.writeFile(path25.join(dir, `${item.name}.py`), item.content, "utf8");
67823
+ return void 0;
67824
+ } catch (error51) {
67825
+ return error51 instanceof Error ? error51.message : String(error51);
67826
+ }
67827
+ }
67828
+ async function postVariables(principal, connection) {
67829
+ const store = userVariableStoreFor(options.dataDir, principal);
67830
+ const user = store.read();
67831
+ const admin = sharedCache.variables;
67832
+ connection.transport.post({
67833
+ type: "variables",
67834
+ user: [...user],
67835
+ admin: [...admin],
67836
+ resolved: resolveSessionVariables(admin, user),
67837
+ adminIds: sharedCache.adminIds,
67838
+ canEditAdmin: isAdminSession(principal)
67839
+ });
67840
+ }
67841
+ async function handleVariableMessage(principal, type, body, connection) {
67842
+ const payload = body;
67843
+ if (type === "requestReviews") {
67844
+ await postReviews(principal, connection);
67845
+ return true;
67846
+ }
67847
+ if (type === "decideReview") {
67848
+ const id = typeof body.id === "string" ? body.id : "";
67849
+ const approved = body.approved === true;
67850
+ const reason = typeof body.reason === "string" ? body.reason : void 0;
67851
+ const decided = await reviews.decide(id, {
67852
+ approved,
67853
+ by: principal.displayName,
67854
+ ...reason !== void 0 ? { reason } : {}
67855
+ });
67856
+ if (decided === void 0) {
67857
+ connection.transport.post({
67858
+ type: "error",
67859
+ message: "That submission has already been decided. Reload to see the current queue."
67860
+ });
67861
+ return true;
67862
+ }
67863
+ if (approved) {
67864
+ const failure = await applyApproval(decided);
67865
+ if (failure !== void 0) {
67866
+ connection.transport.post({ type: "error", message: `Approved, but could not write it: ${failure}` });
67867
+ }
67868
+ }
67869
+ log(`${principal.displayName} ${approved ? "approved" : "rejected"} ${decided.kind} "${decided.name}"`);
67870
+ await broadcastReviews();
67871
+ return true;
67872
+ }
67873
+ if (type === "requestVariables") {
67874
+ await postVariables(principal, connection);
67875
+ return true;
67876
+ }
67877
+ if (type === "saveUserVariables") {
67878
+ const parsed = sessionVariablesSchema.safeParse(payload.variables);
67879
+ if (!parsed.success) {
67880
+ connection.transport.post({ type: "error", message: `Could not save variables: ${parsed.error.message}` });
67881
+ return true;
67882
+ }
67883
+ await userVariableStoreFor(options.dataDir, principal).save(parsed.data);
67884
+ await postVariables(principal, connection);
67885
+ return true;
67886
+ }
67887
+ if (type === "saveAdminVariables" || type === "saveAdminIds") {
67888
+ if (sharedStore === void 0) {
67889
+ connection.transport.post({
67890
+ type: "error",
67891
+ message: "There are no shared settings outside --server mode."
67892
+ });
67893
+ return true;
67894
+ }
67895
+ if (type === "saveAdminVariables") {
67896
+ const parsed = sessionVariablesSchema.safeParse(payload.variables);
67897
+ if (!parsed.success) {
67898
+ connection.transport.post({ type: "error", message: `Could not save variables: ${parsed.error.message}` });
67899
+ return true;
67900
+ }
67901
+ sharedCache = await sharedStore.save({ variables: parsed.data });
67902
+ } else {
67903
+ const ids = Array.isArray(payload.ids) ? payload.ids.filter((id) => typeof id === "string") : [];
67904
+ if (!ids.includes(principal.id)) {
67905
+ log(`${principal.displayName} removed themselves from the administrator list`);
67906
+ }
67907
+ sharedCache = await sharedStore.save({ adminIds: [...new Set(ids)] });
67908
+ }
67909
+ for (const [id, other] of connections) {
67910
+ await postVariables({ id, displayName: id }, other);
67911
+ }
67912
+ return true;
67913
+ }
67914
+ return false;
67915
+ }
66617
67916
  async function serveAsset(pathname, response) {
66618
67917
  const asset = CLIENT_ASSETS[pathname];
66619
67918
  if (asset === void 0) {
@@ -66621,9 +67920,9 @@ async function startServer(options) {
66621
67920
  return;
66622
67921
  }
66623
67922
  try {
66624
- const body = await fs19.readFile(path23.join(options.clientDir, asset));
67923
+ const body = await fs21.readFile(path25.join(options.clientDir, asset));
66625
67924
  response.writeHead(200, {
66626
- "Content-Type": CONTENT_TYPES[path23.extname(asset)] ?? "application/octet-stream",
67925
+ "Content-Type": CONTENT_TYPES[path25.extname(asset)] ?? "application/octet-stream",
66627
67926
  ...securityHeaders()
66628
67927
  });
66629
67928
  response.end(body);
@@ -66651,6 +67950,7 @@ function respondJson(response, status, body) {
66651
67950
  response.end(JSON.stringify(body));
66652
67951
  }
66653
67952
  export {
67953
+ CLIENT_ASSETS,
66654
67954
  startServer
66655
67955
  };
66656
67956
  /*! Bundled license information: