@earendil-works/pi-durable 1.0.2 → 1.0.3

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 (74) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +15 -1
  3. package/dist/env/decode.d.ts +16 -0
  4. package/dist/env/decode.d.ts.map +1 -0
  5. package/dist/env/decode.js +28 -0
  6. package/dist/env/decode.js.map +1 -0
  7. package/dist/env/index.d.ts +155 -3
  8. package/dist/env/index.d.ts.map +1 -1
  9. package/dist/env/index.js +2 -0
  10. package/dist/env/index.js.map +1 -1
  11. package/dist/env/line-scan.d.ts +13 -0
  12. package/dist/env/line-scan.d.ts.map +1 -0
  13. package/dist/env/line-scan.js +147 -0
  14. package/dist/env/line-scan.js.map +1 -0
  15. package/dist/env/node-watch.d.ts +29 -0
  16. package/dist/env/node-watch.d.ts.map +1 -0
  17. package/dist/env/node-watch.js +431 -0
  18. package/dist/env/node-watch.js.map +1 -0
  19. package/dist/env/node.d.ts +11 -2
  20. package/dist/env/node.d.ts.map +1 -1
  21. package/dist/env/node.js +284 -21
  22. package/dist/env/node.js.map +1 -1
  23. package/dist/harness/agent.d.ts +2 -1
  24. package/dist/harness/agent.d.ts.map +1 -1
  25. package/dist/harness/agent.js +5 -0
  26. package/dist/harness/agent.js.map +1 -1
  27. package/dist/harness/generation.d.ts.map +1 -1
  28. package/dist/harness/generation.js +6 -5
  29. package/dist/harness/generation.js.map +1 -1
  30. package/dist/harness/output.d.ts +13 -6
  31. package/dist/harness/output.d.ts.map +1 -1
  32. package/dist/harness/output.js +61 -16
  33. package/dist/harness/output.js.map +1 -1
  34. package/dist/harness/tool.d.ts.map +1 -1
  35. package/dist/harness/tool.js +12 -4
  36. package/dist/harness/tool.js.map +1 -1
  37. package/dist/harness/types.d.ts +24 -3
  38. package/dist/harness/types.d.ts.map +1 -1
  39. package/dist/harness/types.js.map +1 -1
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/testing/env-conformance.d.ts +7 -0
  45. package/dist/testing/env-conformance.d.ts.map +1 -0
  46. package/dist/testing/env-conformance.js +397 -0
  47. package/dist/testing/env-conformance.js.map +1 -0
  48. package/dist/testing/index.d.ts +3 -2
  49. package/dist/testing/index.d.ts.map +1 -1
  50. package/dist/testing/index.js +2 -1
  51. package/dist/testing/index.js.map +1 -1
  52. package/dist/testing/runner.d.ts +4 -2
  53. package/dist/testing/runner.d.ts.map +1 -1
  54. package/dist/testing/runner.js +9 -0
  55. package/dist/testing/runner.js.map +1 -1
  56. package/dist/testing/types.d.ts +18 -0
  57. package/dist/testing/types.d.ts.map +1 -1
  58. package/dist/testing/types.js.map +1 -1
  59. package/dist/tools/bash.d.ts +2 -1
  60. package/dist/tools/bash.d.ts.map +1 -1
  61. package/dist/tools/bash.js +5 -2
  62. package/dist/tools/bash.js.map +1 -1
  63. package/dist/tools/image.d.ts +10 -0
  64. package/dist/tools/image.d.ts.map +1 -1
  65. package/dist/tools/image.js +39 -0
  66. package/dist/tools/image.js.map +1 -1
  67. package/dist/tools/read.d.ts.map +1 -1
  68. package/dist/tools/read.js +148 -75
  69. package/dist/tools/read.js.map +1 -1
  70. package/dist/truncate.d.ts +9 -0
  71. package/dist/truncate.d.ts.map +1 -1
  72. package/dist/truncate.js +12 -4
  73. package/dist/truncate.js.map +1 -1
  74. package/package.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import { createExpectAssertions } from "./assertions.js";
2
+ import { createEnvConformance } from "./env-conformance.js";
2
3
  import { createStorageConformance } from "./storage-conformance.js";
3
4
  /** Registers the runner-independent cases with a Vitest/Jest-compatible test runner. */
4
5
  export function registerStorageConformance(runner, name, withStorage) {
@@ -11,4 +12,12 @@ export function registerStorageConformance(runner, name, withStorage) {
11
12
  runner.it(testCase.name, testCase.run);
12
13
  });
13
14
  }
15
+ /** Registers the runner-independent `ExecutionEnv` cases with a Vitest/Jest-compatible test runner. */
16
+ export function registerEnvConformance(runner, name, withEnv, options = {}) {
17
+ const cases = createEnvConformance({ ...options, assertions: createExpectAssertions(runner.expect), withEnv });
18
+ runner.describe(name, () => {
19
+ for (const testCase of cases)
20
+ runner.it(testCase.name, testCase.run, testCase.timeoutMs);
21
+ });
22
+ }
14
23
  //# sourceMappingURL=runner.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/testing/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAmB,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AASpE,wFAAwF;AACxF,MAAM,UAAU,0BAA0B,CACzC,MAAgC,EAChC,IAAY,EACZ,WAAuC;IAEvC,MAAM,KAAK,GAAG,wBAAwB,CAAC;QACtC,UAAU,EAAE,sBAAsB,CAAC,MAAM,CAAC,MAAM,CAAC;QACjD,WAAW;KACX,CAAC,CAAC;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE;QAC1B,KAAK,MAAM,QAAQ,IAAI,KAAK;YAAE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { createExpectAssertions, type ExpectLike } from \"./assertions.ts\";\nimport { createStorageConformance } from \"./storage-conformance.ts\";\nimport type { StorageConformanceProvider } from \"./types.ts\";\n\nexport interface StorageConformanceRunner {\n\treadonly describe: (name: string, suite: () => void) => unknown;\n\treadonly expect: ExpectLike;\n\treadonly it: (name: string, test: () => Promise<void>) => unknown;\n}\n\n/** Registers the runner-independent cases with a Vitest/Jest-compatible test runner. */\nexport function registerStorageConformance(\n\trunner: StorageConformanceRunner,\n\tname: string,\n\twithStorage: StorageConformanceProvider,\n): void {\n\tconst cases = createStorageConformance({\n\t\tassertions: createExpectAssertions(runner.expect),\n\t\twithStorage,\n\t});\n\trunner.describe(name, () => {\n\t\tfor (const testCase of cases) runner.it(testCase.name, testCase.run);\n\t});\n}\n"]}
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/testing/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAmB,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AASpE,wFAAwF;AACxF,MAAM,UAAU,0BAA0B,CACzC,MAAgC,EAChC,IAAY,EACZ,WAAuC;IAEvC,MAAM,KAAK,GAAG,wBAAwB,CAAC;QACtC,UAAU,EAAE,sBAAsB,CAAC,MAAM,CAAC,MAAM,CAAC;QACjD,WAAW;KACX,CAAC,CAAC;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE;QAC1B,KAAK,MAAM,QAAQ,IAAI,KAAK;YAAE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,sBAAsB,CACrC,MAAgC,EAChC,IAAY,EACZ,OAA+B,EAC/B,OAAO,GAAsD,EAAE;IAE/D,MAAM,KAAK,GAAG,oBAAoB,CAAC,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,sBAAsB,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;IAC/G,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE;QAC1B,KAAK,MAAM,QAAQ,IAAI,KAAK;YAAE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { createExpectAssertions, type ExpectLike } from \"./assertions.ts\";\nimport { createEnvConformance } from \"./env-conformance.ts\";\nimport { createStorageConformance } from \"./storage-conformance.ts\";\nimport type { EnvConformanceOptions, EnvConformanceProvider, StorageConformanceProvider } from \"./types.ts\";\n\nexport interface StorageConformanceRunner {\n\treadonly describe: (name: string, suite: () => void) => unknown;\n\treadonly expect: ExpectLike;\n\treadonly it: (name: string, test: () => Promise<void>, timeoutMs?: number) => unknown;\n}\n\n/** Registers the runner-independent cases with a Vitest/Jest-compatible test runner. */\nexport function registerStorageConformance(\n\trunner: StorageConformanceRunner,\n\tname: string,\n\twithStorage: StorageConformanceProvider,\n): void {\n\tconst cases = createStorageConformance({\n\t\tassertions: createExpectAssertions(runner.expect),\n\t\twithStorage,\n\t});\n\trunner.describe(name, () => {\n\t\tfor (const testCase of cases) runner.it(testCase.name, testCase.run);\n\t});\n}\n\n/** Registers the runner-independent `ExecutionEnv` cases with a Vitest/Jest-compatible test runner. */\nexport function registerEnvConformance(\n\trunner: StorageConformanceRunner,\n\tname: string,\n\twithEnv: EnvConformanceProvider,\n\toptions: Pick<EnvConformanceOptions, \"shell\" | \"symlinks\"> = {},\n): void {\n\tconst cases = createEnvConformance({ ...options, assertions: createExpectAssertions(runner.expect), withEnv });\n\trunner.describe(name, () => {\n\t\tfor (const testCase of cases) runner.it(testCase.name, testCase.run, testCase.timeoutMs);\n\t});\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import type { ExecutionEnv } from "../env/index.ts";
1
2
  import type { Storage } from "../types.ts";
2
3
  export interface StorageConformanceAssertions {
3
4
  ok(value: unknown, message?: string): void;
@@ -16,4 +17,21 @@ export interface StorageConformanceCase {
16
17
  readonly name: string;
17
18
  run(): Promise<void>;
18
19
  }
20
+ export type EnvConformanceAssertions = StorageConformanceAssertions;
21
+ /** Calls `use` once with an environment whose `cwd` is a fresh, empty, writable directory, then cleans up. */
22
+ export type EnvConformanceProvider = (use: (env: ExecutionEnv) => Promise<void>) => Promise<void>;
23
+ export interface EnvConformanceOptions {
24
+ readonly assertions: EnvConformanceAssertions;
25
+ readonly withEnv: EnvConformanceProvider;
26
+ /** Program and flag that run a POSIX shell script from the next argument; default `["sh", "-c"]`. */
27
+ readonly shell?: readonly string[];
28
+ /** Whether the shell's `ln -s` creates symbolic links; default true. */
29
+ readonly symlinks?: boolean;
30
+ }
31
+ export interface EnvConformanceCase {
32
+ readonly name: string;
33
+ /** Cases that wait for an environment's watch latency need longer than a test runner's default timeout. */
34
+ readonly timeoutMs?: number;
35
+ run(): Promise<void>;
36
+ }
19
37
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/testing/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,4BAA4B;IAC5C,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACtD,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACpD,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3D,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACpD,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7E;AAED,MAAM,MAAM,0BAA0B,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAErG,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,UAAU,EAAE,4BAA4B,CAAC;IAClD,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAAC;CACjD;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/testing/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,4BAA4B;IAC5C,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACtD,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACpD,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3D,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACpD,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7E;AAED,MAAM,MAAM,0BAA0B,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAErG,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,UAAU,EAAE,4BAA4B,CAAC;IAClD,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAAC;CACjD;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAED,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,CAAC;AAEpE,8GAA8G;AAC9G,MAAM,MAAM,sBAAsB,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAElG,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;IACzC,qGAAqG;IACrG,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2GAA2G;IAC3G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/testing/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Storage } from \"../types.ts\";\n\nexport interface StorageConformanceAssertions {\n\tok(value: unknown, message?: string): void;\n\tstrictEqual(actual: unknown, expected: unknown): void;\n\tdeepEqual(actual: unknown, expected: unknown): void;\n\tpartialDeepEqual(actual: unknown, expected: unknown): void;\n\tgreaterThan(actual: number, expected: number): void;\n\trejects(operation: Promise<unknown>, messageIncludes: string): Promise<void>;\n}\n\nexport type StorageConformanceProvider = (use: (storage: Storage) => Promise<void>) => Promise<void>;\n\nexport interface StorageConformanceOptions {\n\treadonly assertions: StorageConformanceAssertions;\n\treadonly withStorage: StorageConformanceProvider;\n}\n\nexport interface StorageConformanceCase {\n\treadonly name: string;\n\trun(): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/testing/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ExecutionEnv } from \"../env/index.ts\";\nimport type { Storage } from \"../types.ts\";\n\nexport interface StorageConformanceAssertions {\n\tok(value: unknown, message?: string): void;\n\tstrictEqual(actual: unknown, expected: unknown): void;\n\tdeepEqual(actual: unknown, expected: unknown): void;\n\tpartialDeepEqual(actual: unknown, expected: unknown): void;\n\tgreaterThan(actual: number, expected: number): void;\n\trejects(operation: Promise<unknown>, messageIncludes: string): Promise<void>;\n}\n\nexport type StorageConformanceProvider = (use: (storage: Storage) => Promise<void>) => Promise<void>;\n\nexport interface StorageConformanceOptions {\n\treadonly assertions: StorageConformanceAssertions;\n\treadonly withStorage: StorageConformanceProvider;\n}\n\nexport interface StorageConformanceCase {\n\treadonly name: string;\n\trun(): Promise<void>;\n}\n\nexport type EnvConformanceAssertions = StorageConformanceAssertions;\n\n/** Calls `use` once with an environment whose `cwd` is a fresh, empty, writable directory, then cleans up. */\nexport type EnvConformanceProvider = (use: (env: ExecutionEnv) => Promise<void>) => Promise<void>;\n\nexport interface EnvConformanceOptions {\n\treadonly assertions: EnvConformanceAssertions;\n\treadonly withEnv: EnvConformanceProvider;\n\t/** Program and flag that run a POSIX shell script from the next argument; default `[\"sh\", \"-c\"]`. */\n\treadonly shell?: readonly string[];\n\t/** Whether the shell's `ln -s` creates symbolic links; default true. */\n\treadonly symlinks?: boolean;\n}\n\nexport interface EnvConformanceCase {\n\treadonly name: string;\n\t/** Cases that wait for an environment's watch latency need longer than a test runner's default timeout. */\n\treadonly timeoutMs?: number;\n\trun(): Promise<void>;\n}\n"]}
@@ -19,7 +19,8 @@ export interface BashToolOptions {
19
19
  }
20
20
  /**
21
21
  * Runs a command through the environment's shell. Its output streams to `api.output()`, where the Harness keeps the
22
- * tail within the default limits; the result content is that retained output. Output beyond the limits is spilled to a
22
+ * tail within the default limits; the result content is that retained output. The retained window goes to the
23
+ * environment, which may omit output outside it and report how much it omitted, so dropped counts stay exact. Output beyond the limits is spilled to a
23
24
  * file whose path is reported as a diagnostic. A nonzero exit or timeout throws, which makes an error result that still
24
25
  * carries the output and diagnostics.
25
26
  */
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAM9E,QAAA,MAAM,UAAU;;;EAGd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,UAAU,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEtH,MAAM,WAAW,eAAe;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,WAAW,CAAC;CACtB;AAYD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,gBAAgB,CAAC,OAAO,UAAU,CAAC,CA2C7F"}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAM9E,QAAA,MAAM,UAAU;;;EAGd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,UAAU,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEtH,MAAM,WAAW,eAAe;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,WAAW,CAAC;CACtB;AAYD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,gBAAgB,CAAC,OAAO,UAAU,CAAC,CA6C7F"}
@@ -19,7 +19,8 @@ function validateTimeout(timeout) {
19
19
  }
20
20
  /**
21
21
  * Runs a command through the environment's shell. Its output streams to `api.output()`, where the Harness keeps the
22
- * tail within the default limits; the result content is that retained output. Output beyond the limits is spilled to a
22
+ * tail within the default limits; the result content is that retained output. The retained window goes to the
23
+ * environment, which may omit output outside it and report how much it omitted, so dropped counts stay exact. Output beyond the limits is spilled to a
23
24
  * file whose path is reported as a diagnostic. A nonzero exit or timeout throws, which makes an error result that still
24
25
  * carries the output and diagnostics.
25
26
  */
@@ -45,8 +46,10 @@ export function createBashTool(options) {
45
46
  env: execution.env,
46
47
  inheritEnv: execution.inheritEnv,
47
48
  ...(timeout === undefined ? {} : { timeout }),
48
- onOutput: (text) => api.output(text),
49
+ onOutput: (text, _context, info) => api.output(text, info.skipped),
49
50
  spill: { afterBytes: DEFAULT_MAX_BYTES, afterLines: DEFAULT_MAX_LINES },
51
+ // An environment may then omit output outside the retained tail and report the omission.
52
+ ...(api.outputWindow === undefined ? {} : { window: api.outputWindow }),
50
53
  }, context);
51
54
  const spillPath = result.ok ? result.value.spillPath : result.error.spillPath;
52
55
  if (spillPath !== undefined) {
@@ -1 +1 @@
1
- {"version":3,"file":"bash.js","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context } from \"@earendil-works/chord\";\nimport { type Static, Type } from \"typebox\";\nimport { defineTool } from \"../harness/define.ts\";\nimport type { ToolExecutionApi, ToolRegistration } from \"../harness/types.ts\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from \"../truncate.ts\";\nimport { requireEnv } from \"./env.ts\";\n\nconst MAX_TIMEOUT_SECONDS = 2_147_483_647 / 1000;\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface BashExecution {\n\tcommand: string;\n\tcwd: string;\n\tenv: Record<string, string>;\n\tinheritEnv: boolean;\n}\n\nexport type BashPrepare = (execution: BashExecution, api: ToolExecutionApi, context: Context) => void | Promise<void>;\n\nexport interface BashToolOptions {\n\tcommandPrefix?: string;\n\tprepare?: BashPrepare;\n}\n\nfunction validateTimeout(timeout: number | undefined): void {\n\tif (timeout === undefined) return;\n\tif (!Number.isFinite(timeout) || timeout <= 0) {\n\t\tthrow new Error(\"Invalid timeout: must be a finite number of seconds\");\n\t}\n\tif (timeout > MAX_TIMEOUT_SECONDS) {\n\t\tthrow new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);\n\t}\n}\n\n/**\n * Runs a command through the environment's shell. Its output streams to `api.output()`, where the Harness keeps the\n * tail within the default limits; the result content is that retained output. Output beyond the limits is spilled to a\n * file whose path is reported as a diagnostic. A nonzero exit or timeout throws, which makes an error result that still\n * carries the output and diagnostics.\n */\nexport function createBashTool(options?: BashToolOptions): ToolRegistration<typeof bashSchema> {\n\treturn defineTool({\n\t\tname: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns combined stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tparameters: bashSchema,\n\t\toutputLimits: { retain: \"tail\" },\n\t\tasync execute(args, api, context) {\n\t\t\tconst { command, timeout } = args;\n\t\t\tvalidateTimeout(timeout);\n\t\t\tconst env = requireEnv(api);\n\t\t\tconst execution: BashExecution = {\n\t\t\t\tcommand: options?.commandPrefix ? `${options.commandPrefix}\\n${command}` : command,\n\t\t\t\tcwd: env.cwd,\n\t\t\t\tenv: {},\n\t\t\t\tinheritEnv: true,\n\t\t\t};\n\t\t\tawait options?.prepare?.(execution, api, context);\n\t\t\tconst result = await env.exec(\n\t\t\t\texecution.command,\n\t\t\t\t{\n\t\t\t\t\tcwd: execution.cwd,\n\t\t\t\t\tenv: execution.env,\n\t\t\t\t\tinheritEnv: execution.inheritEnv,\n\t\t\t\t\t...(timeout === undefined ? {} : { timeout }),\n\t\t\t\t\tonOutput: (text) => api.output(text),\n\t\t\t\t\tspill: { afterBytes: DEFAULT_MAX_BYTES, afterLines: DEFAULT_MAX_LINES },\n\t\t\t\t},\n\t\t\t\tcontext,\n\t\t\t);\n\t\t\tconst spillPath = result.ok ? result.value.spillPath : result.error.spillPath;\n\t\t\tif (spillPath !== undefined) {\n\t\t\t\tapi.diagnostic({ severity: \"info\", code: \"full_output\", message: `Full output: ${spillPath}` });\n\t\t\t}\n\t\t\tif (!result.ok) {\n\t\t\t\tif (result.error.code === \"aborted\" && context.abortSignal?.aborted) throw result.error;\n\t\t\t\tif (result.error.code === \"timeout\") throw new Error(`Command timed out after ${timeout} seconds`);\n\t\t\t\tif (result.error.code === \"aborted\") throw new Error(\"Command aborted\");\n\t\t\t\tthrow result.error;\n\t\t\t}\n\t\t\tif (result.value.exitCode !== 0) throw new Error(`Command exited with code ${result.value.exitCode}`);\n\t\t\treturn {};\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"bash.js","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context } from \"@earendil-works/chord\";\nimport { type Static, Type } from \"typebox\";\nimport { defineTool } from \"../harness/define.ts\";\nimport type { ToolExecutionApi, ToolRegistration } from \"../harness/types.ts\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from \"../truncate.ts\";\nimport { requireEnv } from \"./env.ts\";\n\nconst MAX_TIMEOUT_SECONDS = 2_147_483_647 / 1000;\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport type BashToolInput = Static<typeof bashSchema>;\n\nexport interface BashExecution {\n\tcommand: string;\n\tcwd: string;\n\tenv: Record<string, string>;\n\tinheritEnv: boolean;\n}\n\nexport type BashPrepare = (execution: BashExecution, api: ToolExecutionApi, context: Context) => void | Promise<void>;\n\nexport interface BashToolOptions {\n\tcommandPrefix?: string;\n\tprepare?: BashPrepare;\n}\n\nfunction validateTimeout(timeout: number | undefined): void {\n\tif (timeout === undefined) return;\n\tif (!Number.isFinite(timeout) || timeout <= 0) {\n\t\tthrow new Error(\"Invalid timeout: must be a finite number of seconds\");\n\t}\n\tif (timeout > MAX_TIMEOUT_SECONDS) {\n\t\tthrow new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);\n\t}\n}\n\n/**\n * Runs a command through the environment's shell. Its output streams to `api.output()`, where the Harness keeps the\n * tail within the default limits; the result content is that retained output. The retained window goes to the\n * environment, which may omit output outside it and report how much it omitted, so dropped counts stay exact. Output beyond the limits is spilled to a\n * file whose path is reported as a diagnostic. A nonzero exit or timeout throws, which makes an error result that still\n * carries the output and diagnostics.\n */\nexport function createBashTool(options?: BashToolOptions): ToolRegistration<typeof bashSchema> {\n\treturn defineTool({\n\t\tname: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns combined stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tparameters: bashSchema,\n\t\toutputLimits: { retain: \"tail\" },\n\t\tasync execute(args, api, context) {\n\t\t\tconst { command, timeout } = args;\n\t\t\tvalidateTimeout(timeout);\n\t\t\tconst env = requireEnv(api);\n\t\t\tconst execution: BashExecution = {\n\t\t\t\tcommand: options?.commandPrefix ? `${options.commandPrefix}\\n${command}` : command,\n\t\t\t\tcwd: env.cwd,\n\t\t\t\tenv: {},\n\t\t\t\tinheritEnv: true,\n\t\t\t};\n\t\t\tawait options?.prepare?.(execution, api, context);\n\t\t\tconst result = await env.exec(\n\t\t\t\texecution.command,\n\t\t\t\t{\n\t\t\t\t\tcwd: execution.cwd,\n\t\t\t\t\tenv: execution.env,\n\t\t\t\t\tinheritEnv: execution.inheritEnv,\n\t\t\t\t\t...(timeout === undefined ? {} : { timeout }),\n\t\t\t\t\tonOutput: (text, _context, info) => api.output(text, info.skipped),\n\t\t\t\t\tspill: { afterBytes: DEFAULT_MAX_BYTES, afterLines: DEFAULT_MAX_LINES },\n\t\t\t\t\t// An environment may then omit output outside the retained tail and report the omission.\n\t\t\t\t\t...(api.outputWindow === undefined ? {} : { window: api.outputWindow }),\n\t\t\t\t},\n\t\t\t\tcontext,\n\t\t\t);\n\t\t\tconst spillPath = result.ok ? result.value.spillPath : result.error.spillPath;\n\t\t\tif (spillPath !== undefined) {\n\t\t\t\tapi.diagnostic({ severity: \"info\", code: \"full_output\", message: `Full output: ${spillPath}` });\n\t\t\t}\n\t\t\tif (!result.ok) {\n\t\t\t\tif (result.error.code === \"aborted\" && context.abortSignal?.aborted) throw result.error;\n\t\t\t\tif (result.error.code === \"timeout\") throw new Error(`Command timed out after ${timeout} seconds`);\n\t\t\t\tif (result.error.code === \"aborted\") throw new Error(\"Command aborted\");\n\t\t\t\tthrow result.error;\n\t\t\t}\n\t\t\tif (result.value.exitCode !== 0) throw new Error(`Command exited with code ${result.value.exitCode}`);\n\t\t\treturn {};\n\t\t},\n\t});\n}\n"]}
@@ -1,2 +1,12 @@
1
+ /** Positional reads of a file of `size` bytes. */
2
+ export type ByteSource = {
3
+ readonly size: number;
4
+ read(offset: number, length: number): Promise<Uint8Array>;
5
+ };
6
+ /**
7
+ * `detectSupportedImageMimeType` of a whole file, reading only its header and, for PNG, the chunk headers up to the
8
+ * first `acTL` or `IDAT`.
9
+ */
10
+ export declare function detectSupportedImageMimeTypeOf(source: ByteSource): Promise<string | undefined>;
1
11
  export declare function detectSupportedImageMimeType(buffer: Uint8Array): string | undefined;
2
12
  //# sourceMappingURL=image.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/tools/image.ts"],"names":[],"mappings":"AAEA,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAOnF"}
1
+ {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/tools/image.ts"],"names":[],"mappings":"AAKA,kDAAkD;AAClD,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAC1D,CAAC;AAEF;;;GAGG;AACH,wBAAsB,8BAA8B,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIpG;AA0BD,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAOnF"}
@@ -1,4 +1,43 @@
1
1
  const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
2
+ /** Bytes every check except the APNG chunk walk needs: BMP reads up to offset 29. */
3
+ const HEADER_BYTES = 32;
4
+ const BLOCK_BYTES = 64 * 1024;
5
+ /**
6
+ * `detectSupportedImageMimeType` of a whole file, reading only its header and, for PNG, the chunk headers up to the
7
+ * first `acTL` or `IDAT`.
8
+ */
9
+ export async function detectSupportedImageMimeTypeOf(source) {
10
+ const header = await source.read(0, HEADER_BYTES);
11
+ if (!startsWith(header, PNG_SIGNATURE))
12
+ return detectSupportedImageMimeType(header);
13
+ return isPng(header) && !(await isAnimatedPngOf(source)) ? "image/png" : undefined;
14
+ }
15
+ /** `isAnimatedPng` over a file read in blocks. */
16
+ async function isAnimatedPngOf(source) {
17
+ let block = new Uint8Array(0);
18
+ let blockStart = 0;
19
+ const bytesAt = async (offset, length) => {
20
+ if (offset < blockStart || offset + length > blockStart + block.length) {
21
+ blockStart = offset;
22
+ block = await source.read(offset, BLOCK_BYTES);
23
+ }
24
+ return block.subarray(offset - blockStart, offset - blockStart + length);
25
+ };
26
+ let offset = PNG_SIGNATURE.length;
27
+ while (offset + 8 <= source.size) {
28
+ const chunkHeader = await bytesAt(offset, 8);
29
+ const chunkLength = readUint32BE(chunkHeader, 0);
30
+ if (startsWithAscii(chunkHeader, 4, "acTL"))
31
+ return true;
32
+ if (startsWithAscii(chunkHeader, 4, "IDAT"))
33
+ return false;
34
+ const nextOffset = offset + 8 + chunkLength + 4;
35
+ if (nextOffset <= offset || nextOffset > source.size)
36
+ return false;
37
+ offset = nextOffset;
38
+ }
39
+ return false;
40
+ }
2
41
  export function detectSupportedImageMimeType(buffer) {
3
42
  if (startsWith(buffer, [0xff, 0xd8, 0xff]))
4
43
  return buffer[3] === 0xf7 ? undefined : "image/jpeg";
@@ -1 +1 @@
1
- {"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/tools/image.ts"],"names":[],"mappings":"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","sourcesContent":["const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\n\nexport function detectSupportedImageMimeType(buffer: Uint8Array): string | undefined {\n\tif (startsWith(buffer, [0xff, 0xd8, 0xff])) return buffer[3] === 0xf7 ? undefined : \"image/jpeg\";\n\tif (startsWith(buffer, PNG_SIGNATURE)) return isPng(buffer) && !isAnimatedPng(buffer) ? \"image/png\" : undefined;\n\tif (startsWithAscii(buffer, 0, \"GIF87a\") || startsWithAscii(buffer, 0, \"GIF89a\")) return \"image/gif\";\n\tif (startsWithAscii(buffer, 0, \"RIFF\") && startsWithAscii(buffer, 8, \"WEBP\")) return \"image/webp\";\n\tif (startsWithAscii(buffer, 0, \"BM\") && isBmp(buffer)) return \"image/bmp\";\n\treturn undefined;\n}\n\nfunction isPng(buffer: Uint8Array): boolean {\n\treturn (\n\t\tbuffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, \"IHDR\")\n\t);\n}\n\nfunction isAnimatedPng(buffer: Uint8Array): boolean {\n\tlet offset = PNG_SIGNATURE.length;\n\twhile (offset + 8 <= buffer.length) {\n\t\tconst chunkLength = readUint32BE(buffer, offset);\n\t\tconst chunkTypeOffset = offset + 4;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"acTL\")) return true;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"IDAT\")) return false;\n\t\tconst nextOffset = offset + 8 + chunkLength + 4;\n\t\tif (nextOffset <= offset || nextOffset > buffer.length) return false;\n\t\toffset = nextOffset;\n\t}\n\treturn false;\n}\n\nfunction isBmp(buffer: Uint8Array): boolean {\n\tif (buffer.length < 26) return false;\n\tconst declaredFileSize = readUint32LE(buffer, 2);\n\tconst pixelDataOffset = readUint32LE(buffer, 10);\n\tconst dibHeaderSize = readUint32LE(buffer, 14);\n\tif (declaredFileSize !== 0 && declaredFileSize < 26) return false;\n\tif (pixelDataOffset < 14 + dibHeaderSize) return false;\n\tif (declaredFileSize !== 0 && pixelDataOffset >= declaredFileSize) return false;\n\n\tlet colorPlanes: number;\n\tlet bitsPerPixel: number;\n\tif (dibHeaderSize === 12) {\n\t\tcolorPlanes = readUint16LE(buffer, 22);\n\t\tbitsPerPixel = readUint16LE(buffer, 24);\n\t} else if (dibHeaderSize >= 40 && dibHeaderSize <= 124) {\n\t\tif (buffer.length < 30) return false;\n\t\tcolorPlanes = readUint16LE(buffer, 26);\n\t\tbitsPerPixel = readUint16LE(buffer, 28);\n\t} else {\n\t\treturn false;\n\t}\n\treturn colorPlanes === 1 && [1, 4, 8, 16, 24, 32].includes(bitsPerPixel);\n}\n\nfunction readUint16LE(buffer: Uint8Array, offset: number): number {\n\treturn (buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8);\n}\n\nfunction readUint32BE(buffer: Uint8Array, offset: number): number {\n\treturn (\n\t\t(buffer[offset] ?? 0) * 0x1000000 +\n\t\t((buffer[offset + 1] ?? 0) << 16) +\n\t\t((buffer[offset + 2] ?? 0) << 8) +\n\t\t(buffer[offset + 3] ?? 0)\n\t);\n}\n\nfunction readUint32LE(buffer: Uint8Array, offset: number): number {\n\treturn (\n\t\t(buffer[offset] ?? 0) +\n\t\t((buffer[offset + 1] ?? 0) << 8) +\n\t\t((buffer[offset + 2] ?? 0) << 16) +\n\t\t(buffer[offset + 3] ?? 0) * 0x1000000\n\t);\n}\n\nfunction startsWith(buffer: Uint8Array, bytes: number[]): boolean {\n\tif (buffer.length < bytes.length) return false;\n\treturn bytes.every((byte, index) => buffer[index] === byte);\n}\n\nfunction startsWithAscii(buffer: Uint8Array, offset: number, text: string): boolean {\n\tif (buffer.length < offset + text.length) return false;\n\tfor (let index = 0; index < text.length; index++) {\n\t\tif (buffer[offset + index] !== text.charCodeAt(index)) return false;\n\t}\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/tools/image.ts"],"names":[],"mappings":"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","sourcesContent":["const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\n/** Bytes every check except the APNG chunk walk needs: BMP reads up to offset 29. */\nconst HEADER_BYTES = 32;\nconst BLOCK_BYTES = 64 * 1024;\n\n/** Positional reads of a file of `size` bytes. */\nexport type ByteSource = {\n\treadonly size: number;\n\tread(offset: number, length: number): Promise<Uint8Array>;\n};\n\n/**\n * `detectSupportedImageMimeType` of a whole file, reading only its header and, for PNG, the chunk headers up to the\n * first `acTL` or `IDAT`.\n */\nexport async function detectSupportedImageMimeTypeOf(source: ByteSource): Promise<string | undefined> {\n\tconst header = await source.read(0, HEADER_BYTES);\n\tif (!startsWith(header, PNG_SIGNATURE)) return detectSupportedImageMimeType(header);\n\treturn isPng(header) && !(await isAnimatedPngOf(source)) ? \"image/png\" : undefined;\n}\n\n/** `isAnimatedPng` over a file read in blocks. */\nasync function isAnimatedPngOf(source: ByteSource): Promise<boolean> {\n\tlet block: Uint8Array = new Uint8Array(0);\n\tlet blockStart = 0;\n\tconst bytesAt = async (offset: number, length: number): Promise<Uint8Array> => {\n\t\tif (offset < blockStart || offset + length > blockStart + block.length) {\n\t\t\tblockStart = offset;\n\t\t\tblock = await source.read(offset, BLOCK_BYTES);\n\t\t}\n\t\treturn block.subarray(offset - blockStart, offset - blockStart + length);\n\t};\n\tlet offset = PNG_SIGNATURE.length;\n\twhile (offset + 8 <= source.size) {\n\t\tconst chunkHeader = await bytesAt(offset, 8);\n\t\tconst chunkLength = readUint32BE(chunkHeader, 0);\n\t\tif (startsWithAscii(chunkHeader, 4, \"acTL\")) return true;\n\t\tif (startsWithAscii(chunkHeader, 4, \"IDAT\")) return false;\n\t\tconst nextOffset = offset + 8 + chunkLength + 4;\n\t\tif (nextOffset <= offset || nextOffset > source.size) return false;\n\t\toffset = nextOffset;\n\t}\n\treturn false;\n}\n\nexport function detectSupportedImageMimeType(buffer: Uint8Array): string | undefined {\n\tif (startsWith(buffer, [0xff, 0xd8, 0xff])) return buffer[3] === 0xf7 ? undefined : \"image/jpeg\";\n\tif (startsWith(buffer, PNG_SIGNATURE)) return isPng(buffer) && !isAnimatedPng(buffer) ? \"image/png\" : undefined;\n\tif (startsWithAscii(buffer, 0, \"GIF87a\") || startsWithAscii(buffer, 0, \"GIF89a\")) return \"image/gif\";\n\tif (startsWithAscii(buffer, 0, \"RIFF\") && startsWithAscii(buffer, 8, \"WEBP\")) return \"image/webp\";\n\tif (startsWithAscii(buffer, 0, \"BM\") && isBmp(buffer)) return \"image/bmp\";\n\treturn undefined;\n}\n\nfunction isPng(buffer: Uint8Array): boolean {\n\treturn (\n\t\tbuffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, \"IHDR\")\n\t);\n}\n\nfunction isAnimatedPng(buffer: Uint8Array): boolean {\n\tlet offset = PNG_SIGNATURE.length;\n\twhile (offset + 8 <= buffer.length) {\n\t\tconst chunkLength = readUint32BE(buffer, offset);\n\t\tconst chunkTypeOffset = offset + 4;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"acTL\")) return true;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"IDAT\")) return false;\n\t\tconst nextOffset = offset + 8 + chunkLength + 4;\n\t\tif (nextOffset <= offset || nextOffset > buffer.length) return false;\n\t\toffset = nextOffset;\n\t}\n\treturn false;\n}\n\nfunction isBmp(buffer: Uint8Array): boolean {\n\tif (buffer.length < 26) return false;\n\tconst declaredFileSize = readUint32LE(buffer, 2);\n\tconst pixelDataOffset = readUint32LE(buffer, 10);\n\tconst dibHeaderSize = readUint32LE(buffer, 14);\n\tif (declaredFileSize !== 0 && declaredFileSize < 26) return false;\n\tif (pixelDataOffset < 14 + dibHeaderSize) return false;\n\tif (declaredFileSize !== 0 && pixelDataOffset >= declaredFileSize) return false;\n\n\tlet colorPlanes: number;\n\tlet bitsPerPixel: number;\n\tif (dibHeaderSize === 12) {\n\t\tcolorPlanes = readUint16LE(buffer, 22);\n\t\tbitsPerPixel = readUint16LE(buffer, 24);\n\t} else if (dibHeaderSize >= 40 && dibHeaderSize <= 124) {\n\t\tif (buffer.length < 30) return false;\n\t\tcolorPlanes = readUint16LE(buffer, 26);\n\t\tbitsPerPixel = readUint16LE(buffer, 28);\n\t} else {\n\t\treturn false;\n\t}\n\treturn colorPlanes === 1 && [1, 4, 8, 16, 24, 32].includes(bitsPerPixel);\n}\n\nfunction readUint16LE(buffer: Uint8Array, offset: number): number {\n\treturn (buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8);\n}\n\nfunction readUint32BE(buffer: Uint8Array, offset: number): number {\n\treturn (\n\t\t(buffer[offset] ?? 0) * 0x1000000 +\n\t\t((buffer[offset + 1] ?? 0) << 16) +\n\t\t((buffer[offset + 2] ?? 0) << 8) +\n\t\t(buffer[offset + 3] ?? 0)\n\t);\n}\n\nfunction readUint32LE(buffer: Uint8Array, offset: number): number {\n\treturn (\n\t\t(buffer[offset] ?? 0) +\n\t\t((buffer[offset + 1] ?? 0) << 8) +\n\t\t((buffer[offset + 2] ?? 0) << 16) +\n\t\t(buffer[offset + 3] ?? 0) * 0x1000000\n\t);\n}\n\nfunction startsWith(buffer: Uint8Array, bytes: number[]): boolean {\n\tif (buffer.length < bytes.length) return false;\n\treturn bytes.every((byte, index) => buffer[index] === byte);\n}\n\nfunction startsWithAscii(buffer: Uint8Array, offset: number, text: string): boolean {\n\tif (buffer.length < offset + text.length) return false;\n\tfor (let index = 0; index < text.length; index++) {\n\t\tif (buffer[offset + index] !== text.charCodeAt(index)) return false;\n\t}\n\treturn true;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAI5C,OAAO,KAAK,EAAkB,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAAoD,KAAK,gBAAgB,EAAgB,MAAM,gBAAgB,CAAC;AAKvH,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,MAAM,eAAe,GAAG;IAC7B,yEAAyE;IACzE,UAAU,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;CAC/C,CAAC;AAEF,kHAAkH;AAClH,wBAAgB,cAAc,IAAI,gBAAgB,CAAC,OAAO,UAAU,EAAE,eAAe,CAAC,CAsFrF"}
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAW5C,OAAO,KAAK,EAAkB,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EAIN,KAAK,gBAAgB,EAGrB,MAAM,gBAAgB,CAAC;AAKxB,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,MAAM,eAAe,GAAG;IAC7B,yEAAyE;IACzE,UAAU,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;CAC/C,CAAC;AAmCF,kHAAkH;AAClH,wBAAgB,cAAc,IAAI,gBAAgB,CAAC,OAAO,UAAU,EAAE,eAAe,CAAC,CAwBrF"}
@@ -1,16 +1,43 @@
1
1
  import { Type } from "typebox";
2
- import { getOrThrow } from "../env/index.js";
2
+ import { getOrThrow, rangeDecoder, startsWithBom, } from "../env/index.js";
3
3
  import { defineTool } from "../harness/define.js";
4
4
  import { characterEnd } from "../harness/output.js";
5
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "../truncate.js";
5
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHeadOf, utf8ByteLength, } from "../truncate.js";
6
6
  import { requireEnv } from "./env.js";
7
- import { detectSupportedImageMimeType } from "./image.js";
7
+ import { detectSupportedImageMimeTypeOf } from "./image.js";
8
8
  import { resolveReadToolPath } from "./path-utils.js";
9
9
  const readSchema = Type.Object({
10
10
  path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
11
11
  offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
12
12
  limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
13
13
  });
14
+ const READ_CHUNK = 64 * 1024;
15
+ /** `Array.prototype.slice`'s conversion of an index: NaN is 0, other values truncate toward zero. */
16
+ function sliceIndex(value) {
17
+ return Number.isNaN(value) ? 0 : Math.trunc(value);
18
+ }
19
+ /**
20
+ * The decoded start of bytes `[start, end)` of the file, decoded as part of the whole file: all of it, or enough for
21
+ * `truncateHeadOf` (more than `DEFAULT_MAX_BYTES + 1` bytes, or `DEFAULT_MAX_LINES` newlines).
22
+ */
23
+ async function readHead(reader, start, end, skipBom, context) {
24
+ const decoder = rangeDecoder();
25
+ let text = "";
26
+ let newlines = 0;
27
+ for (let position = skipBom && start === 0 ? 3 : start; position < end;) {
28
+ const bytes = getOrThrow(await reader.read(position, Math.min(READ_CHUNK, end - position), context));
29
+ if (bytes.length === 0)
30
+ break;
31
+ position += bytes.length;
32
+ const decoded = decoder.decode(bytes, { stream: true });
33
+ text += decoded;
34
+ for (let index = decoded.indexOf("\n"); index !== -1; index = decoded.indexOf("\n", index + 1))
35
+ newlines++;
36
+ if (newlines >= DEFAULT_MAX_LINES || utf8ByteLength(text) > DEFAULT_MAX_BYTES + 1)
37
+ return text;
38
+ }
39
+ return text + decoder.decode();
40
+ }
14
41
  /** Reads text files. Remarks about truncation and continuation are diagnostics; the content is only file text. */
15
42
  export function createReadTool() {
16
43
  return defineTool({
@@ -21,81 +48,127 @@ export function createReadTool() {
21
48
  const { path, offset, limit } = args;
22
49
  const env = requireEnv(api);
23
50
  const absolutePath = await resolveReadToolPath(env, path, context);
24
- const bytes = getOrThrow(await env.readBinaryFile(absolutePath, context));
25
- const mimeType = detectSupportedImageMimeType(bytes);
26
- if (mimeType) {
27
- // Image content is not supported yet.
28
- return {
29
- content: [],
30
- isError: true,
31
- diagnostics: [
32
- {
33
- severity: "error",
34
- code: "unsupported_image",
35
- message: `${path} is an image (${mimeType}); reading images is not supported`,
36
- },
37
- ],
38
- };
39
- }
40
- const textContent = new TextDecoder().decode(bytes);
41
- const allLines = textContent.split("\n");
42
- const totalFileLines = allLines.length;
43
- const startLine = offset ? Math.max(0, offset - 1) : 0;
44
- const startLineDisplay = startLine + 1;
45
- if (startLine >= allLines.length) {
46
- throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
47
- }
48
- let selectedContent;
49
- let userLimitedLines;
50
- if (limit !== undefined) {
51
- const endLine = Math.min(startLine + limit, allLines.length);
52
- selectedContent = allLines.slice(startLine, endLine).join("\n");
53
- userLimitedLines = endLine - startLine;
54
- }
55
- else {
56
- selectedContent = allLines.slice(startLine).join("\n");
51
+ const reader = getOrThrow(await env.openBinaryReader(absolutePath, undefined, context));
52
+ try {
53
+ // A concurrent writer can change the file between the scan and the reads; retry once from a fresh scan.
54
+ for (let attempt = 0;; attempt++) {
55
+ const before = getOrThrow(await reader.info(context));
56
+ const result = await readText(reader, before, path, offset, limit, context);
57
+ const after = getOrThrow(await reader.info(context));
58
+ if (after.size === before.size && after.mtimeMs === before.mtimeMs)
59
+ return result;
60
+ if (attempt === 1)
61
+ throw new Error(`${path} changed while it was read`);
62
+ }
57
63
  }
58
- const { content: headText, ...truncation } = truncateHead(selectedContent);
59
- const diagnostics = [];
60
- let outputText = headText;
61
- let details;
62
- if (truncation.firstLineExceedsLimit) {
63
- // Show the start of the line, cut at the byte limit on a character boundary.
64
- const lineBytes = new TextEncoder().encode(allLines[startLine]);
65
- const end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);
66
- outputText = new TextDecoder().decode(lineBytes.subarray(0, end));
67
- diagnostics.push({
68
- severity: "warn",
69
- code: "truncated",
70
- message: `Line ${startLineDisplay} is ${formatSize(lineBytes.byteLength)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,
71
- });
72
- details = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };
64
+ finally {
65
+ await reader.close(context);
73
66
  }
74
- else if (truncation.truncated) {
75
- const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
76
- const nextOffset = endLineDisplay + 1;
77
- const limitText = truncation.truncatedBy === "lines" ? "" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;
78
- diagnostics.push({
79
- severity: "info",
80
- code: "truncated",
81
- message: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,
82
- });
83
- details = { truncation };
84
- }
85
- else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
86
- const remaining = allLines.length - (startLine + userLimitedLines);
87
- const nextOffset = startLine + userLimitedLines + 1;
88
- diagnostics.push({
89
- severity: "info",
90
- message: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,
91
- });
92
- }
93
- return {
94
- content: outputText === "" ? [] : [{ type: "text", text: outputText }],
95
- ...(details === undefined ? {} : { details }),
96
- diagnostics,
97
- };
98
67
  },
99
68
  });
100
69
  }
70
+ /**
71
+ * The read result for the opened file. It equals decoding the whole file with `TextDecoder`, splitting it on `\n`,
72
+ * and bounding the selected lines with `truncateHead`, while reading only one scan's worth of the file plus the head.
73
+ */
74
+ async function readText(reader, info, path, offset, limit, context) {
75
+ const mimeType = await detectSupportedImageMimeTypeOf({
76
+ size: info.size,
77
+ read: async (position, length) => getOrThrow(await reader.read(position, length, context)),
78
+ });
79
+ if (mimeType) {
80
+ // Image content is not supported yet.
81
+ return {
82
+ content: [],
83
+ isError: true,
84
+ diagnostics: [
85
+ {
86
+ severity: "error",
87
+ code: "unsupported_image",
88
+ message: `${path} is an image (${mimeType}); reading images is not supported`,
89
+ },
90
+ ],
91
+ };
92
+ }
93
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
94
+ const startLineDisplay = startLine + 1;
95
+ // Lines are selected like `allLines.slice(startLine, endLine)`, which truncates fractional indices.
96
+ const sliceStart = sliceIndex(startLine);
97
+ // One pass finds the line count and the selection; a selection past the last line ends with it, as `slice` does,
98
+ // and an empty one (a zero or negative limit) is scanned as one line and then ignored.
99
+ // A start beyond any file is scanned from 0 only to count lines; the offset check below then fails as before.
100
+ const scanStart = Number.isSafeInteger(sliceStart) ? sliceStart : 0;
101
+ const requestedEnd = limit === undefined ? undefined : Math.max(scanStart + 1, sliceIndex(startLine + limit));
102
+ const scanEnd = requestedEnd !== undefined && Number.isSafeInteger(requestedEnd) ? requestedEnd : undefined;
103
+ const scanOf = async (endLine) => getOrThrow(await reader.scanLines({ startLine: scanStart, ...(endLine === undefined ? {} : { endLine }) }, context));
104
+ let scan = await scanOf(scanEnd);
105
+ const totalFileLines = scan.newlines + 1;
106
+ if (startLine >= totalFileLines) {
107
+ throw new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);
108
+ }
109
+ let userLimitedLines;
110
+ let selectedLineCount = totalFileLines - sliceStart;
111
+ if (limit !== undefined) {
112
+ const endLine = Math.min(startLine + limit, totalFileLines);
113
+ userLimitedLines = endLine - startLine;
114
+ // `slice` counts a negative end from the end of the lines, which only the line count tells; scan again for it.
115
+ const relativeEnd = sliceIndex(endLine);
116
+ const sliceEnd = relativeEnd < 0 ? Math.max(totalFileLines + relativeEnd, 0) : relativeEnd;
117
+ selectedLineCount = Math.max(0, sliceEnd - sliceStart);
118
+ if (selectedLineCount > 0 && relativeEnd < 0)
119
+ scan = await scanOf(sliceEnd);
120
+ }
121
+ const empty = selectedLineCount === 0;
122
+ // Counted like `truncateHead`: a trailing newline adds no line, and empty text has none.
123
+ const endsWithNewline = !empty && scan.lastLineStart === scan.end && scan.lastLineStart > scan.start;
124
+ const totals = {
125
+ lines: empty || scan.selectedBytes === 0 ? 0 : selectedLineCount - (endsWithNewline ? 1 : 0),
126
+ bytes: empty ? 0 : scan.selectedBytes,
127
+ };
128
+ const firstBytes = getOrThrow(await reader.read(0, 3, context));
129
+ const head = empty ? "" : await readHead(reader, scan.start, scan.end, startsWithBom(firstBytes), context);
130
+ const { content: headText, ...truncation } = truncateHeadOf(head, totals);
131
+ const diagnostics = [];
132
+ let outputText = headText;
133
+ let details;
134
+ if (truncation.firstLineExceedsLimit) {
135
+ // Show the start of the line, cut at the byte limit on a character boundary. Like `allLines[startLine]`, a
136
+ // fractional start line names no line.
137
+ const integral = Number.isInteger(startLine);
138
+ const lineBytes = new TextEncoder().encode(integral ? (head.split("\n")[0] ?? "") : "");
139
+ const lineSize = integral ? scan.firstLineBytes : 0;
140
+ const end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);
141
+ outputText = new TextDecoder().decode(lineBytes.subarray(0, end));
142
+ diagnostics.push({
143
+ severity: "warn",
144
+ code: "truncated",
145
+ message: `Line ${startLineDisplay} is ${formatSize(lineSize)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,
146
+ });
147
+ details = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };
148
+ }
149
+ else if (truncation.truncated) {
150
+ const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
151
+ const nextOffset = endLineDisplay + 1;
152
+ const limitText = truncation.truncatedBy === "lines" ? "" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;
153
+ diagnostics.push({
154
+ severity: "info",
155
+ code: "truncated",
156
+ message: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,
157
+ });
158
+ details = { truncation };
159
+ }
160
+ else if (userLimitedLines !== undefined && startLine + userLimitedLines < totalFileLines) {
161
+ const remaining = totalFileLines - (startLine + userLimitedLines);
162
+ const nextOffset = startLine + userLimitedLines + 1;
163
+ diagnostics.push({
164
+ severity: "info",
165
+ message: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,
166
+ });
167
+ }
168
+ return {
169
+ content: outputText === "" ? [] : [{ type: "text", text: outputText }],
170
+ ...(details === undefined ? {} : { details }),
171
+ diagnostics,
172
+ };
173
+ }
101
174
  //# sourceMappingURL=read.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Static, Type } from \"typebox\";\nimport { getOrThrow } from \"../env/index.ts\";\nimport { defineTool } from \"../harness/define.ts\";\nimport { characterEnd } from \"../harness/output.ts\";\nimport type { ToolDiagnostic, ToolRegistration } from \"../harness/types.ts\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"../truncate.ts\";\nimport { requireEnv } from \"./env.ts\";\nimport { detectSupportedImageMimeType } from \"./image.ts\";\nimport { resolveReadToolPath } from \"./path-utils.ts\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport type ReadToolDetails = {\n\t/** How the shown text was cut; the text itself is the result content. */\n\ttruncation?: Omit<TruncationResult, \"content\">;\n};\n\n/** Reads text files. Remarks about truncation and continuation are diagnostics; the content is only file text. */\nexport function createReadTool(): ToolRegistration<typeof readSchema, ReadToolDetails> {\n\treturn defineTool({\n\t\tname: \"read\",\n\t\tdescription: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tparameters: readSchema,\n\t\tasync execute(args, api, context) {\n\t\t\tconst { path, offset, limit } = args;\n\t\t\tconst env = requireEnv(api);\n\t\t\tconst absolutePath = await resolveReadToolPath(env, path, context);\n\t\t\tconst bytes = getOrThrow(await env.readBinaryFile(absolutePath, context));\n\t\t\tconst mimeType = detectSupportedImageMimeType(bytes);\n\t\t\tif (mimeType) {\n\t\t\t\t// Image content is not supported yet.\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [],\n\t\t\t\t\tisError: true,\n\t\t\t\t\tdiagnostics: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tseverity: \"error\",\n\t\t\t\t\t\t\tcode: \"unsupported_image\",\n\t\t\t\t\t\t\tmessage: `${path} is an image (${mimeType}); reading images is not supported`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst textContent = new TextDecoder().decode(bytes);\n\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\tconst totalFileLines = allLines.length;\n\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\tif (startLine >= allLines.length) {\n\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t}\n\n\t\t\tlet selectedContent: string;\n\t\t\tlet userLimitedLines: number | undefined;\n\t\t\tif (limit !== undefined) {\n\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t} else {\n\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t}\n\n\t\t\tconst { content: headText, ...truncation } = truncateHead(selectedContent);\n\t\t\tconst diagnostics: ToolDiagnostic[] = [];\n\t\t\tlet outputText = headText;\n\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t// Show the start of the line, cut at the byte limit on a character boundary.\n\t\t\t\tconst lineBytes = new TextEncoder().encode(allLines[startLine]);\n\t\t\t\tconst end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);\n\t\t\t\toutputText = new TextDecoder().decode(lineBytes.subarray(0, end));\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tseverity: \"warn\",\n\t\t\t\t\tcode: \"truncated\",\n\t\t\t\t\tmessage: `Line ${startLineDisplay} is ${formatSize(lineBytes.byteLength)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,\n\t\t\t\t});\n\t\t\t\tdetails = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };\n\t\t\t} else if (truncation.truncated) {\n\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\t\t\t\tconst limitText = truncation.truncatedBy === \"lines\" ? \"\" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tseverity: \"info\",\n\t\t\t\t\tcode: \"truncated\",\n\t\t\t\t\tmessage: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,\n\t\t\t\t});\n\t\t\t\tdetails = { truncation };\n\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tseverity: \"info\",\n\t\t\t\t\tmessage: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: outputText === \"\" ? [] : [{ type: \"text\", text: outputText }],\n\t\t\t\t...(details === undefined ? {} : { details }),\n\t\t\t\tdiagnostics,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context } from \"@earendil-works/chord\";\nimport { type Static, Type } from \"typebox\";\nimport {\n\ttype BinaryReader,\n\ttype FileInfo,\n\tgetOrThrow,\n\ttype LineScan,\n\trangeDecoder,\n\tstartsWithBom,\n} from \"../env/index.ts\";\nimport { defineTool } from \"../harness/define.ts\";\nimport { characterEnd } from \"../harness/output.ts\";\nimport type { ToolDiagnostic, ToolRegistration } from \"../harness/types.ts\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tDEFAULT_MAX_LINES,\n\tformatSize,\n\ttype TruncationResult,\n\ttruncateHeadOf,\n\tutf8ByteLength,\n} from \"../truncate.ts\";\nimport { requireEnv } from \"./env.ts\";\nimport { detectSupportedImageMimeTypeOf } from \"./image.ts\";\nimport { resolveReadToolPath } from \"./path-utils.ts\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport type ReadToolDetails = {\n\t/** How the shown text was cut; the text itself is the result content. */\n\ttruncation?: Omit<TruncationResult, \"content\">;\n};\n\nconst READ_CHUNK = 64 * 1024;\n\n/** `Array.prototype.slice`'s conversion of an index: NaN is 0, other values truncate toward zero. */\nfunction sliceIndex(value: number): number {\n\treturn Number.isNaN(value) ? 0 : Math.trunc(value);\n}\n\n/**\n * The decoded start of bytes `[start, end)` of the file, decoded as part of the whole file: all of it, or enough for\n * `truncateHeadOf` (more than `DEFAULT_MAX_BYTES + 1` bytes, or `DEFAULT_MAX_LINES` newlines).\n */\nasync function readHead(\n\treader: BinaryReader,\n\tstart: number,\n\tend: number,\n\tskipBom: boolean,\n\tcontext: Context,\n): Promise<string> {\n\tconst decoder = rangeDecoder();\n\tlet text = \"\";\n\tlet newlines = 0;\n\tfor (let position = skipBom && start === 0 ? 3 : start; position < end; ) {\n\t\tconst bytes = getOrThrow(await reader.read(position, Math.min(READ_CHUNK, end - position), context));\n\t\tif (bytes.length === 0) break;\n\t\tposition += bytes.length;\n\t\tconst decoded = decoder.decode(bytes, { stream: true });\n\t\ttext += decoded;\n\t\tfor (let index = decoded.indexOf(\"\\n\"); index !== -1; index = decoded.indexOf(\"\\n\", index + 1)) newlines++;\n\t\tif (newlines >= DEFAULT_MAX_LINES || utf8ByteLength(text) > DEFAULT_MAX_BYTES + 1) return text;\n\t}\n\treturn text + decoder.decode();\n}\n\n/** Reads text files. Remarks about truncation and continuation are diagnostics; the content is only file text. */\nexport function createReadTool(): ToolRegistration<typeof readSchema, ReadToolDetails> {\n\treturn defineTool({\n\t\tname: \"read\",\n\t\tdescription: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tparameters: readSchema,\n\t\tasync execute(args, api, context) {\n\t\t\tconst { path, offset, limit } = args;\n\t\t\tconst env = requireEnv(api);\n\t\t\tconst absolutePath = await resolveReadToolPath(env, path, context);\n\t\t\tconst reader = getOrThrow(await env.openBinaryReader(absolutePath, undefined, context));\n\t\t\ttry {\n\t\t\t\t// A concurrent writer can change the file between the scan and the reads; retry once from a fresh scan.\n\t\t\t\tfor (let attempt = 0; ; attempt++) {\n\t\t\t\t\tconst before = getOrThrow(await reader.info(context));\n\t\t\t\t\tconst result = await readText(reader, before, path, offset, limit, context);\n\t\t\t\t\tconst after = getOrThrow(await reader.info(context));\n\t\t\t\t\tif (after.size === before.size && after.mtimeMs === before.mtimeMs) return result;\n\t\t\t\t\tif (attempt === 1) throw new Error(`${path} changed while it was read`);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tawait reader.close(context);\n\t\t\t}\n\t\t},\n\t});\n}\n\n/**\n * The read result for the opened file. It equals decoding the whole file with `TextDecoder`, splitting it on `\\n`,\n * and bounding the selected lines with `truncateHead`, while reading only one scan's worth of the file plus the head.\n */\nasync function readText(\n\treader: BinaryReader,\n\tinfo: FileInfo,\n\tpath: string,\n\toffset: number | undefined,\n\tlimit: number | undefined,\n\tcontext: Context,\n) {\n\tconst mimeType = await detectSupportedImageMimeTypeOf({\n\t\tsize: info.size,\n\t\tread: async (position, length) => getOrThrow(await reader.read(position, length, context)),\n\t});\n\tif (mimeType) {\n\t\t// Image content is not supported yet.\n\t\treturn {\n\t\t\tcontent: [],\n\t\t\tisError: true,\n\t\t\tdiagnostics: [\n\t\t\t\t{\n\t\t\t\t\tseverity: \"error\" as const,\n\t\t\t\t\tcode: \"unsupported_image\",\n\t\t\t\t\tmessage: `${path} is an image (${mimeType}); reading images is not supported`,\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\tconst startLineDisplay = startLine + 1;\n\t// Lines are selected like `allLines.slice(startLine, endLine)`, which truncates fractional indices.\n\tconst sliceStart = sliceIndex(startLine);\n\t// One pass finds the line count and the selection; a selection past the last line ends with it, as `slice` does,\n\t// and an empty one (a zero or negative limit) is scanned as one line and then ignored.\n\t// A start beyond any file is scanned from 0 only to count lines; the offset check below then fails as before.\n\tconst scanStart = Number.isSafeInteger(sliceStart) ? sliceStart : 0;\n\tconst requestedEnd = limit === undefined ? undefined : Math.max(scanStart + 1, sliceIndex(startLine + limit));\n\tconst scanEnd = requestedEnd !== undefined && Number.isSafeInteger(requestedEnd) ? requestedEnd : undefined;\n\tconst scanOf = async (endLine: number | undefined): Promise<LineScan> =>\n\t\tgetOrThrow(\n\t\t\tawait reader.scanLines({ startLine: scanStart, ...(endLine === undefined ? {} : { endLine }) }, context),\n\t\t);\n\tlet scan = await scanOf(scanEnd);\n\tconst totalFileLines = scan.newlines + 1;\n\tif (startLine >= totalFileLines) {\n\t\tthrow new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);\n\t}\n\n\tlet userLimitedLines: number | undefined;\n\tlet selectedLineCount = totalFileLines - sliceStart;\n\tif (limit !== undefined) {\n\t\tconst endLine = Math.min(startLine + limit, totalFileLines);\n\t\tuserLimitedLines = endLine - startLine;\n\t\t// `slice` counts a negative end from the end of the lines, which only the line count tells; scan again for it.\n\t\tconst relativeEnd = sliceIndex(endLine);\n\t\tconst sliceEnd = relativeEnd < 0 ? Math.max(totalFileLines + relativeEnd, 0) : relativeEnd;\n\t\tselectedLineCount = Math.max(0, sliceEnd - sliceStart);\n\t\tif (selectedLineCount > 0 && relativeEnd < 0) scan = await scanOf(sliceEnd);\n\t}\n\tconst empty = selectedLineCount === 0;\n\t// Counted like `truncateHead`: a trailing newline adds no line, and empty text has none.\n\tconst endsWithNewline = !empty && scan.lastLineStart === scan.end && scan.lastLineStart > scan.start;\n\tconst totals = {\n\t\tlines: empty || scan.selectedBytes === 0 ? 0 : selectedLineCount - (endsWithNewline ? 1 : 0),\n\t\tbytes: empty ? 0 : scan.selectedBytes,\n\t};\n\tconst firstBytes = getOrThrow(await reader.read(0, 3, context));\n\tconst head = empty ? \"\" : await readHead(reader, scan.start, scan.end, startsWithBom(firstBytes), context);\n\n\tconst { content: headText, ...truncation } = truncateHeadOf(head, totals);\n\tconst diagnostics: ToolDiagnostic[] = [];\n\tlet outputText = headText;\n\tlet details: ReadToolDetails | undefined;\n\tif (truncation.firstLineExceedsLimit) {\n\t\t// Show the start of the line, cut at the byte limit on a character boundary. Like `allLines[startLine]`, a\n\t\t// fractional start line names no line.\n\t\tconst integral = Number.isInteger(startLine);\n\t\tconst lineBytes = new TextEncoder().encode(integral ? (head.split(\"\\n\")[0] ?? \"\") : \"\");\n\t\tconst lineSize = integral ? scan.firstLineBytes : 0;\n\t\tconst end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);\n\t\toutputText = new TextDecoder().decode(lineBytes.subarray(0, end));\n\t\tdiagnostics.push({\n\t\t\tseverity: \"warn\",\n\t\t\tcode: \"truncated\",\n\t\t\tmessage: `Line ${startLineDisplay} is ${formatSize(lineSize)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,\n\t\t});\n\t\tdetails = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };\n\t} else if (truncation.truncated) {\n\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\tconst nextOffset = endLineDisplay + 1;\n\t\tconst limitText = truncation.truncatedBy === \"lines\" ? \"\" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;\n\t\tdiagnostics.push({\n\t\t\tseverity: \"info\",\n\t\t\tcode: \"truncated\",\n\t\t\tmessage: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,\n\t\t});\n\t\tdetails = { truncation };\n\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < totalFileLines) {\n\t\tconst remaining = totalFileLines - (startLine + userLimitedLines);\n\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\tdiagnostics.push({\n\t\t\tseverity: \"info\",\n\t\t\tmessage: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,\n\t\t});\n\t}\n\n\treturn {\n\t\tcontent: outputText === \"\" ? [] : [{ type: \"text\" as const, text: outputText }],\n\t\t...(details === undefined ? {} : { details }),\n\t\tdiagnostics,\n\t};\n}\n"]}
@@ -52,4 +52,13 @@ export declare function formatSize(bytes: number): string;
52
52
  * returns empty content with firstLineExceedsLimit=true.
53
53
  */
54
54
  export declare function truncateHead(content: string, options?: TruncationOptions): TruncationResult;
55
+ /**
56
+ * `truncateHead` of a text known by a prefix and its totals (`lines` counted like `truncateHead`, ignoring a trailing
57
+ * newline). The prefix must be the whole text, or longer than `maxBytes + 1` UTF-8 bytes, or hold at least `maxLines`
58
+ * newlines; then the result equals `truncateHead` of the whole text.
59
+ */
60
+ export declare function truncateHeadOf(prefix: string, totals: {
61
+ readonly lines: number;
62
+ readonly bytes: number;
63
+ }, options?: TruncationOptions): TruncationResult;
55
64
  //# sourceMappingURL=truncate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAChC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AASD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BtD;AASD;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAgF/F"}
1
+ {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAChC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AASD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BtD;AASD;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAM/F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC7B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC1D,OAAO,GAAE,iBAAsB,GAC7B,gBAAgB,CAgFlB"}