@ariobarin/glossa 0.1.1 → 0.2.0-beta.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 (2) hide show
  1. package/dist/app.js +905 -319
  2. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -39921,6 +39921,9 @@ var MAX_TEXT_BYTES = 1024 * 1024;
39921
39921
  var MAX_EDIT_DIFF_BYTES = 128 * 1024;
39922
39922
  var MAX_EDIT_OPERATIONS = 100;
39923
39923
  var MAX_COMMAND_OUTPUT_BYTES = 12 * 1024;
39924
+ var MAX_COMMAND_RETAINED_STREAM_BYTES = 1024 * 1024;
39925
+ var DEFAULT_COMMAND_OUTPUT_RANGE_BYTES = 32 * 1024;
39926
+ var MAX_COMMAND_OUTPUT_RANGE_BYTES = 64 * 1024;
39924
39927
  var DEFAULT_COMMAND_TIMEOUT_MS = 15 * 60 * 1e3;
39925
39928
  var MAX_COMMAND_TIMEOUT_MS = 60 * 60 * 1e3;
39926
39929
  var DEFAULT_COMMAND_FAST_WAIT_MS = 750;
@@ -39976,12 +39979,16 @@ var listFilesJobSchema = listFilesRequestSchema.extend({
39976
39979
  requestId: external_exports.string().uuid(),
39977
39980
  timeoutMs: structuredReadTimeoutSchema
39978
39981
  });
39982
+ var searchGlobSchema = external_exports.string().min(1).max(256).refine((value) => !/[\r\n\u0000]/.test(value), "Search glob must fit on one line").describe("Root-relative glob pattern using forward slashes, for example src/**/*.ts.");
39979
39983
  var searchTextRequestSchema = external_exports.object({
39980
- query: external_exports.string().min(1).max(256).refine((value) => !/[\r\n\u0000]/.test(value), "Search text must fit on one line").describe("Literal single-line UTF-8 text to search for."),
39984
+ query: external_exports.string().min(1).max(256).refine((value) => !/[\r\n\u0000]/.test(value), "Search text must fit on one line").describe("Single-line UTF-8 search expression. Interpreted literally by default or as a JavaScript regular expression when matchMode is regex."),
39981
39985
  path: relativePathSchema.optional().describe("File or directory relative to the exposed root. Defaults to the root."),
39986
+ matchMode: external_exports.enum(["literal", "regex"]).optional().describe("How to interpret query. Defaults to literal; regex uses JavaScript regular-expression syntax."),
39982
39987
  caseSensitive: external_exports.boolean().optional().describe("Whether matching is case-sensitive. Defaults to false."),
39983
39988
  maxResults: external_exports.number().int().min(1).max(MAX_SEARCH_TEXT_RESULTS).optional().describe("Maximum matching lines to return, from 1 through 100. Defaults to 50."),
39984
- extensions: external_exports.array(external_exports.string().regex(/^\.[A-Za-z0-9][A-Za-z0-9._-]{0,19}$/).describe("Filename suffix including the leading dot, such as .ts or .d.ts.")).min(1).max(20).optional().describe("Optional filename extensions to search.")
39989
+ extensions: external_exports.array(external_exports.string().regex(/^\.[A-Za-z0-9][A-Za-z0-9._-]{0,19}$/).describe("Filename suffix including the leading dot, such as .ts or .d.ts.")).min(1).max(20).optional().describe("Optional filename extensions to search."),
39990
+ includeGlobs: external_exports.array(searchGlobSchema).min(1).max(20).optional().describe("Optional root-relative glob patterns. A file must match at least one include pattern before its contents are scanned."),
39991
+ excludeGlobs: external_exports.array(searchGlobSchema).min(1).max(20).optional().describe("Optional root-relative glob patterns. Matching files are skipped before their contents are scanned.")
39985
39992
  }).strict();
39986
39993
  var searchTextJobSchema = searchTextRequestSchema.extend({
39987
39994
  type: external_exports.literal("search_text"),
@@ -40000,8 +40007,8 @@ var readFileRangeJobSchema = readFileRangeRequestSchema.extend({
40000
40007
  });
40001
40008
  var writeFileRequestSchema = external_exports.object({
40002
40009
  path: relativePathSchema,
40003
- content: boundedTextSchema.describe("Complete UTF-8 text content that will replace the file."),
40004
- expectedSha256: external_exports.string().regex(/^[a-f0-9]{64}$/).optional().describe("Full-file SHA-256 returned by read_file or read_file_range. When provided, the write fails if the file changed.")
40010
+ content: boundedTextSchema.describe("Complete UTF-8 text content for the new file or replacement revision."),
40011
+ expectedSha256: external_exports.string().regex(/^[a-f0-9]{64}$/).optional().describe("Full-file SHA-256 returned by read_file or read_file_range. Omit only when creating a new path; when provided, write_file replaces exactly that existing revision and fails if it is missing or changed.")
40005
40012
  }).strict();
40006
40013
  var writeFileJobSchema = writeFileRequestSchema.extend({
40007
40014
  type: external_exports.literal("write_file"),
@@ -40029,6 +40036,30 @@ var editFileJobSchema = editFileRequestSchema.safeExtend({
40029
40036
  type: external_exports.literal("edit_file"),
40030
40037
  requestId: external_exports.string().uuid()
40031
40038
  });
40039
+ var makeDirectoryRequestSchema = external_exports.object({
40040
+ path: relativePathSchema,
40041
+ recursive: external_exports.boolean().optional().describe("Whether to create missing parent directories. Defaults to false.")
40042
+ }).strict();
40043
+ var makeDirectoryJobSchema = makeDirectoryRequestSchema.extend({
40044
+ type: external_exports.literal("make_directory"),
40045
+ requestId: external_exports.string().uuid()
40046
+ });
40047
+ var deletePathRequestSchema = external_exports.object({
40048
+ path: relativePathSchema,
40049
+ recursive: external_exports.boolean().optional().describe("Whether to delete a non-empty directory tree. Defaults to false.")
40050
+ }).strict();
40051
+ var deletePathJobSchema = deletePathRequestSchema.extend({
40052
+ type: external_exports.literal("delete_path"),
40053
+ requestId: external_exports.string().uuid()
40054
+ });
40055
+ var movePathRequestSchema = external_exports.object({
40056
+ source: relativePathSchema.describe("Existing file or directory to move."),
40057
+ destination: relativePathSchema.describe("New path inside the exposed root. The destination must not already exist.")
40058
+ }).strict();
40059
+ var movePathJobSchema = movePathRequestSchema.extend({
40060
+ type: external_exports.literal("move_path"),
40061
+ requestId: external_exports.string().uuid()
40062
+ });
40032
40063
  function requireOneCommand(value, context) {
40033
40064
  if ((value.argv ? 1 : 0) + (value.shellCommand ? 1 : 0) !== 1) {
40034
40065
  context.addIssue({
@@ -40058,6 +40089,16 @@ var getCommandJobSchema = getCommandRequestSchema.extend({
40058
40089
  type: external_exports.literal("get_command"),
40059
40090
  requestId: external_exports.string().uuid()
40060
40091
  });
40092
+ var readCommandOutputRequestSchema = external_exports.object({
40093
+ commandId: external_exports.string().uuid().describe("Command identifier returned by run_command."),
40094
+ stream: external_exports.enum(["stdout", "stderr"]).describe("Command output stream to read independently."),
40095
+ offset: external_exports.number().int().min(0).optional().describe("Zero-based retained byte offset. Defaults to 0."),
40096
+ maxBytes: external_exports.number().int().min(4).max(MAX_COMMAND_OUTPUT_RANGE_BYTES).optional().describe("Maximum retained source bytes to inspect, from 4 through 65536. Defaults to 32768.")
40097
+ }).strict();
40098
+ var readCommandOutputJobSchema = readCommandOutputRequestSchema.extend({
40099
+ type: external_exports.literal("read_command_output"),
40100
+ requestId: external_exports.string().uuid()
40101
+ });
40061
40102
  var cancelCommandRequestSchema = external_exports.object({
40062
40103
  commandId: external_exports.string().uuid().describe("Command identifier returned by run_command.")
40063
40104
  }).strict();
@@ -40072,8 +40113,12 @@ var workerJobSchema = external_exports.discriminatedUnion("type", [
40072
40113
  readFileRangeJobSchema,
40073
40114
  writeFileJobSchema,
40074
40115
  editFileJobSchema,
40116
+ makeDirectoryJobSchema,
40117
+ deletePathJobSchema,
40118
+ movePathJobSchema,
40075
40119
  runCommandJobSchema,
40076
40120
  getCommandJobSchema,
40121
+ readCommandOutputJobSchema,
40077
40122
  cancelCommandJobSchema
40078
40123
  ]);
40079
40124
  var workerResultSchema = external_exports.object({
@@ -49820,6 +49865,8 @@ var WorkerError = class extends Error {
49820
49865
  var STREAM_HEAD_BYTES = Math.floor(MAX_COMMAND_OUTPUT_BYTES / 3);
49821
49866
  var STREAM_TAIL_BYTES = MAX_COMMAND_OUTPUT_BYTES - STREAM_HEAD_BYTES;
49822
49867
  var RESTRICTED_SCAN_TAIL_BYTES = 1024;
49868
+ var COMMAND_RECORD_RETENTION_MS = 5 * 60 * 1e3;
49869
+ var MAX_RETAINED_COMMAND_RECORDS = 8;
49823
49870
  function restrictedDataError() {
49824
49871
  return new WorkerError(
49825
49872
  RESTRICTED_DATA_ERROR_CODE,
@@ -49836,20 +49883,26 @@ function scanOutputChunk(previousTail, chunk) {
49836
49883
  tail
49837
49884
  };
49838
49885
  }
49886
+ function markRestrictedData(record2) {
49887
+ if (record2.restrictedDataDetected) return;
49888
+ record2.restrictedDataDetected = true;
49889
+ record2.stdout = emptyCapture();
49890
+ record2.stderr = emptyCapture();
49891
+ record2.stdoutScanTail = Buffer.alloc(0);
49892
+ record2.stderrScanTail = Buffer.alloc(0);
49893
+ if (record2.status === "running") {
49894
+ record2.requestedTerminal = "canceled";
49895
+ void terminateProcessTree(record2.child).catch(() => void 0);
49896
+ }
49897
+ markChanged(record2);
49898
+ }
49839
49899
  function recordCommandOutput(record2, streamName, chunk) {
49840
49900
  if (record2.restrictedDataDetected || chunk.byteLength === 0) return;
49841
49901
  const tailName = streamName === "stdout" ? "stdoutScanTail" : "stderrScanTail";
49842
49902
  const scan = scanOutputChunk(record2[tailName], chunk);
49843
49903
  record2[tailName] = scan.tail;
49844
49904
  if (scan.detected) {
49845
- record2.restrictedDataDetected = true;
49846
- record2.stdout = emptyCapture();
49847
- record2.stderr = emptyCapture();
49848
- if (record2.status === "running") {
49849
- record2.requestedTerminal = "canceled";
49850
- void terminateProcessTree(record2.child).catch(() => void 0);
49851
- }
49852
- markChanged(record2);
49905
+ markRestrictedData(record2);
49853
49906
  return;
49854
49907
  }
49855
49908
  if (capture(record2, record2[streamName], chunk)) markChanged(record2);
@@ -49864,6 +49917,17 @@ function appendTail(existing, chunk) {
49864
49917
  function capture(_record2, stream, chunk) {
49865
49918
  if (chunk.byteLength === 0) return false;
49866
49919
  stream.totalBytes += chunk.byteLength;
49920
+ const retentionBudget = MAX_COMMAND_RETAINED_STREAM_BYTES - stream.retainedBytes;
49921
+ if (retentionBudget > 0) {
49922
+ const retained = chunk.subarray(0, Math.min(chunk.byteLength, retentionBudget));
49923
+ if (retained.byteLength > 0) {
49924
+ stream.retained.push(Buffer.from(retained));
49925
+ stream.retainedBytes += retained.byteLength;
49926
+ }
49927
+ }
49928
+ if (chunk.byteLength > Math.max(0, retentionBudget)) {
49929
+ stream.retentionTruncated = true;
49930
+ }
49867
49931
  let offset = 0;
49868
49932
  if (stream.headBytes < STREAM_HEAD_BYTES) {
49869
49933
  const accepted = chunk.subarray(
@@ -49912,7 +49976,10 @@ function emptyCapture() {
49912
49976
  head: [],
49913
49977
  headBytes: 0,
49914
49978
  tail: Buffer.alloc(0),
49915
- totalBytes: 0
49979
+ retained: [],
49980
+ retainedBytes: 0,
49981
+ totalBytes: 0,
49982
+ retentionTruncated: false
49916
49983
  };
49917
49984
  }
49918
49985
  function retainedBytes(stream, complete) {
@@ -49954,6 +50021,44 @@ function utf8SuffixWithinBudget(value, budget) {
49954
50021
  }
49955
50022
  return characters.slice(start).join("");
49956
50023
  }
50024
+ function isUtf8Continuation(byte) {
50025
+ return byte !== void 0 && (byte & 192) === 128;
50026
+ }
50027
+ function utf8SequenceBytes(byte) {
50028
+ if ((byte & 128) === 0) return 1;
50029
+ if ((byte & 224) === 192) return 2;
50030
+ if ((byte & 240) === 224) return 3;
50031
+ if ((byte & 248) === 240) return 4;
50032
+ return 1;
50033
+ }
50034
+ function retainedRange(stream, requestedOffset, maxBytes) {
50035
+ const retained = Buffer.concat(stream.retained, stream.retainedBytes);
50036
+ let offset = requestedOffset;
50037
+ while (offset < retained.byteLength && isUtf8Continuation(retained[offset])) {
50038
+ offset += 1;
50039
+ }
50040
+ if (offset >= retained.byteLength) {
50041
+ return { offset, content: "" };
50042
+ }
50043
+ let end = Math.min(retained.byteLength, offset + maxBytes);
50044
+ if (end < retained.byteLength) {
50045
+ while (end > offset && isUtf8Continuation(retained[end])) end -= 1;
50046
+ }
50047
+ if (end > offset) {
50048
+ let lead = end - 1;
50049
+ while (lead > offset && isUtf8Continuation(retained[lead])) lead -= 1;
50050
+ const expected = utf8SequenceBytes(retained[lead]);
50051
+ if (expected > 1 && end - lead < expected) end = lead;
50052
+ }
50053
+ if (end <= offset) {
50054
+ end = Math.min(retained.byteLength, offset + maxBytes);
50055
+ }
50056
+ return {
50057
+ offset,
50058
+ content: retained.subarray(offset, end).toString("utf8"),
50059
+ ...end < retained.byteLength ? { nextOffset: end } : {}
50060
+ };
50061
+ }
49957
50062
  function renderStream(stream, budget, complete) {
49958
50063
  if (budget <= 0 || stream.totalBytes === 0) {
49959
50064
  return { content: "", truncated: stream.totalBytes > 0 };
@@ -50046,6 +50151,21 @@ var CommandService = class {
50046
50151
  policy;
50047
50152
  #commands = /* @__PURE__ */ new Map();
50048
50153
  #activeCommandId = null;
50154
+ #pruneRetainedCommands() {
50155
+ while (this.#commands.size >= MAX_RETAINED_COMMAND_RECORDS) {
50156
+ const oldestTerminal = [...this.#commands].find(
50157
+ ([, record2]) => record2.status !== "running"
50158
+ );
50159
+ if (!oldestTerminal) return;
50160
+ this.#commands.delete(oldestTerminal[0]);
50161
+ }
50162
+ }
50163
+ #scheduleDeletion(commandId) {
50164
+ setTimeout(
50165
+ () => this.#commands.delete(commandId),
50166
+ COMMAND_RECORD_RETENTION_MS
50167
+ ).unref();
50168
+ }
50049
50169
  async start(options) {
50050
50170
  if (this.#activeCommandId) {
50051
50171
  const active = this.#commands.get(this.#activeCommandId);
@@ -50130,6 +50250,7 @@ var CommandService = class {
50130
50250
  void terminateProcessTree(child);
50131
50251
  }, timeoutMs);
50132
50252
  record2.timeout.unref();
50253
+ this.#pruneRetainedCommands();
50133
50254
  this.#commands.set(id, record2);
50134
50255
  this.#activeCommandId = id;
50135
50256
  child.stdout.on("data", (chunk) => {
@@ -50147,6 +50268,7 @@ var CommandService = class {
50147
50268
  this.#activeCommandId = null;
50148
50269
  markChanged(record2);
50149
50270
  record2.complete();
50271
+ this.#scheduleDeletion(id);
50150
50272
  });
50151
50273
  child.once("close", (exitCode, signal) => {
50152
50274
  if (record2.status !== "running") return;
@@ -50158,7 +50280,7 @@ var CommandService = class {
50158
50280
  this.#activeCommandId = null;
50159
50281
  markChanged(record2);
50160
50282
  record2.complete();
50161
- setTimeout(() => this.#commands.delete(id), 5 * 60 * 1e3).unref();
50283
+ this.#scheduleDeletion(id);
50162
50284
  });
50163
50285
  if (options.stdin !== void 0) child.stdin.end(options.stdin);
50164
50286
  else child.stdin.end();
@@ -50215,6 +50337,53 @@ var CommandService = class {
50215
50337
  }
50216
50338
  return this.snapshot(record2);
50217
50339
  }
50340
+ async readOutput(commandId, streamName, offset = 0, maxBytes = DEFAULT_COMMAND_OUTPUT_RANGE_BYTES) {
50341
+ const record2 = this.#commands.get(commandId);
50342
+ if (!record2) throw new WorkerError("command_not_found", "The command was not found.");
50343
+ if (streamName !== "stdout" && streamName !== "stderr") {
50344
+ throw new WorkerError(
50345
+ "invalid_output_stream",
50346
+ "Command output stream must be stdout or stderr."
50347
+ );
50348
+ }
50349
+ if (!Number.isInteger(offset) || offset < 0) {
50350
+ throw new WorkerError(
50351
+ "invalid_output_offset",
50352
+ "Command output offset must be a non-negative integer."
50353
+ );
50354
+ }
50355
+ if (!Number.isInteger(maxBytes) || maxBytes < 4 || maxBytes > MAX_COMMAND_OUTPUT_RANGE_BYTES) {
50356
+ throw new WorkerError(
50357
+ "invalid_output_range",
50358
+ "Command output range must be between 4 and 65536 source bytes."
50359
+ );
50360
+ }
50361
+ if (record2.restrictedDataDetected) throw restrictedDataError();
50362
+ const stream = record2[streamName];
50363
+ if (offset > stream.retainedBytes) {
50364
+ throw new WorkerError(
50365
+ "output_offset_out_of_range",
50366
+ "The command output offset exceeds the retained stream length."
50367
+ );
50368
+ }
50369
+ const range = retainedRange(stream, offset, maxBytes);
50370
+ if (containsRestrictedAuthenticationData(range.content)) {
50371
+ markRestrictedData(record2);
50372
+ throw restrictedDataError();
50373
+ }
50374
+ return {
50375
+ commandId,
50376
+ stream: streamName,
50377
+ status: record2.status,
50378
+ offset: range.offset,
50379
+ content: range.content,
50380
+ ...range.nextOffset === void 0 ? {} : { nextOffset: range.nextOffset },
50381
+ retainedBytes: stream.retainedBytes,
50382
+ totalBytes: stream.totalBytes,
50383
+ retentionTruncated: stream.retentionTruncated,
50384
+ complete: record2.status !== "running"
50385
+ };
50386
+ }
50218
50387
  async cancel(commandId) {
50219
50388
  const record2 = this.#commands.get(commandId);
50220
50389
  if (!record2) throw new WorkerError("command_not_found", "The command was not found.");
@@ -50262,124 +50431,414 @@ var CommandService = class {
50262
50431
  import { createHash, randomUUID as randomUUID2 } from "node:crypto";
50263
50432
  import {
50264
50433
  chmod as chmod2,
50265
- lstat,
50434
+ link as link2,
50435
+ lstat as lstat2,
50436
+ mkdir as mkdir3,
50266
50437
  opendir,
50267
50438
  open,
50268
50439
  readFile as readFile3,
50269
50440
  rename,
50270
50441
  rm as rm2,
50271
- stat,
50442
+ rmdir,
50443
+ stat as stat2,
50272
50444
  writeFile as writeFile3
50273
50445
  } from "node:fs/promises";
50274
- import path5 from "node:path";
50446
+ import path6 from "node:path";
50275
50447
  import { performance as performance2 } from "node:perf_hooks";
50276
50448
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
50277
- function sha256(content) {
50278
- return createHash("sha256").update(content).digest("hex");
50449
+
50450
+ // src/worker/path-policy.ts
50451
+ import { lstat, realpath, stat } from "node:fs/promises";
50452
+ import os5 from "node:os";
50453
+ import path5 from "node:path";
50454
+ function samePath(left, right) {
50455
+ return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
50279
50456
  }
50280
- var fileWriteTails = /* @__PURE__ */ new Map();
50281
- async function withFileWriteLock(target, operation) {
50282
- const normalized = path5.normalize(target);
50283
- const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;
50284
- const predecessor = fileWriteTails.get(key);
50285
- let release;
50286
- const tail = new Promise((resolve) => {
50287
- release = resolve;
50288
- });
50289
- fileWriteTails.set(key, tail);
50290
- if (predecessor) await predecessor;
50457
+ function accountHomeDirectory() {
50291
50458
  try {
50292
- return await operation();
50293
- } finally {
50294
- release();
50295
- if (fileWriteTails.get(key) === tail) fileWriteTails.delete(key);
50459
+ return os5.userInfo().homedir;
50460
+ } catch {
50461
+ return os5.homedir();
50296
50462
  }
50297
50463
  }
50298
- async function requireRevision(target, expectedSha256) {
50299
- let actual = null;
50300
- try {
50301
- actual = sha256(await readFile3(target));
50302
- } catch (error46) {
50303
- if (error46.code !== "ENOENT") throw error46;
50464
+ function isWithin(root, candidate) {
50465
+ const relative = path5.relative(root, candidate);
50466
+ return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== ".." && !path5.isAbsolute(relative);
50467
+ }
50468
+ function validateRelativePath(value) {
50469
+ if (value.includes("\0")) {
50470
+ throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
50304
50471
  }
50305
- if (actual !== expectedSha256) {
50306
- throw new WorkerError("stale_revision", "The file revision has changed.");
50472
+ const explicitNativePosixPath = process.platform !== "win32" && value.startsWith("./");
50473
+ if (path5.isAbsolute(value) || path5.posix.isAbsolute(value) || !explicitNativePosixPath && path5.win32.isAbsolute(value)) {
50474
+ throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
50307
50475
  }
50476
+ const segments = explicitNativePosixPath ? value.split(/\/+/).filter(Boolean) : value.split(/[\\/]+/);
50477
+ if (segments.includes("..")) {
50478
+ throw new WorkerError("path_traversal", "Parent path traversal is not allowed.");
50479
+ }
50480
+ return value === "" ? "." : value;
50308
50481
  }
50309
- var DEFAULT_LIST_FILES_LIMIT = 100;
50310
- var DEFAULT_SEARCH_TEXT_RESULTS = 50;
50311
- var DEFAULT_READ_RANGE_LINES = 200;
50312
- var MAX_REPOSITORY_SCAN_ENTRIES = 2e4;
50313
- var MAX_SEARCH_FILES = 5e3;
50314
- var MAX_SEARCH_BYTES = 32 * 1024 * 1024;
50315
- var SKIPPED_RECURSIVE_DIRECTORIES = /* @__PURE__ */ new Set([
50316
- ".git",
50317
- ".hg",
50318
- ".svn",
50319
- "node_modules"
50320
- ]);
50321
- function compareNames(left, right) {
50322
- return left < right ? -1 : left > right ? 1 : 0;
50323
- }
50324
- function relativeSortKey(root, target) {
50325
- const relative = path5.relative(root, target);
50326
- if (!relative) return ".";
50327
- return process.platform === "win32" ? relative.replaceAll("\\", "/") : relative;
50328
- }
50329
- function displayPath(root, target) {
50330
- const relative = relativeSortKey(root, target);
50331
- if (process.platform === "win32") return relative;
50332
- return relative.includes("\\") ? "./" + relative : relative;
50333
- }
50334
- function isUnavailableFileError(error46) {
50335
- const code = error46?.code;
50336
- return code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "ENOTDIR";
50337
- }
50338
- function isUnavailableDiscoveredPathError(error46) {
50339
- return isUnavailableFileError(error46) || error46 instanceof WorkerError && error46.code === "path_not_found";
50340
- }
50341
- function isLinkedPathError(error46) {
50342
- return error46 instanceof WorkerError && error46.code === "linked_path";
50482
+ async function canonicalizeRoot(candidate) {
50483
+ const root = await realpath(path5.resolve(candidate)).catch((error46) => {
50484
+ if (error46.code === "ENOENT") {
50485
+ throw new WorkerError("root_not_found", "The workspace directory does not exist.");
50486
+ }
50487
+ throw error46;
50488
+ });
50489
+ const rootStat = await stat(root);
50490
+ if (!rootStat.isDirectory()) {
50491
+ throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
50492
+ }
50493
+ const filesystemRoot = path5.parse(root).root;
50494
+ const homes = await Promise.all(
50495
+ [os5.homedir(), accountHomeDirectory()].map(
50496
+ async (home) => await realpath(home).catch(() => path5.resolve(home))
50497
+ )
50498
+ );
50499
+ const isHomeDirectory = homes.some((home) => samePath(root, home));
50500
+ if (samePath(root, filesystemRoot) || isHomeDirectory) {
50501
+ const kind = isHomeDirectory ? "your home directory" : "a filesystem root";
50502
+ throw new WorkerError(
50503
+ "broad_root_refused",
50504
+ `The selected root is ${kind}, which Glossa will not expose. Choose a project directory instead.`
50505
+ );
50506
+ }
50507
+ return root;
50343
50508
  }
50344
- async function readBoundedFile(target, maximumBytes, expectedBytes) {
50345
- const handle = await open(target, "r");
50346
- try {
50347
- const openedStat = await handle.stat();
50348
- if (!openedStat.isFile()) {
50349
- throw new WorkerError("not_file", "The requested path is not a file.");
50509
+ var PathPolicy = class _PathPolicy {
50510
+ constructor(root) {
50511
+ this.root = root;
50512
+ }
50513
+ root;
50514
+ static async create(candidate) {
50515
+ return new _PathPolicy(await canonicalizeRoot(candidate));
50516
+ }
50517
+ async resolveExisting(relativePath) {
50518
+ const lexical = this.resolveLexical(relativePath);
50519
+ await this.rejectLinkedComponents(lexical);
50520
+ const canonical = await realpath(lexical).catch((error46) => {
50521
+ if (error46.code === "ENOENT") {
50522
+ throw new WorkerError("path_not_found", "The requested path does not exist.");
50523
+ }
50524
+ throw error46;
50525
+ });
50526
+ if (!isWithin(this.root, canonical)) {
50527
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50350
50528
  }
50351
- if (openedStat.size !== expectedBytes) {
50352
- throw new WorkerError("file_changed", "The file changed while it was being read.");
50529
+ return canonical;
50530
+ }
50531
+ async resolveDiscoveredExisting(candidate) {
50532
+ const lexical = path5.resolve(candidate);
50533
+ if (!isWithin(this.root, lexical)) {
50534
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50353
50535
  }
50354
- if (openedStat.size > maximumBytes) {
50355
- throw new WorkerError("search_byte_limit", "The search byte limit was reached.");
50536
+ await this.rejectLinkedComponents(lexical);
50537
+ const canonical = await realpath(lexical).catch((error46) => {
50538
+ if (error46.code === "ENOENT") {
50539
+ throw new WorkerError("path_not_found", "The requested path does not exist.");
50540
+ }
50541
+ throw error46;
50542
+ });
50543
+ if (!isWithin(this.root, canonical)) {
50544
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50356
50545
  }
50357
- const buffer = Buffer.allocUnsafe(openedStat.size);
50358
- let offset = 0;
50359
- while (offset < buffer.byteLength) {
50360
- const { bytesRead } = await handle.read(
50361
- buffer,
50362
- offset,
50363
- buffer.byteLength - offset,
50364
- offset
50365
- );
50366
- if (bytesRead === 0) break;
50367
- offset += bytesRead;
50546
+ return canonical;
50547
+ }
50548
+ async resolveWritableFile(relativePath) {
50549
+ const lexical = this.resolveLexical(relativePath);
50550
+ const parent = path5.dirname(lexical);
50551
+ await this.rejectLinkedComponents(parent);
50552
+ const canonicalParent = await realpath(parent).catch((error46) => {
50553
+ if (error46.code === "ENOENT") {
50554
+ throw new WorkerError("parent_not_found", "The destination directory does not exist.");
50555
+ }
50556
+ throw error46;
50557
+ });
50558
+ if (!isWithin(this.root, canonicalParent)) {
50559
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50368
50560
  }
50369
- const completedStat = await handle.stat();
50370
- if (completedStat.size !== openedStat.size || offset !== openedStat.size) {
50371
- throw new WorkerError("file_changed", "The file changed while it was being read.");
50561
+ if (!(await stat(canonicalParent)).isDirectory()) {
50562
+ throw new WorkerError("not_directory", "The destination parent is not a directory.");
50372
50563
  }
50373
- return Buffer.from(buffer);
50374
- } finally {
50375
- await handle.close();
50564
+ try {
50565
+ const targetStat = await lstat(lexical);
50566
+ if (targetStat.isSymbolicLink()) {
50567
+ throw new WorkerError("linked_path", "Writes through links are not allowed.");
50568
+ }
50569
+ if (targetStat.isDirectory()) {
50570
+ throw new WorkerError("not_file", "The destination is a directory.");
50571
+ }
50572
+ const canonicalTarget = await realpath(lexical);
50573
+ if (!isWithin(this.root, canonicalTarget)) {
50574
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50575
+ }
50576
+ return canonicalTarget;
50577
+ } catch (error46) {
50578
+ if (error46 instanceof WorkerError) throw error46;
50579
+ if (error46.code !== "ENOENT") throw error46;
50580
+ }
50581
+ return path5.join(canonicalParent, path5.basename(lexical));
50376
50582
  }
50377
- }
50378
- function normalizedLines(content) {
50379
- if (content.length === 0) return [];
50380
- const normalized = content.replace(/\r\n?/g, "\n");
50381
- const lines = normalized.split("\n");
50382
- if (normalized.endsWith("\n")) lines.pop();
50583
+ async resolveWritableDirectory(relativePath, recursive) {
50584
+ const lexical = this.resolveLexical(relativePath);
50585
+ await this.rejectLinkedComponents(lexical);
50586
+ try {
50587
+ const targetStat = await lstat(lexical);
50588
+ if (targetStat.isSymbolicLink()) {
50589
+ throw new WorkerError("linked_path", "Directory creation through links is not allowed.");
50590
+ }
50591
+ if (!targetStat.isDirectory()) {
50592
+ throw new WorkerError("not_directory", "The destination is not a directory.");
50593
+ }
50594
+ const canonicalTarget = await realpath(lexical);
50595
+ if (!isWithin(this.root, canonicalTarget)) {
50596
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50597
+ }
50598
+ return { target: canonicalTarget, exists: true };
50599
+ } catch (error46) {
50600
+ if (error46 instanceof WorkerError) throw error46;
50601
+ if (error46.code !== "ENOENT") throw error46;
50602
+ }
50603
+ if (!recursive) {
50604
+ const parent = path5.dirname(lexical);
50605
+ await this.rejectLinkedComponents(parent);
50606
+ const canonicalParent = await realpath(parent).catch(
50607
+ (error46) => {
50608
+ if (error46.code === "ENOENT") {
50609
+ throw new WorkerError(
50610
+ "parent_not_found",
50611
+ "The destination directory does not exist."
50612
+ );
50613
+ }
50614
+ throw error46;
50615
+ }
50616
+ );
50617
+ if (!isWithin(this.root, canonicalParent)) {
50618
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50619
+ }
50620
+ if (!(await stat(canonicalParent)).isDirectory()) {
50621
+ throw new WorkerError("not_directory", "The destination parent is not a directory.");
50622
+ }
50623
+ return {
50624
+ target: path5.join(canonicalParent, path5.basename(lexical)),
50625
+ exists: false
50626
+ };
50627
+ }
50628
+ let existingAncestor = path5.dirname(lexical);
50629
+ while (!samePath(existingAncestor, this.root)) {
50630
+ try {
50631
+ await this.rejectLinkedComponents(existingAncestor);
50632
+ const canonicalAncestor = await realpath(existingAncestor);
50633
+ if (!isWithin(this.root, canonicalAncestor)) {
50634
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50635
+ }
50636
+ if (!(await stat(canonicalAncestor)).isDirectory()) {
50637
+ throw new WorkerError("not_directory", "The destination parent is not a directory.");
50638
+ }
50639
+ return {
50640
+ target: path5.join(
50641
+ canonicalAncestor,
50642
+ path5.relative(existingAncestor, lexical)
50643
+ ),
50644
+ exists: false
50645
+ };
50646
+ } catch (error46) {
50647
+ if (error46 instanceof WorkerError) throw error46;
50648
+ if (error46.code !== "ENOENT") throw error46;
50649
+ }
50650
+ existingAncestor = path5.dirname(existingAncestor);
50651
+ }
50652
+ return { target: lexical, exists: false };
50653
+ }
50654
+ async resolveVacantPath(relativePath) {
50655
+ const lexical = this.resolveLexical(relativePath);
50656
+ const parent = path5.dirname(lexical);
50657
+ await this.rejectLinkedComponents(parent);
50658
+ const canonicalParent = await realpath(parent).catch(
50659
+ (error46) => {
50660
+ if (error46.code === "ENOENT") {
50661
+ throw new WorkerError(
50662
+ "parent_not_found",
50663
+ "The destination directory does not exist."
50664
+ );
50665
+ }
50666
+ throw error46;
50667
+ }
50668
+ );
50669
+ if (!isWithin(this.root, canonicalParent)) {
50670
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50671
+ }
50672
+ if (!(await stat(canonicalParent)).isDirectory()) {
50673
+ throw new WorkerError("not_directory", "The destination parent is not a directory.");
50674
+ }
50675
+ try {
50676
+ await lstat(lexical);
50677
+ throw new WorkerError(
50678
+ "destination_exists",
50679
+ "The destination already exists."
50680
+ );
50681
+ } catch (error46) {
50682
+ if (error46 instanceof WorkerError) throw error46;
50683
+ if (error46.code !== "ENOENT") throw error46;
50684
+ }
50685
+ return path5.join(canonicalParent, path5.basename(lexical));
50686
+ }
50687
+ resolveLexical(relativePath) {
50688
+ const validated = validateRelativePath(relativePath);
50689
+ const candidate = path5.resolve(this.root, validated);
50690
+ if (!isWithin(this.root, candidate)) {
50691
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50692
+ }
50693
+ return candidate;
50694
+ }
50695
+ async rejectLinkedComponents(candidate) {
50696
+ if (!isWithin(this.root, candidate)) {
50697
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50698
+ }
50699
+ const relative = path5.relative(this.root, candidate);
50700
+ if (!relative) return;
50701
+ let current = this.root;
50702
+ for (const segment of relative.split(path5.sep)) {
50703
+ current = path5.join(current, segment);
50704
+ try {
50705
+ const currentStat = await lstat(current);
50706
+ if (currentStat.isSymbolicLink()) {
50707
+ throw new WorkerError(
50708
+ "linked_path",
50709
+ "Symlink and junction paths are not allowed."
50710
+ );
50711
+ }
50712
+ } catch (error46) {
50713
+ if (error46 instanceof WorkerError) throw error46;
50714
+ if (error46.code === "ENOENT") return;
50715
+ throw error46;
50716
+ }
50717
+ }
50718
+ }
50719
+ };
50720
+
50721
+ // src/worker/file-service.ts
50722
+ function sha256(content) {
50723
+ return createHash("sha256").update(content).digest("hex");
50724
+ }
50725
+ function isPathWithin(root, candidate) {
50726
+ const relative = path6.relative(root, candidate);
50727
+ return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
50728
+ }
50729
+ var fileWriteTails = /* @__PURE__ */ new Map();
50730
+ async function withFileWriteLock(target, operation) {
50731
+ const normalized = path6.normalize(target);
50732
+ const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;
50733
+ const predecessor = fileWriteTails.get(key);
50734
+ let release;
50735
+ const tail = new Promise((resolve) => {
50736
+ release = resolve;
50737
+ });
50738
+ fileWriteTails.set(key, tail);
50739
+ if (predecessor) await predecessor;
50740
+ try {
50741
+ return await operation();
50742
+ } finally {
50743
+ release();
50744
+ if (fileWriteTails.get(key) === tail) fileWriteTails.delete(key);
50745
+ }
50746
+ }
50747
+ async function withFileWriteLocks(targets, operation) {
50748
+ const unique = [...new Set(targets.map((target) => path6.normalize(target)))].sort(
50749
+ (left, right) => left.localeCompare(right)
50750
+ );
50751
+ const run = async (index) => {
50752
+ const target = unique[index];
50753
+ return target === void 0 ? await operation() : await withFileWriteLock(target, async () => await run(index + 1));
50754
+ };
50755
+ return await run(0);
50756
+ }
50757
+ async function requireRevision(target, expectedSha256) {
50758
+ let actual = null;
50759
+ try {
50760
+ actual = sha256(await readFile3(target));
50761
+ } catch (error46) {
50762
+ if (error46.code !== "ENOENT") throw error46;
50763
+ }
50764
+ if (actual !== expectedSha256) {
50765
+ throw new WorkerError("stale_revision", "The file revision has changed.");
50766
+ }
50767
+ }
50768
+ var DEFAULT_LIST_FILES_LIMIT = 100;
50769
+ var DEFAULT_SEARCH_TEXT_RESULTS = 50;
50770
+ var DEFAULT_READ_RANGE_LINES = 200;
50771
+ var MAX_REPOSITORY_SCAN_ENTRIES = 2e4;
50772
+ var MAX_SEARCH_FILES = 5e3;
50773
+ var MAX_SEARCH_BYTES = 32 * 1024 * 1024;
50774
+ var SKIPPED_RECURSIVE_DIRECTORIES = /* @__PURE__ */ new Set([
50775
+ ".git",
50776
+ ".hg",
50777
+ ".svn",
50778
+ "node_modules"
50779
+ ]);
50780
+ function compareNames(left, right) {
50781
+ return left < right ? -1 : left > right ? 1 : 0;
50782
+ }
50783
+ function relativeSortKey(root, target) {
50784
+ const relative = path6.relative(root, target);
50785
+ if (!relative) return ".";
50786
+ return process.platform === "win32" ? relative.replaceAll("\\", "/") : relative;
50787
+ }
50788
+ function displayPath(root, target) {
50789
+ const relative = relativeSortKey(root, target);
50790
+ if (process.platform === "win32") return relative;
50791
+ return relative.includes("\\") ? "./" + relative : relative;
50792
+ }
50793
+ function isUnavailableFileError(error46) {
50794
+ const code = error46?.code;
50795
+ return code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "ENOTDIR";
50796
+ }
50797
+ function isUnavailableDiscoveredPathError(error46) {
50798
+ return isUnavailableFileError(error46) || error46 instanceof WorkerError && error46.code === "path_not_found";
50799
+ }
50800
+ function isLinkedPathError(error46) {
50801
+ return error46 instanceof WorkerError && error46.code === "linked_path";
50802
+ }
50803
+ async function readBoundedFile(target, maximumBytes, expectedBytes) {
50804
+ const handle = await open(target, "r");
50805
+ try {
50806
+ const openedStat = await handle.stat();
50807
+ if (!openedStat.isFile()) {
50808
+ throw new WorkerError("not_file", "The requested path is not a file.");
50809
+ }
50810
+ if (openedStat.size !== expectedBytes) {
50811
+ throw new WorkerError("file_changed", "The file changed while it was being read.");
50812
+ }
50813
+ if (openedStat.size > maximumBytes) {
50814
+ throw new WorkerError("search_byte_limit", "The search byte limit was reached.");
50815
+ }
50816
+ const buffer = Buffer.allocUnsafe(openedStat.size);
50817
+ let offset = 0;
50818
+ while (offset < buffer.byteLength) {
50819
+ const { bytesRead } = await handle.read(
50820
+ buffer,
50821
+ offset,
50822
+ buffer.byteLength - offset,
50823
+ offset
50824
+ );
50825
+ if (bytesRead === 0) break;
50826
+ offset += bytesRead;
50827
+ }
50828
+ const completedStat = await handle.stat();
50829
+ if (completedStat.size !== openedStat.size || offset !== openedStat.size) {
50830
+ throw new WorkerError("file_changed", "The file changed while it was being read.");
50831
+ }
50832
+ return Buffer.from(buffer);
50833
+ } finally {
50834
+ await handle.close();
50835
+ }
50836
+ }
50837
+ function normalizedLines(content) {
50838
+ if (content.length === 0) return [];
50839
+ const normalized = content.replace(/\r\n?/g, "\n");
50840
+ const lines = normalized.split("\n");
50841
+ if (normalized.endsWith("\n")) lines.pop();
50383
50842
  return lines;
50384
50843
  }
50385
50844
  function escapeRegExp(value) {
@@ -50563,7 +51022,7 @@ var FileService = class {
50563
51022
  this.#beforeDeadline = dependencies.beforeDeadline ?? defaultBeforeDeadline;
50564
51023
  this.#openDirectory = dependencies.openDirectory ?? ((directory) => opendir(directory));
50565
51024
  this.#readFileBytes = dependencies.readFileBytes ?? readBoundedFile;
50566
- this.#lstatPath = dependencies.lstatPath ?? ((target) => lstat(target));
51025
+ this.#lstatPath = dependencies.lstatPath ?? ((target) => lstat2(target));
50567
51026
  this.#trustDirectoryEntryTypes = dependencies.trustDirectoryEntryTypes ?? dependencies.lstatPath === void 0;
50568
51027
  this.#maxRepositoryScanEntries = dependencies.maxRepositoryScanEntries ?? MAX_REPOSITORY_SCAN_ENTRIES;
50569
51028
  this.#maxSearchBytes = dependencies.maxSearchBytes ?? MAX_SEARCH_BYTES;
@@ -50634,7 +51093,7 @@ var FileService = class {
50634
51093
  }
50635
51094
  async #readResolvedText(target, maximumBytes = MAX_TEXT_BYTES) {
50636
51095
  const boundedMaximum = Math.min(maximumBytes, MAX_TEXT_BYTES);
50637
- const targetStat = await stat(target);
51096
+ const targetStat = await stat2(target);
50638
51097
  if (!targetStat.isFile()) {
50639
51098
  throw new WorkerError("not_file", "The requested path is not a file.");
50640
51099
  }
@@ -50677,7 +51136,7 @@ var FileService = class {
50677
51136
  this.policy.resolveExisting(startPath),
50678
51137
  deadlineAt
50679
51138
  );
50680
- const startStat = await this.#withinDeadline(stat(start), deadlineAt);
51139
+ const startStat = await this.#withinDeadline(stat2(start), deadlineAt);
50681
51140
  if (!startStat.isDirectory()) {
50682
51141
  throw new WorkerError("not_directory", "The requested path is not a directory.");
50683
51142
  }
@@ -50706,7 +51165,7 @@ var FileService = class {
50706
51165
  if (batch.overflow) throw scanLimitError();
50707
51166
  scannedEntries += batch.children.length;
50708
51167
  for (const child of batch.children) {
50709
- const target = path5.join(directory, child.name);
51168
+ const target = path6.join(directory, child.name);
50710
51169
  heapPush(frontier, {
50711
51170
  target,
50712
51171
  sortKey: relativeSortKey(this.policy.root, target),
@@ -50730,7 +51189,7 @@ var FileService = class {
50730
51189
  throw scanLimitError();
50731
51190
  }
50732
51191
  scannedEntries += 1;
50733
- const target = path5.join(directory, child.name);
51192
+ const target = path6.join(directory, child.name);
50734
51193
  let childStat;
50735
51194
  try {
50736
51195
  childStat = await this.#withinDeadline(
@@ -50876,15 +51335,31 @@ var FileService = class {
50876
51335
  );
50877
51336
  const matchLimit = maxResults + 1;
50878
51337
  const extensions = options.extensions?.map((extension) => extension.toLowerCase());
50879
- const matcher = new RegExp(
50880
- escapeRegExp(options.query),
50881
- options.caseSensitive === true ? "u" : "iu"
50882
- );
51338
+ const includeGlobs = options.includeGlobs;
51339
+ const excludeGlobs = options.excludeGlobs;
51340
+ const globMatches = (relativePath, patterns) => {
51341
+ if (!patterns) return false;
51342
+ const normalized = relativePath.replaceAll("\\", "/");
51343
+ try {
51344
+ return patterns.some((pattern) => path6.posix.matchesGlob(normalized, pattern));
51345
+ } catch {
51346
+ throw new WorkerError("invalid_search", "Search glob pattern is invalid.");
51347
+ }
51348
+ };
51349
+ let matcher;
51350
+ try {
51351
+ matcher = new RegExp(
51352
+ options.matchMode === "regex" ? options.query : escapeRegExp(options.query),
51353
+ options.caseSensitive === true ? "u" : "iu"
51354
+ );
51355
+ } catch {
51356
+ throw new WorkerError("invalid_search", "Search regular expression is invalid.");
51357
+ }
50883
51358
  const start = await this.#withinDeadline(
50884
51359
  this.policy.resolveExisting(options.path ?? "."),
50885
51360
  deadlineAt
50886
51361
  );
50887
- const startStat = await this.#withinDeadline(stat(start), deadlineAt);
51362
+ const startStat = await this.#withinDeadline(stat2(start), deadlineAt);
50888
51363
  const matches = [];
50889
51364
  let scannedEntries = 0;
50890
51365
  let scannedFiles = 0;
@@ -50895,10 +51370,12 @@ var FileService = class {
50895
51370
  const searchFile = async (target) => {
50896
51371
  const relative = displayPath(this.policy.root, target);
50897
51372
  if (extensions && !extensions.some(
50898
- (extension) => path5.basename(target).toLowerCase().endsWith(extension)
51373
+ (extension) => path6.basename(target).toLowerCase().endsWith(extension)
50899
51374
  )) {
50900
51375
  return false;
50901
51376
  }
51377
+ if (includeGlobs && !globMatches(relative, includeGlobs)) return false;
51378
+ if (globMatches(relative, excludeGlobs)) return false;
50902
51379
  if (scannedFiles >= MAX_SEARCH_FILES) {
50903
51380
  scanTruncated = true;
50904
51381
  return true;
@@ -50988,7 +51465,7 @@ var FileService = class {
50988
51465
  scannedEntries += batch.children.length;
50989
51466
  for (const child of batch.children) {
50990
51467
  this.#assertBeforeDeadline(deadlineAt);
50991
- const target = path5.join(directory, child.name);
51468
+ const target = path6.join(directory, child.name);
50992
51469
  let targetType;
50993
51470
  if (this.#trustDirectoryEntryTypes) {
50994
51471
  if (child.isSymbolicLink()) {
@@ -51123,6 +51600,118 @@ var FileService = class {
51123
51600
  ...endLine < lines.length ? { nextLine: endLine + 1 } : {}
51124
51601
  };
51125
51602
  }
51603
+ async makeDirectory(relativePath, recursive = false) {
51604
+ const initial = await this.policy.resolveWritableDirectory(
51605
+ relativePath,
51606
+ recursive
51607
+ );
51608
+ if (initial.exists) return { created: false };
51609
+ return await withFileWriteLock(initial.target, async () => {
51610
+ const current = await this.policy.resolveWritableDirectory(
51611
+ relativePath,
51612
+ recursive
51613
+ );
51614
+ if (current.exists) return { created: false };
51615
+ try {
51616
+ await mkdir3(current.target, { recursive });
51617
+ } catch (error46) {
51618
+ if (error46.code !== "EEXIST") throw error46;
51619
+ }
51620
+ const resolved = await this.policy.resolveExisting(relativePath);
51621
+ if (!(await stat2(resolved)).isDirectory()) {
51622
+ throw new WorkerError("not_directory", "The destination is not a directory.");
51623
+ }
51624
+ return { created: true };
51625
+ });
51626
+ }
51627
+ async deletePath(relativePath, recursive = false) {
51628
+ const initial = await this.policy.resolveExisting(relativePath);
51629
+ if (samePath(initial, this.policy.root)) {
51630
+ throw new WorkerError(
51631
+ "root_operation_refused",
51632
+ "The exposed workspace root cannot be deleted."
51633
+ );
51634
+ }
51635
+ return await withFileWriteLock(initial, async () => {
51636
+ const target = await this.policy.resolveExisting(relativePath);
51637
+ if (samePath(target, this.policy.root)) {
51638
+ throw new WorkerError(
51639
+ "root_operation_refused",
51640
+ "The exposed workspace root cannot be deleted."
51641
+ );
51642
+ }
51643
+ const targetStat = await lstat2(target);
51644
+ if (!targetStat.isFile() && !targetStat.isDirectory()) {
51645
+ throw new WorkerError(
51646
+ "unsupported_path_type",
51647
+ "Only regular files and directories can be deleted."
51648
+ );
51649
+ }
51650
+ try {
51651
+ if (targetStat.isDirectory()) {
51652
+ if (recursive) {
51653
+ await rm2(target, { recursive: true, force: false });
51654
+ } else {
51655
+ await rmdir(target);
51656
+ }
51657
+ } else {
51658
+ await rm2(target, { force: false });
51659
+ }
51660
+ } catch (error46) {
51661
+ const code = error46.code;
51662
+ if (targetStat.isDirectory() && !recursive && (code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM")) {
51663
+ throw new WorkerError(
51664
+ "directory_not_empty",
51665
+ "The directory is not empty. Set recursive to true to delete its contents."
51666
+ );
51667
+ }
51668
+ throw error46;
51669
+ }
51670
+ return {
51671
+ deletedType: targetStat.isDirectory() ? "directory" : "file"
51672
+ };
51673
+ });
51674
+ }
51675
+ async movePath(sourcePath, destinationPath) {
51676
+ const initialSource = await this.policy.resolveExisting(sourcePath);
51677
+ if (samePath(initialSource, this.policy.root)) {
51678
+ throw new WorkerError(
51679
+ "root_operation_refused",
51680
+ "The exposed workspace root cannot be moved."
51681
+ );
51682
+ }
51683
+ const initialDestination = await this.policy.resolveVacantPath(destinationPath);
51684
+ return await withFileWriteLocks(
51685
+ [initialSource, initialDestination],
51686
+ async () => {
51687
+ const source = await this.policy.resolveExisting(sourcePath);
51688
+ if (samePath(source, this.policy.root)) {
51689
+ throw new WorkerError(
51690
+ "root_operation_refused",
51691
+ "The exposed workspace root cannot be moved."
51692
+ );
51693
+ }
51694
+ const destination = await this.policy.resolveVacantPath(destinationPath);
51695
+ const sourceStat = await lstat2(source);
51696
+ if (!sourceStat.isFile() && !sourceStat.isDirectory()) {
51697
+ throw new WorkerError(
51698
+ "unsupported_path_type",
51699
+ "Only regular files and directories can be moved."
51700
+ );
51701
+ }
51702
+ if (sourceStat.isDirectory() && isPathWithin(source, destination)) {
51703
+ throw new WorkerError(
51704
+ "invalid_destination",
51705
+ "A directory cannot be moved inside itself."
51706
+ );
51707
+ }
51708
+ await rename(source, destination);
51709
+ return {
51710
+ movedType: sourceStat.isDirectory() ? "directory" : "file"
51711
+ };
51712
+ }
51713
+ );
51714
+ }
51126
51715
  async editText(relativePath, edits, expectedSha256) {
51127
51716
  const original = await this.readText(relativePath);
51128
51717
  if (expectedSha256 && original.sha256 !== expectedSha256) {
@@ -51174,25 +51763,45 @@ var FileService = class {
51174
51763
  return await withFileWriteLock(target, async () => {
51175
51764
  let existingMode;
51176
51765
  try {
51177
- const existingStat = await stat(target);
51766
+ const existingStat = await stat2(target);
51178
51767
  if (existingStat.isFile()) existingMode = existingStat.mode & 4095;
51179
51768
  } catch (error46) {
51180
51769
  if (error46.code !== "ENOENT") throw error46;
51181
51770
  }
51771
+ if (existingMode !== void 0 && expectedSha256 === void 0) {
51772
+ throw new WorkerError(
51773
+ "path_exists",
51774
+ "The file already exists. Read it first and pass expectedSha256 to replace that revision."
51775
+ );
51776
+ }
51182
51777
  if (expectedSha256) await requireRevision(target, expectedSha256);
51183
- const temporary = path5.join(path5.dirname(target), `.glossa-${randomUUID2()}.tmp`);
51778
+ const temporary = path6.join(path6.dirname(target), `.glossa-${randomUUID2()}.tmp`);
51184
51779
  try {
51185
51780
  await writeFile3(temporary, bytes, { flag: "wx", mode: 384 });
51186
51781
  target = await this.policy.resolveWritableFile(relativePath);
51187
- const tempStat = await lstat(temporary);
51782
+ const tempStat = await lstat2(temporary);
51188
51783
  if (!tempStat.isFile() || tempStat.isSymbolicLink()) {
51189
51784
  throw new WorkerError("unsafe_temporary_file", "The atomic write temporary file changed.");
51190
51785
  }
51191
51786
  if (existingMode !== void 0 && process.platform !== "win32") {
51192
51787
  await chmod2(temporary, existingMode);
51193
51788
  }
51194
- if (expectedSha256) await requireRevision(target, expectedSha256);
51195
- await rename(temporary, target);
51789
+ if (expectedSha256) {
51790
+ await requireRevision(target, expectedSha256);
51791
+ await rename(temporary, target);
51792
+ } else {
51793
+ try {
51794
+ await link2(temporary, target);
51795
+ } catch (error46) {
51796
+ if (error46.code === "EEXIST") {
51797
+ throw new WorkerError(
51798
+ "path_exists",
51799
+ "The file already exists. Read it first and pass expectedSha256 to replace that revision."
51800
+ );
51801
+ }
51802
+ throw error46;
51803
+ }
51804
+ }
51196
51805
  } finally {
51197
51806
  await rm2(temporary, { force: true });
51198
51807
  }
@@ -51201,173 +51810,6 @@ var FileService = class {
51201
51810
  }
51202
51811
  };
51203
51812
 
51204
- // src/worker/path-policy.ts
51205
- import { lstat as lstat2, realpath, stat as stat2 } from "node:fs/promises";
51206
- import os5 from "node:os";
51207
- import path6 from "node:path";
51208
- function samePath(left, right) {
51209
- return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
51210
- }
51211
- function accountHomeDirectory() {
51212
- try {
51213
- return os5.userInfo().homedir;
51214
- } catch {
51215
- return os5.homedir();
51216
- }
51217
- }
51218
- function isWithin(root, candidate) {
51219
- const relative = path6.relative(root, candidate);
51220
- return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
51221
- }
51222
- function validateRelativePath(value) {
51223
- if (value.includes("\0")) {
51224
- throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
51225
- }
51226
- const explicitNativePosixPath = process.platform !== "win32" && value.startsWith("./");
51227
- if (path6.isAbsolute(value) || path6.posix.isAbsolute(value) || !explicitNativePosixPath && path6.win32.isAbsolute(value)) {
51228
- throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
51229
- }
51230
- const segments = explicitNativePosixPath ? value.split(/\/+/).filter(Boolean) : value.split(/[\\/]+/);
51231
- if (segments.includes("..")) {
51232
- throw new WorkerError("path_traversal", "Parent path traversal is not allowed.");
51233
- }
51234
- return value === "" ? "." : value;
51235
- }
51236
- async function canonicalizeRoot(candidate) {
51237
- const root = await realpath(path6.resolve(candidate)).catch((error46) => {
51238
- if (error46.code === "ENOENT") {
51239
- throw new WorkerError("root_not_found", "The workspace directory does not exist.");
51240
- }
51241
- throw error46;
51242
- });
51243
- const rootStat = await stat2(root);
51244
- if (!rootStat.isDirectory()) {
51245
- throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
51246
- }
51247
- const filesystemRoot = path6.parse(root).root;
51248
- const homes = await Promise.all(
51249
- [os5.homedir(), accountHomeDirectory()].map(
51250
- async (home) => await realpath(home).catch(() => path6.resolve(home))
51251
- )
51252
- );
51253
- const isHomeDirectory = homes.some((home) => samePath(root, home));
51254
- if (samePath(root, filesystemRoot) || isHomeDirectory) {
51255
- const kind = isHomeDirectory ? "your home directory" : "a filesystem root";
51256
- throw new WorkerError(
51257
- "broad_root_refused",
51258
- `The selected root is ${kind}, which Glossa will not expose. Choose a project directory instead.`
51259
- );
51260
- }
51261
- return root;
51262
- }
51263
- var PathPolicy = class _PathPolicy {
51264
- constructor(root) {
51265
- this.root = root;
51266
- }
51267
- root;
51268
- static async create(candidate) {
51269
- return new _PathPolicy(await canonicalizeRoot(candidate));
51270
- }
51271
- async resolveExisting(relativePath) {
51272
- const lexical = this.resolveLexical(relativePath);
51273
- await this.rejectLinkedComponents(lexical);
51274
- const canonical = await realpath(lexical).catch((error46) => {
51275
- if (error46.code === "ENOENT") {
51276
- throw new WorkerError("path_not_found", "The requested path does not exist.");
51277
- }
51278
- throw error46;
51279
- });
51280
- if (!isWithin(this.root, canonical)) {
51281
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51282
- }
51283
- return canonical;
51284
- }
51285
- async resolveDiscoveredExisting(candidate) {
51286
- const lexical = path6.resolve(candidate);
51287
- if (!isWithin(this.root, lexical)) {
51288
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51289
- }
51290
- await this.rejectLinkedComponents(lexical);
51291
- const canonical = await realpath(lexical).catch((error46) => {
51292
- if (error46.code === "ENOENT") {
51293
- throw new WorkerError("path_not_found", "The requested path does not exist.");
51294
- }
51295
- throw error46;
51296
- });
51297
- if (!isWithin(this.root, canonical)) {
51298
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51299
- }
51300
- return canonical;
51301
- }
51302
- async resolveWritableFile(relativePath) {
51303
- const lexical = this.resolveLexical(relativePath);
51304
- const parent = path6.dirname(lexical);
51305
- await this.rejectLinkedComponents(parent);
51306
- const canonicalParent = await realpath(parent).catch((error46) => {
51307
- if (error46.code === "ENOENT") {
51308
- throw new WorkerError("parent_not_found", "The destination directory does not exist.");
51309
- }
51310
- throw error46;
51311
- });
51312
- if (!isWithin(this.root, canonicalParent)) {
51313
- throw new WorkerError("path_escape", "The destination escapes the exposed root.");
51314
- }
51315
- if (!(await stat2(canonicalParent)).isDirectory()) {
51316
- throw new WorkerError("not_directory", "The destination parent is not a directory.");
51317
- }
51318
- try {
51319
- const targetStat = await lstat2(lexical);
51320
- if (targetStat.isSymbolicLink()) {
51321
- throw new WorkerError("linked_path", "Writes through links are not allowed.");
51322
- }
51323
- if (targetStat.isDirectory()) {
51324
- throw new WorkerError("not_file", "The destination is a directory.");
51325
- }
51326
- const canonicalTarget = await realpath(lexical);
51327
- if (!isWithin(this.root, canonicalTarget)) {
51328
- throw new WorkerError("path_escape", "The destination escapes the exposed root.");
51329
- }
51330
- return canonicalTarget;
51331
- } catch (error46) {
51332
- if (error46 instanceof WorkerError) throw error46;
51333
- if (error46.code !== "ENOENT") throw error46;
51334
- }
51335
- return path6.join(canonicalParent, path6.basename(lexical));
51336
- }
51337
- resolveLexical(relativePath) {
51338
- const validated = validateRelativePath(relativePath);
51339
- const candidate = path6.resolve(this.root, validated);
51340
- if (!isWithin(this.root, candidate)) {
51341
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51342
- }
51343
- return candidate;
51344
- }
51345
- async rejectLinkedComponents(candidate) {
51346
- if (!isWithin(this.root, candidate)) {
51347
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51348
- }
51349
- const relative = path6.relative(this.root, candidate);
51350
- if (!relative) return;
51351
- let current = this.root;
51352
- for (const segment of relative.split(path6.sep)) {
51353
- current = path6.join(current, segment);
51354
- try {
51355
- const currentStat = await lstat2(current);
51356
- if (currentStat.isSymbolicLink()) {
51357
- throw new WorkerError(
51358
- "linked_path",
51359
- "Symlink and junction paths are not allowed."
51360
- );
51361
- }
51362
- } catch (error46) {
51363
- if (error46 instanceof WorkerError) throw error46;
51364
- if (error46.code === "ENOENT") return;
51365
- throw error46;
51366
- }
51367
- }
51368
- }
51369
- };
51370
-
51371
51813
  // src/worker/local-worker.ts
51372
51814
  function restrictedDataError2() {
51373
51815
  return new WorkerError(
@@ -51402,6 +51844,14 @@ function jobInputContainsRestrictedData(job) {
51402
51844
  path: job.path,
51403
51845
  edits: job.edits
51404
51846
  });
51847
+ case "make_directory":
51848
+ case "delete_path":
51849
+ return containsRestrictedAuthenticationData(job.path);
51850
+ case "move_path":
51851
+ return containsRestrictedAuthenticationData({
51852
+ source: job.source,
51853
+ destination: job.destination
51854
+ });
51405
51855
  case "run_command":
51406
51856
  return containsRestrictedAuthenticationData({
51407
51857
  argv: job.argv,
@@ -51409,12 +51859,13 @@ function jobInputContainsRestrictedData(job) {
51409
51859
  stdin: job.stdin
51410
51860
  });
51411
51861
  case "get_command":
51862
+ case "read_command_output":
51412
51863
  case "cancel_command":
51413
51864
  return false;
51414
51865
  }
51415
51866
  }
51416
51867
  function resultMayContainRestrictedData(job) {
51417
- return job.type === "read_file" || job.type === "list_files" || job.type === "search_text" || job.type === "read_file_range" || job.type === "edit_file" || job.type === "run_command" || job.type === "get_command" || job.type === "cancel_command";
51868
+ return job.type === "read_file" || job.type === "list_files" || job.type === "search_text" || job.type === "read_file_range" || job.type === "edit_file" || job.type === "run_command" || job.type === "get_command" || job.type === "read_command_output" || job.type === "cancel_command";
51418
51869
  }
51419
51870
  var LocalWorker = class _LocalWorker {
51420
51871
  constructor(accessProfile, policy, files, commands) {
@@ -51439,13 +51890,13 @@ var LocalWorker = class _LocalWorker {
51439
51890
  async handle(job) {
51440
51891
  try {
51441
51892
  const permissions = workerPermissions(this.accessProfile);
51442
- if ((job.type === "write_file" || job.type === "edit_file") && !permissions.writeFiles) {
51893
+ if ((job.type === "write_file" || job.type === "edit_file" || job.type === "make_directory" || job.type === "delete_path" || job.type === "move_path") && !permissions.writeFiles) {
51443
51894
  throw new WorkerError(
51444
51895
  "write_access_disabled",
51445
51896
  "This worker was started without file-write access."
51446
51897
  );
51447
51898
  }
51448
- if ((job.type === "run_command" || job.type === "get_command" || job.type === "cancel_command") && !permissions.runCommands) {
51899
+ if ((job.type === "run_command" || job.type === "get_command" || job.type === "read_command_output" || job.type === "cancel_command") && !permissions.runCommands) {
51449
51900
  throw new WorkerError(
51450
51901
  "command_access_disabled",
51451
51902
  "This worker was started without system-command access."
@@ -51470,9 +51921,12 @@ var LocalWorker = class _LocalWorker {
51470
51921
  value = await this.files.searchText({
51471
51922
  query: job.query,
51472
51923
  ...job.path ? { path: job.path } : {},
51924
+ ...job.matchMode === void 0 ? {} : { matchMode: job.matchMode },
51473
51925
  ...job.caseSensitive === void 0 ? {} : { caseSensitive: job.caseSensitive },
51474
51926
  ...job.maxResults === void 0 ? {} : { maxResults: job.maxResults },
51475
51927
  ...job.extensions ? { extensions: job.extensions } : {},
51928
+ ...job.includeGlobs ? { includeGlobs: job.includeGlobs } : {},
51929
+ ...job.excludeGlobs ? { excludeGlobs: job.excludeGlobs } : {},
51476
51930
  timeoutMs: job.timeoutMs
51477
51931
  });
51478
51932
  break;
@@ -51503,6 +51957,15 @@ var LocalWorker = class _LocalWorker {
51503
51957
  );
51504
51958
  break;
51505
51959
  }
51960
+ case "make_directory":
51961
+ value = await this.files.makeDirectory(job.path, job.recursive);
51962
+ break;
51963
+ case "delete_path":
51964
+ value = await this.files.deletePath(job.path, job.recursive);
51965
+ break;
51966
+ case "move_path":
51967
+ value = await this.files.movePath(job.source, job.destination);
51968
+ break;
51506
51969
  case "run_command":
51507
51970
  value = await this.commands.start({
51508
51971
  ...job.argv ? { argv: job.argv } : {},
@@ -51519,6 +51982,14 @@ var LocalWorker = class _LocalWorker {
51519
51982
  job.afterSequence
51520
51983
  );
51521
51984
  break;
51985
+ case "read_command_output":
51986
+ value = await this.commands.readOutput(
51987
+ job.commandId,
51988
+ job.stream,
51989
+ job.offset,
51990
+ job.maxBytes
51991
+ );
51992
+ break;
51522
51993
  case "cancel_command":
51523
51994
  value = await this.commands.cancel(job.commandId);
51524
51995
  break;
@@ -51581,6 +52052,7 @@ function supportsCapability(value, capability) {
51581
52052
  function jobLane(job) {
51582
52053
  switch (job.type) {
51583
52054
  case "get_command":
52055
+ case "read_command_output":
51584
52056
  return "status";
51585
52057
  case "cancel_command":
51586
52058
  return "cancel";
@@ -51591,14 +52063,20 @@ function jobLane(job) {
51591
52063
  return "read";
51592
52064
  case "write_file":
51593
52065
  case "edit_file":
52066
+ case "make_directory":
52067
+ case "delete_path":
52068
+ case "move_path":
51594
52069
  case "run_command":
51595
52070
  return "mutation";
51596
52071
  }
51597
52072
  }
51598
- function acceptedJobTypes(counts, total, structuredReads) {
52073
+ function acceptedJobTypes(counts, total, structuredReads, structuredMutations, commandOutputRanges) {
51599
52074
  if (total >= MAX_CONCURRENT_JOBS) return [];
51600
52075
  const accepted = [];
51601
- if (counts.status < 1) accepted.push("get_command");
52076
+ if (counts.status < 1) {
52077
+ accepted.push("get_command");
52078
+ if (commandOutputRanges) accepted.push("read_command_output");
52079
+ }
51602
52080
  if (counts.cancel < 1) accepted.push("cancel_command");
51603
52081
  if (counts.read < 2) {
51604
52082
  accepted.push("read_file");
@@ -51608,6 +52086,9 @@ function acceptedJobTypes(counts, total, structuredReads) {
51608
52086
  }
51609
52087
  if (counts.mutation < 1) {
51610
52088
  accepted.push("write_file", "edit_file", "run_command");
52089
+ if (structuredMutations) {
52090
+ accepted.push("make_directory", "delete_path", "move_path");
52091
+ }
51611
52092
  }
51612
52093
  return accepted;
51613
52094
  }
@@ -51712,6 +52193,25 @@ var RemoteWorker = class {
51712
52193
  }
51713
52194
  }
51714
52195
  async #register() {
52196
+ const currentBody = {
52197
+ workerId: this.#workerId,
52198
+ capabilities: {
52199
+ commandProgress: true,
52200
+ concurrentJobs: true,
52201
+ structuredReads: true,
52202
+ structuredMutations: true,
52203
+ commandOutputRanges: true
52204
+ }
52205
+ };
52206
+ const mutationBody = {
52207
+ workerId: this.#workerId,
52208
+ capabilities: {
52209
+ commandProgress: true,
52210
+ concurrentJobs: true,
52211
+ structuredReads: true,
52212
+ structuredMutations: true
52213
+ }
52214
+ };
51715
52215
  const structuredBody = {
51716
52216
  workerId: this.#workerId,
51717
52217
  capabilities: {
@@ -51724,14 +52224,54 @@ var RemoteWorker = class {
51724
52224
  workerId: this.#workerId,
51725
52225
  capabilities: { commandProgress: true, concurrentJobs: true }
51726
52226
  };
52227
+ const versionedCurrentBody = this.#workerVersion ? { ...currentBody, workerVersion: this.#workerVersion } : void 0;
52228
+ const versionedMutationBody = this.#workerVersion ? { ...mutationBody, workerVersion: this.#workerVersion } : void 0;
51727
52229
  const versionedStructuredBody = this.#workerVersion ? { ...structuredBody, workerVersion: this.#workerVersion } : void 0;
51728
52230
  const preferredProfileBody = this.#accessProfile ? {
51729
- ...versionedStructuredBody ?? structuredBody,
52231
+ ...versionedCurrentBody ?? currentBody,
51730
52232
  accessProfile: this.#accessProfile,
51731
52233
  ...this.#workspaceLabel ? { workspaceLabel: this.#workspaceLabel } : {}
51732
52234
  } : void 0;
51733
52235
  const attempts = [
51734
52236
  ...preferredProfileBody ? [{ body: preferredProfileBody, legacyRelay: false }] : [],
52237
+ ...versionedCurrentBody && this.#workspaceLabel ? [{
52238
+ body: {
52239
+ ...versionedCurrentBody,
52240
+ workspaceLabel: this.#workspaceLabel
52241
+ },
52242
+ legacyRelay: false
52243
+ }] : [],
52244
+ ...this.#workspaceLabel ? [{
52245
+ body: {
52246
+ ...currentBody,
52247
+ workspaceLabel: this.#workspaceLabel
52248
+ },
52249
+ legacyRelay: false
52250
+ }] : [],
52251
+ ...versionedCurrentBody ? [{ body: versionedCurrentBody, legacyRelay: false }] : [],
52252
+ {
52253
+ body: currentBody,
52254
+ legacyRelay: false
52255
+ },
52256
+ ...versionedMutationBody && this.#workspaceLabel ? [{
52257
+ body: {
52258
+ ...versionedMutationBody,
52259
+ workspaceLabel: this.#workspaceLabel
52260
+ },
52261
+ legacyRelay: false
52262
+ }] : [],
52263
+ ...this.#workspaceLabel ? [{
52264
+ body: {
52265
+ ...mutationBody,
52266
+ workspaceLabel: this.#workspaceLabel
52267
+ },
52268
+ legacyRelay: false
52269
+ }] : [],
52270
+ ...versionedMutationBody ? [{ body: versionedMutationBody, legacyRelay: false }] : [],
52271
+ {
52272
+ body: mutationBody,
52273
+ legacyRelay: false
52274
+ },
51735
52275
  ...versionedStructuredBody && this.#workspaceLabel ? [{
51736
52276
  body: {
51737
52277
  ...versionedStructuredBody,
@@ -51795,6 +52335,8 @@ var RemoteWorker = class {
51795
52335
  legacyRelay: attempt.legacyRelay,
51796
52336
  concurrentJobs: !attempt.legacyRelay && supportsCapability(value, "concurrentJobs"),
51797
52337
  structuredReads: !attempt.legacyRelay && supportsCapability(value, "structuredReads"),
52338
+ structuredMutations: !attempt.legacyRelay && supportsCapability(value, "structuredMutations"),
52339
+ commandOutputRanges: !attempt.legacyRelay && supportsCapability(value, "commandOutputRanges"),
51798
52340
  accessProfileAccepted: this.#accessProfile === void 0 || "accessProfile" in value && value.accessProfile === this.#accessProfile,
51799
52341
  workspaceLabelAccepted: this.#workspaceLabel === void 0 || "workspaceLabel" in value && value.workspaceLabel === this.#workspaceLabel,
51800
52342
  ...workerToken ? { workerToken } : {}
@@ -51889,7 +52431,9 @@ var RemoteWorker = class {
51889
52431
  const acceptedTypes = acceptedJobTypes(
51890
52432
  counts,
51891
52433
  inFlight.size,
51892
- session.structuredReads
52434
+ session.structuredReads,
52435
+ session.structuredMutations,
52436
+ session.commandOutputRanges
51893
52437
  );
51894
52438
  if (acceptedTypes.length === 0) {
51895
52439
  await Promise.race(inFlight);
@@ -51919,7 +52463,9 @@ var RemoteWorker = class {
51919
52463
  const refreshedTypes = acceptedJobTypes(
51920
52464
  counts,
51921
52465
  inFlight.size,
51922
- session.structuredReads
52466
+ session.structuredReads,
52467
+ session.structuredMutations,
52468
+ session.commandOutputRanges
51923
52469
  );
51924
52470
  const newlyAcceptedTypes = refreshedTypes.filter(
51925
52471
  (type) => !acceptedTypes.includes(type)
@@ -52099,6 +52645,15 @@ function activitySafeJob(job) {
52099
52645
  newText: ""
52100
52646
  }]
52101
52647
  };
52648
+ case "make_directory":
52649
+ case "delete_path":
52650
+ return { ...job, path: "[restricted input blocked]" };
52651
+ case "move_path":
52652
+ return {
52653
+ ...job,
52654
+ source: "[restricted input blocked]",
52655
+ destination: "[restricted input blocked]"
52656
+ };
52102
52657
  case "run_command":
52103
52658
  return {
52104
52659
  type: "run_command",
@@ -52108,6 +52663,7 @@ function activitySafeJob(job) {
52108
52663
  ...job.waitMs === void 0 ? {} : { waitMs: job.waitMs }
52109
52664
  };
52110
52665
  case "get_command":
52666
+ case "read_command_output":
52111
52667
  case "cancel_command":
52112
52668
  return job;
52113
52669
  }
@@ -52541,6 +53097,16 @@ function summarizeJob(job) {
52541
53097
  `${job.edits.length} ${job.edits.length === 1 ? "edit" : "edits"}`,
52542
53098
  ...job.expectedSha256 ? ["guarded"] : []
52543
53099
  ]);
53100
+ case "make_directory":
53101
+ return pathSummary(job.path, job.recursive ? ["recursive"] : []);
53102
+ case "delete_path":
53103
+ return pathSummary(job.path, job.recursive ? ["recursive"] : []);
53104
+ case "move_path":
53105
+ return {
53106
+ target: `${quoteActivityInput(job.source)} \u2192 ${quoteActivityInput(job.destination)}`,
53107
+ details: [],
53108
+ truncation: "middle"
53109
+ };
52544
53110
  case "run_command":
52545
53111
  return {
52546
53112
  target: job.argv ? `argv [${job.argv.map(quoteActivityInput).join(", ")}]` : `shell ${quoteActivityInput(job.shellCommand ?? "")}`,
@@ -52560,6 +53126,15 @@ function summarizeJob(job) {
52560
53126
  ],
52561
53127
  truncation: "middle"
52562
53128
  };
53129
+ case "read_command_output":
53130
+ return {
53131
+ target: `command ${job.commandId} ${job.stream}`,
53132
+ details: [
53133
+ ...job.offset === void 0 ? [] : [`offset ${job.offset}`],
53134
+ ...job.maxBytes === void 0 ? [] : [`max ${job.maxBytes} bytes`]
53135
+ ],
53136
+ truncation: "middle"
53137
+ };
52563
53138
  case "cancel_command":
52564
53139
  return {
52565
53140
  target: `command ${job.commandId}`,
@@ -53504,7 +54079,7 @@ import path8 from "node:path";
53504
54079
 
53505
54080
  // src/update-lock.ts
53506
54081
  import { randomUUID as randomUUID4 } from "node:crypto";
53507
- import { mkdir as mkdir3, readdir, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
54082
+ import { mkdir as mkdir4, readdir, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
53508
54083
  import path7 from "node:path";
53509
54084
  var UPDATE_SUFFIX = ".update";
53510
54085
  var SESSION_SUFFIX = ".session";
@@ -53552,7 +54127,7 @@ async function activeSessionFiles(directory) {
53552
54127
  return await activeLeaseFiles(directory, SESSION_SUFFIX);
53553
54128
  }
53554
54129
  async function createUpdateLock(directory) {
53555
- await mkdir3(directory, { recursive: true, mode: 448 });
54130
+ await mkdir4(directory, { recursive: true, mode: 448 });
53556
54131
  const lockFile = path7.join(
53557
54132
  directory,
53558
54133
  `${process.pid}-${randomUUID4()}${UPDATE_SUFFIX}`
@@ -53587,7 +54162,7 @@ async function withUpdateLease(action, directory = updateRuntimeDirectory()) {
53587
54162
  }
53588
54163
  }
53589
54164
  async function withWorkspaceLease(action, directory = updateRuntimeDirectory()) {
53590
- await mkdir3(directory, { recursive: true, mode: 448 });
54165
+ await mkdir4(directory, { recursive: true, mode: 448 });
53591
54166
  if (await updateLockIsActive(directory)) {
53592
54167
  throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
53593
54168
  }
@@ -53725,14 +54300,25 @@ async function fetchText(url2, fetchImpl, version2) {
53725
54300
  if (!response.ok) throw new Error(`Glossa could not download ${url2} (HTTP ${response.status}).`);
53726
54301
  return await response.text();
53727
54302
  }
54303
+ function npmInstallInvocation(version2, platform2 = process.platform, commandShell = process.env.ComSpec ?? "cmd.exe") {
54304
+ const normalizedVersion = import_semver.default.valid(version2);
54305
+ if (!normalizedVersion) throw new Error(`Glossa version ${version2} is invalid.`);
54306
+ const packageSpec = `${PACKAGE_NAME}@${normalizedVersion}`;
54307
+ if (platform2 === "win32") {
54308
+ return {
54309
+ command: commandShell,
54310
+ args: ["/d", "/s", "/c", `npm.cmd install --global ${packageSpec}`]
54311
+ };
54312
+ }
54313
+ return {
54314
+ command: "npm",
54315
+ args: ["install", "--global", packageSpec]
54316
+ };
54317
+ }
53728
54318
  async function runNpmInstall(version2) {
53729
- const command = process.platform === "win32" ? "npm.cmd" : "npm";
54319
+ const { command, args } = npmInstallInvocation(version2);
53730
54320
  await new Promise((resolve, reject) => {
53731
- const child = spawn3(
53732
- command,
53733
- ["install", "--global", `${PACKAGE_NAME}@${version2}`],
53734
- { stdio: "inherit" }
53735
- );
54321
+ const child = spawn3(command, args, { stdio: "inherit" });
53736
54322
  child.once("error", reject);
53737
54323
  child.once("close", (status) => {
53738
54324
  if (status === 0) resolve();
@@ -53795,7 +54381,7 @@ async function installUpdate(info, distribution, options = {}) {
53795
54381
  }
53796
54382
 
53797
54383
  // src/update-state.ts
53798
- import { chmod as chmod4, mkdir as mkdir4, readFile as readFile4, rename as rename3, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
54384
+ import { chmod as chmod4, mkdir as mkdir5, readFile as readFile4, rename as rename3, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
53799
54385
  import path9 from "node:path";
53800
54386
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
53801
54387
  function defaultUpdateChannel(version2) {
@@ -53838,7 +54424,7 @@ async function loadUpdateState(currentVersion, file2 = updateStateFile()) {
53838
54424
  async function saveUpdateState(state, file2 = updateStateFile()) {
53839
54425
  const directory = path9.dirname(file2);
53840
54426
  const temporary = `${file2}.${process.pid}.tmp`;
53841
- await mkdir4(directory, { recursive: true, mode: 448 });
54427
+ await mkdir5(directory, { recursive: true, mode: 448 });
53842
54428
  try {
53843
54429
  await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
53844
54430
  `, {
@@ -53876,14 +54462,14 @@ function isUpdateCheckDue(lastCheckedAt, now = Date.now()) {
53876
54462
  }
53877
54463
 
53878
54464
  // src/usage-store.ts
53879
- import { appendFile, chmod as chmod5, mkdir as mkdir5, readFile as readFile5 } from "node:fs/promises";
54465
+ import { appendFile, chmod as chmod5, mkdir as mkdir6, readFile as readFile5 } from "node:fs/promises";
53880
54466
  import path10 from "node:path";
53881
54467
  function usageFile() {
53882
54468
  return path10.join(configDirectory(), "usage.jsonl");
53883
54469
  }
53884
54470
  async function recordUsageEvent(event, file2 = usageFile()) {
53885
54471
  const directory = path10.dirname(file2);
53886
- await mkdir5(directory, { recursive: true, mode: 448 });
54472
+ await mkdir6(directory, { recursive: true, mode: 448 });
53887
54473
  await appendFile(file2, `${JSON.stringify(event)}
53888
54474
  `, {
53889
54475
  encoding: "utf8",
@@ -53927,7 +54513,7 @@ async function selectExposureRoot(explicitPath, cwd2 = process.cwd()) {
53927
54513
 
53928
54514
  // src/worker/workspace-lease.ts
53929
54515
  import { createHash as createHash3 } from "node:crypto";
53930
- import { lstat as lstat3, mkdir as mkdir6, rm as rm6, stat as stat3 } from "node:fs/promises";
54516
+ import { lstat as lstat3, mkdir as mkdir7, rm as rm6, stat as stat3 } from "node:fs/promises";
53931
54517
  import net from "node:net";
53932
54518
  import path12 from "node:path";
53933
54519
  import { setTimeout as delay3 } from "node:timers/promises";
@@ -53958,7 +54544,7 @@ function defaultLeaseDirectory() {
53958
54544
  return path12.join(accountHomeDirectory(), ".glossa-workspace-leases");
53959
54545
  }
53960
54546
  async function ensureLeaseDirectory(directory) {
53961
- await mkdir6(directory, { recursive: true, mode: 448 });
54547
+ await mkdir7(directory, { recursive: true, mode: 448 });
53962
54548
  const directoryStat = await lstat3(directory);
53963
54549
  if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
53964
54550
  throw new Error("The Glossa workspace lease location is not a private directory.");
@@ -53982,7 +54568,7 @@ async function acquireGuard(directory, identity) {
53982
54568
  const deadline = Date.now() + GUARD_TIMEOUT_MS;
53983
54569
  while (true) {
53984
54570
  try {
53985
- await mkdir6(guard, { mode: 448 });
54571
+ await mkdir7(guard, { mode: 448 });
53986
54572
  return async () => await rm6(guard, { recursive: true, force: true });
53987
54573
  } catch (error46) {
53988
54574
  if (error46.code !== "EEXIST") throw error46;
@@ -54077,10 +54663,10 @@ async function acquireWorkspaceLease(root, options = {}) {
54077
54663
  };
54078
54664
  } catch (error46) {
54079
54665
  await closeServer(server);
54080
- if (error46.code !== "EADDRINUSE") throw error46;
54081
54666
  if (await endpointIsActive(endpoint3)) {
54082
54667
  throw new WorkspaceAlreadyActiveError();
54083
54668
  }
54669
+ if (error46.code !== "EADDRINUSE") throw error46;
54084
54670
  if (process.platform !== "win32") await rm6(endpoint3, { force: true });
54085
54671
  }
54086
54672
  }
@@ -54091,7 +54677,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54091
54677
  }
54092
54678
 
54093
54679
  // src/main.ts
54094
- var VERSION = "0.1.1";
54680
+ var VERSION = "0.2.0-beta.1";
54095
54681
  var DISTRIBUTION = "npm";
54096
54682
  var HELP = `Glossa ${VERSION}
54097
54683