pi-browser-harness 0.2.0 → 0.3.2

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 (43) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/package.json +4 -1
  3. package/skills/pi-browser-harness/SKILL.md +64 -6
  4. package/src/cdp/discovery.ts +95 -0
  5. package/src/cdp/errors.ts +19 -0
  6. package/src/cdp/session.ts +139 -0
  7. package/src/cdp/transport.ts +221 -0
  8. package/src/cdp/types.ts +72 -0
  9. package/src/client.ts +238 -0
  10. package/src/domains/click.ts +73 -0
  11. package/src/domains/dialog.ts +31 -0
  12. package/src/domains/drag.ts +66 -0
  13. package/src/domains/files.ts +194 -0
  14. package/src/domains/history.ts +76 -0
  15. package/src/domains/js.ts +197 -0
  16. package/src/domains/keyboard.ts +127 -0
  17. package/src/domains/navigate.ts +146 -0
  18. package/src/domains/network.ts +80 -0
  19. package/src/domains/page.ts +88 -0
  20. package/src/domains/screenshot.ts +150 -0
  21. package/src/domains/scroll.ts +60 -0
  22. package/src/domains/tabs.ts +121 -0
  23. package/src/domains/viewport.ts +40 -0
  24. package/src/index.ts +65 -142
  25. package/src/prompt.ts +27 -4
  26. package/src/registry.ts +55 -0
  27. package/src/schemas/common.ts +11 -0
  28. package/src/setup.ts +43 -26
  29. package/src/state.ts +22 -54
  30. package/src/util/js-template.ts +27 -0
  31. package/src/util/keycodes.ts +10 -0
  32. package/src/util/mutex.ts +25 -0
  33. package/src/util/paths.ts +21 -0
  34. package/src/util/result.ts +15 -0
  35. package/src/util/sharp-shim.ts +49 -0
  36. package/src/util/time.ts +29 -0
  37. package/src/util/tool.ts +193 -0
  38. package/src/util/truncate.ts +35 -0
  39. package/tsconfig.json +10 -1
  40. package/src/daemon.ts +0 -881
  41. package/src/protocol.ts +0 -77
  42. package/src/renderers.ts +0 -21
  43. package/src/tools.ts +0 -1363
package/src/state.ts CHANGED
@@ -1,77 +1,45 @@
1
1
  /**
2
2
  * Session persistence for pi-browser-harness.
3
3
  *
4
- * Persists daemon namespace, tab history, and preferences across
5
- * session reloads and branch navigation.
4
+ * Persists the daemon namespace and (when applicable) the remote browser ID
5
+ * across session reloads and branch navigation.
6
6
  */
7
7
 
8
8
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
9
9
 
10
- // ── Types ────────────────────────────────────────────────────────────────────
10
+ export type BrowserState = {
11
+ readonly namespace: string;
12
+ readonly remoteBrowserId?: string;
13
+ };
11
14
 
12
- export interface BrowserState {
13
- /** Daemon namespace (BU_NAME) for this session */
14
- namespace: string;
15
- /** Recently active tab target IDs */
16
- tabHistory: string[];
17
- /** Cloud browser ID (for remote sessions) */
18
- remoteBrowserId?: string;
19
- /** Screenshot output directory */
20
- screenshotDir: string;
21
- /** Whether debug click overlay is enabled */
22
- debugClicks: boolean;
23
- }
15
+ export const defaultState = (namespace = "default"): BrowserState => ({ namespace });
24
16
 
25
- // ── Defaults ─────────────────────────────────────────────────────────────────
26
-
27
- export function defaultState(namespace = "default"): BrowserState {
28
- return {
29
- namespace,
30
- tabHistory: [],
31
- screenshotDir: "",
32
- debugClicks: false,
33
- };
34
- }
35
-
36
- // ── Persist ──────────────────────────────────────────────────────────────────
37
-
38
- export function persistState(pi: ExtensionAPI, state: BrowserState): void {
17
+ export const persistState = (pi: ExtensionAPI, state: BrowserState): void => {
39
18
  pi.appendEntry<BrowserState>("browser-harness-state", state);
40
- }
41
-
42
- // ── Restore ──────────────────────────────────────────────────────────────────
19
+ };
43
20
 
44
21
  /**
45
22
  * Find the last browser-harness-state entry in the current branch
46
23
  * and return the restored state, merged with defaults.
24
+ *
25
+ * If `currentNamespace` is supplied (e.g. from the --browser-namespace flag),
26
+ * it overrides whatever is in the persisted entry.
47
27
  */
48
- export function restoreState(ctx: ExtensionContext, currentNamespace?: string): BrowserState {
28
+ export const restoreState = (ctx: ExtensionContext, currentNamespace?: string): BrowserState => {
49
29
  const branchEntries = ctx.sessionManager.getBranch();
50
- const defaults = defaultState(currentNamespace);
51
-
52
- // Walk branch from newest to oldest to find last persisted state
30
+ const fallback = defaultState(currentNamespace);
53
31
  for (let i = branchEntries.length - 1; i >= 0; i--) {
54
32
  const entry = branchEntries[i];
55
- if (entry.type === "custom" && entry.customType === "browser-harness-state") {
33
+ if (entry?.type === "custom" && entry.customType === "browser-harness-state") {
56
34
  const data = entry.data as Partial<BrowserState> | undefined;
57
35
  if (data) {
58
- // Merge persisted data onto defaults, but always keep the current
59
- // namespace (which comes from the --browser-namespace flag or auto-gen).
60
- return { ...defaults, ...data, namespace: currentNamespace || data.namespace || defaults.namespace };
36
+ return {
37
+ ...fallback,
38
+ ...data,
39
+ namespace: currentNamespace ?? data.namespace ?? fallback.namespace,
40
+ };
61
41
  }
62
42
  }
63
43
  }
64
-
65
- return defaults;
66
- }
67
-
68
- // ── Merge helpers ────────────────────────────────────────────────────────────
69
-
70
- export function addToTabHistory(state: BrowserState, targetId: string): BrowserState {
71
- const filtered = state.tabHistory.filter((id) => id !== targetId);
72
- return { ...state, tabHistory: [targetId, ...filtered].slice(0, 20) };
73
- }
74
-
75
- export function setRemoteBrowser(state: BrowserState, browserId: string): BrowserState {
76
- return { ...state, remoteBrowserId: browserId };
77
- }
44
+ return fallback;
45
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Build a JavaScript source string with safely-interpolated values.
3
+ * Every interpolated value is JSON.stringify'd, so strings, numbers,
4
+ * objects, and special characters all become valid JS literals.
5
+ *
6
+ * This is the ONLY supported way to interpolate untrusted values into
7
+ * evaluation source — never use raw template literals for JS that
8
+ * crosses the CDP boundary. The static parts of the template (the
9
+ * `strings` array) must remain hard-coded; only the `${...}` slots
10
+ * are sanitized.
11
+ *
12
+ * Values must be JSON-serializable: BigInt and Symbol throw at runtime;
13
+ * functions and undefined serialize to `undefined`/omitted, which yields
14
+ * a ReferenceError when evaluated. Pass strings, numbers, booleans,
15
+ * plain objects, or arrays.
16
+ */
17
+ export const safeJs = (
18
+ strings: TemplateStringsArray,
19
+ ...values: ReadonlyArray<unknown>
20
+ ): string => {
21
+ let out = strings[0] ?? "";
22
+ for (let i = 0; i < values.length; i++) {
23
+ out += JSON.stringify(values[i]);
24
+ out += strings[i + 1] ?? "";
25
+ }
26
+ return out;
27
+ };
@@ -0,0 +1,10 @@
1
+ const VIRTUAL_KEY_CODES: Readonly<Record<string, number>> = {
2
+ Enter: 13, Tab: 9, Backspace: 8, Escape: 27, Delete: 46, " ": 32,
3
+ ArrowLeft: 37, ArrowUp: 38, ArrowRight: 39, ArrowDown: 40,
4
+ Home: 36, End: 35, PageUp: 33, PageDown: 34,
5
+ };
6
+
7
+ export const SPECIAL_KEYS: ReadonlyArray<string> = Object.keys(VIRTUAL_KEY_CODES);
8
+
9
+ export const virtualKeyCode = (key: string): number =>
10
+ VIRTUAL_KEY_CODES[key] ?? (key.length === 1 ? key.charCodeAt(0) : 0);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Lightweight async mutex (~10 lines). Acquire returns a release function.
3
+ * When the mutex is free, acquire() resolves immediately. When held,
4
+ * callers are queued and execute serially in FIFO order.
5
+ */
6
+ export type Mutex = {
7
+ acquire(): Promise<() => void>;
8
+ };
9
+
10
+ export const createMutex = (): Mutex => {
11
+ let queue = Promise.resolve<unknown>(undefined);
12
+
13
+ return {
14
+ async acquire(): Promise<() => void> {
15
+ let release = () => {};
16
+ const next = new Promise<void>((resolve) => {
17
+ release = resolve;
18
+ });
19
+ const prev = queue;
20
+ queue = prev.then(() => next);
21
+ await prev;
22
+ return release;
23
+ },
24
+ };
25
+ };
@@ -0,0 +1,21 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ const NAMESPACE_RE = /^[a-zA-Z0-9_-]+$/;
6
+
7
+ const requireValidNamespace = (namespace: string): void => {
8
+ if (!NAMESPACE_RE.test(namespace)) {
9
+ throw new Error(`Invalid namespace (must match ${NAMESPACE_RE}): ${JSON.stringify(namespace)}`);
10
+ }
11
+ };
12
+
13
+ export const screenshotPath = (namespace: string, ext: "png" | "jpeg" = "png"): string => {
14
+ requireValidNamespace(namespace);
15
+ return join(tmpdir(), `pi-browser-screenshot-${namespace}-${randomUUID()}.${ext}`);
16
+ };
17
+
18
+ export const pdfPath = (namespace: string): string => {
19
+ requireValidNamespace(namespace);
20
+ return join(tmpdir(), `pi-browser-pdf-${namespace}-${randomUUID()}.pdf`);
21
+ };
@@ -0,0 +1,15 @@
1
+ export type Result<T, E> =
2
+ | { readonly success: true; readonly data: T }
3
+ | { readonly success: false; readonly error: E };
4
+
5
+ export const ok = <T>(data: T): Result<T, never> => ({ success: true, data });
6
+ export const err = <E>(error: E): Result<never, E> => ({ success: false, error });
7
+
8
+ export const map = <T, U, E>(r: Result<T, E>, f: (t: T) => U): Result<U, E> =>
9
+ r.success ? ok(f(r.data)) : r;
10
+
11
+ export const andThen = <T, U, E>(r: Result<T, E>, f: (t: T) => Result<U, E>): Result<U, E> =>
12
+ r.success ? f(r.data) : r;
13
+
14
+ export const mapErr = <T, E, F>(r: Result<T, E>, f: (e: E) => F): Result<T, F> =>
15
+ r.success ? r : err(f(r.error));
@@ -0,0 +1,49 @@
1
+ // Typed minimal shim for the optional `sharp` dependency. Sharp is a heavy
2
+ // native module not installed in every workspace; we import dynamically and
3
+ // distinguish "missing" (no install) from "error" (installed but threw).
4
+ //
5
+ // We declare only the surface we use. The dynamic import returns `unknown`
6
+ // at the boundary; the cast inside loadSharp() is the documented escape
7
+ // hatch — every other consumer sees the typed SharpFactory.
8
+
9
+ export type SharpInstance = {
10
+ metadata(): Promise<{ width?: number; height?: number }>;
11
+ composite(layers: ReadonlyArray<{ input: Buffer; top: number; left: number }>): SharpInstance;
12
+ resize(width: number, height: number, opts?: { fit?: "inside" }): SharpInstance;
13
+ toFile(path: string): Promise<unknown>;
14
+ };
15
+
16
+ export type SharpFactory = (input: string | Buffer) => SharpInstance;
17
+
18
+ export type SharpLoad =
19
+ | { readonly kind: "ok"; readonly sharp: SharpFactory }
20
+ | { readonly kind: "missing" }
21
+ | { readonly kind: "error"; readonly message: string };
22
+
23
+ export const loadSharp = async (): Promise<SharpLoad> => {
24
+ let mod: unknown;
25
+ try {
26
+ // sharp is an optional dependency; it may not be installed.
27
+ // @ts-ignore — optional peer dependency not required at compile time
28
+ mod = await import("sharp");
29
+ } catch (e) {
30
+ const msg = e instanceof Error ? e.message : String(e);
31
+ if (
32
+ msg.includes("Cannot find module") ||
33
+ msg.includes("MODULE_NOT_FOUND") ||
34
+ msg.includes("ERR_MODULE_NOT_FOUND")
35
+ ) {
36
+ return { kind: "missing" };
37
+ }
38
+ return { kind: "error", message: msg };
39
+ }
40
+ if (mod === null || mod === undefined) return { kind: "missing" };
41
+ // The CommonJS sharp module's default export IS the factory. ESM-wrapped
42
+ // sharp puts it on .default. Probe both.
43
+ const m = mod as { default?: unknown };
44
+ const candidate: unknown = typeof m.default === "function" ? m.default : mod;
45
+ if (typeof candidate !== "function") {
46
+ return { kind: "error", message: "sharp module did not expose a callable factory" };
47
+ }
48
+ return { kind: "ok", sharp: candidate as SharpFactory };
49
+ };
@@ -0,0 +1,29 @@
1
+ export const sleep = (ms: number, signal?: AbortSignal): Promise<void> =>
2
+ new Promise((resolve, reject) => {
3
+ if (signal?.aborted) {
4
+ reject(signal.reason ?? new Error("aborted"));
5
+ return;
6
+ }
7
+ const onAbort = (): void => {
8
+ clearTimeout(timer);
9
+ reject(signal?.reason ?? new Error("aborted"));
10
+ };
11
+ const timer = setTimeout(() => {
12
+ signal?.removeEventListener("abort", onAbort);
13
+ resolve();
14
+ }, ms);
15
+ signal?.addEventListener("abort", onAbort, { once: true });
16
+ });
17
+
18
+ export type Deadline = {
19
+ readonly remainingMs: () => number;
20
+ readonly expired: () => boolean;
21
+ };
22
+
23
+ export const deadline = (totalMs: number): Deadline => {
24
+ const end = Date.now() + totalMs;
25
+ return {
26
+ remainingMs: () => Math.max(0, end - Date.now()),
27
+ expired: () => Date.now() >= end,
28
+ };
29
+ };
@@ -0,0 +1,193 @@
1
+ import type { TSchema, Static } from "typebox";
2
+ import type {
3
+ AgentToolResult,
4
+ AgentToolUpdateCallback,
5
+ ExtensionAPI,
6
+ ExtensionContext,
7
+ Theme,
8
+ ToolDefinition,
9
+ ToolRenderResultOptions,
10
+ } from "@mariozechner/pi-coding-agent";
11
+ import type { Component } from "@mariozechner/pi-tui";
12
+ import type { BrowserClient } from "../client";
13
+ import type { Result } from "./result";
14
+
15
+ export type ToolOk = {
16
+ readonly text: string;
17
+ readonly details?: Readonly<Record<string, unknown>>;
18
+ };
19
+
20
+ export type ToolErrKind =
21
+ | "not_connected"
22
+ | "cdp_error"
23
+ | "timeout"
24
+ | "invalid_state"
25
+ | "io_error"
26
+ | "internal";
27
+
28
+ export type ToolErr = {
29
+ readonly kind: ToolErrKind;
30
+ readonly message: string;
31
+ readonly details?: Readonly<Record<string, unknown>>;
32
+ };
33
+
34
+ export type HandlerContext = {
35
+ readonly client: BrowserClient;
36
+ readonly signal: AbortSignal | undefined;
37
+ readonly onUpdate: (update: ToolOk) => void;
38
+ readonly extensionCtx: ExtensionContext;
39
+ };
40
+
41
+ export type ToolHandler<S extends TSchema> = (
42
+ args: Static<S>,
43
+ ctx: HandlerContext,
44
+ ) => Promise<Result<ToolOk, ToolErr>>;
45
+
46
+ export type BrowserToolDefinition<S extends TSchema> = {
47
+ readonly name: string;
48
+ readonly label: string;
49
+ readonly description: string;
50
+ readonly promptSnippet: string;
51
+ readonly promptGuidelines: ReadonlyArray<string>;
52
+ readonly parameters: S;
53
+ readonly handler: ToolHandler<S>;
54
+ readonly renderCall?: (args: Static<S>, theme: Theme) => Component;
55
+ readonly renderResult?: (result: AgentToolResult<unknown>, expanded: boolean, theme: Theme) => Component;
56
+ /** If true, the tool acquires the client's mutation mutex before executing.
57
+ * This ensures that two serialized tools never run concurrently,
58
+ * preventing race conditions on shared CDP session/page state. */
59
+ readonly serialized?: boolean;
60
+ readonly ensureAlive?: boolean;
61
+ };
62
+
63
+ // Type-erased entry for the registry's heterogeneous TOOLS array.
64
+ // renderCall accepts `never` so any concrete args type satisfies it via
65
+ // function-parameter contravariance (a handler that takes SpecificT is
66
+ // assignable to one that takes never, because never is a subtype of everything).
67
+ export type AnyBrowserToolDefinition = Omit<BrowserToolDefinition<TSchema>, "renderCall" | "renderResult"> & {
68
+ readonly renderCall?: (args: never, theme: Theme) => Component;
69
+ readonly renderResult?: (result: AgentToolResult<unknown>, expanded: boolean, theme: Theme) => Component;
70
+ };
71
+
72
+ export const defineBrowserTool = <S extends TSchema>(
73
+ def: BrowserToolDefinition<S>,
74
+ ): BrowserToolDefinition<S> => def;
75
+
76
+ type OkDetails = { readonly ok: true } & Readonly<Record<string, unknown>>;
77
+ type ErrDetails = { readonly ok: false; readonly kind: ToolErrKind; readonly message: string } & Readonly<Record<string, unknown>>;
78
+
79
+ const toToolResult = (
80
+ r: Result<ToolOk, ToolErr>,
81
+ toolName: string,
82
+ ): AgentToolResult<OkDetails | ErrDetails> => {
83
+ if (r.success) {
84
+ const details: OkDetails = { ok: true, ...(r.data.details ?? {}) };
85
+ return {
86
+ content: [{ type: "text", text: r.data.text }],
87
+ details,
88
+ };
89
+ }
90
+ const details: ErrDetails = {
91
+ ok: false,
92
+ kind: r.error.kind,
93
+ message: r.error.message,
94
+ ...(r.error.details ?? {}),
95
+ };
96
+ return {
97
+ // isError is an extension the pi-coding-agent runtime picks up from the result object;
98
+ // AgentToolResult<T> itself doesn't declare it but the runtime reads it via duck-typing.
99
+ // We cast through unknown to avoid widening the return type.
100
+ ...(({ isError: true }) as unknown as object),
101
+ content: [{ type: "text", text: `${toolName} failed (${r.error.kind}): ${r.error.message}` }],
102
+ details,
103
+ };
104
+ };
105
+
106
+ export const registerBrowserTool = (
107
+ pi: ExtensionAPI,
108
+ client: BrowserClient,
109
+ def: AnyBrowserToolDefinition,
110
+ ): void => {
111
+ // AnyBrowserToolDefinition is type-erased; we recover the schema-typed surface
112
+ // by casting def to the generic form. The cast is safe because defineBrowserTool
113
+ // ensures the handler, parameters, and renderCall were all created with the same S.
114
+ type S = TSchema;
115
+ const td: ToolDefinition<S> = {
116
+ name: def.name,
117
+ label: def.label,
118
+ description: def.description,
119
+ promptSnippet: def.promptSnippet,
120
+ promptGuidelines: [...def.promptGuidelines],
121
+ parameters: def.parameters,
122
+ ...(def.renderCall
123
+ ? {
124
+ renderCall: (args: Static<S>, theme: Theme, _ctx: Parameters<NonNullable<ToolDefinition<S>["renderCall"]>>[2]) =>
125
+ // Non-null assertion safe: this branch only runs when renderCall was provided.
126
+ // Cast from never->Component to unknown->Component: safe because at runtime
127
+ // args is always the concrete Static<S> value the tool was defined with.
128
+ (def.renderCall as (args: unknown, theme: Theme) => Component)(args, theme),
129
+ }
130
+ : {}),
131
+ ...(def.renderResult
132
+ ? {
133
+ renderResult: (
134
+ result: AgentToolResult<unknown>,
135
+ options: ToolRenderResultOptions,
136
+ theme: Theme,
137
+ _ctx: Parameters<NonNullable<ToolDefinition<S>["renderResult"]>>[3],
138
+ ) =>
139
+ // Non-null assertion safe: this branch only runs when renderResult was provided.
140
+ def.renderResult!(result, options.expanded ?? false, theme),
141
+ }
142
+ : {}),
143
+ async execute(_toolCallId, args, signal, onUpdate, extensionCtx) {
144
+ if (def.ensureAlive !== false) {
145
+ const alive = await client.ensureAlive();
146
+ if (!alive.success) {
147
+ return toToolResult(
148
+ { success: false, error: { kind: "not_connected", message: alive.error.message } },
149
+ def.name,
150
+ );
151
+ }
152
+ }
153
+ // Serialized tools acquire the client's mutation mutex so that only one
154
+ // mutation tool runs at a time. Observation tools skip this and run freely
155
+ // in parallel. AbortSignal is checked after acquiring the lock so that a
156
+ // cancelled task doesn't hold the mutex forever.
157
+ let release: (() => void) | undefined;
158
+ if (def.serialized) {
159
+ const acquire = await client.mutationMutex().acquire();
160
+ release = acquire;
161
+ if (signal?.aborted) {
162
+ release();
163
+ return toToolResult(
164
+ { success: false, error: { kind: "invalid_state", message: "Tool execution aborted before entering serialized lane" } },
165
+ def.name,
166
+ );
167
+ }
168
+ }
169
+ try {
170
+ const result = await def.handler(args, {
171
+ client,
172
+ signal,
173
+ onUpdate: (u) => {
174
+ if (onUpdate) {
175
+ // AgentToolUpdateCallback expects AgentToolResult<TDetails>; we use unknown for TDetails.
176
+ const update: AgentToolResult<OkDetails> = {
177
+ content: [{ type: "text", text: u.text }],
178
+ details: { ok: true, ...(u.details ?? {}) },
179
+ };
180
+ // Cast needed: onUpdate is AgentToolUpdateCallback<unknown> (generic TDetails).
181
+ (onUpdate as AgentToolUpdateCallback<OkDetails>)(update);
182
+ }
183
+ },
184
+ extensionCtx,
185
+ });
186
+ return toToolResult(result, def.name);
187
+ } finally {
188
+ release?.();
189
+ }
190
+ },
191
+ };
192
+ pi.registerTool(td);
193
+ };
@@ -0,0 +1,35 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ DEFAULT_MAX_BYTES,
6
+ DEFAULT_MAX_LINES,
7
+ formatSize,
8
+ truncateHead,
9
+ withFileMutationQueue,
10
+ } from "@mariozechner/pi-coding-agent";
11
+
12
+ const tempDirs: string[] = [];
13
+
14
+ export const cleanupTempDirs = async (): Promise<void> => {
15
+ const dirs = tempDirs.splice(0);
16
+ await Promise.all(dirs.map((d) => rm(d, { recursive: true, force: true }).catch(() => {})));
17
+ };
18
+
19
+ export type TruncatedOutput = {
20
+ readonly text: string;
21
+ readonly fullOutputPath?: string;
22
+ readonly wasTruncated: boolean;
23
+ };
24
+
25
+ export const applyTruncation = async (output: string, prefix: string): Promise<TruncatedOutput> => {
26
+ const t = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
27
+ if (!t.truncated) return { text: t.content, wasTruncated: false };
28
+ const dir = await mkdtemp(join(tmpdir(), `pi-bh-${prefix}-`));
29
+ tempDirs.push(dir);
30
+ const file = join(dir, "output.txt");
31
+ await withFileMutationQueue(file, async () => { await writeFile(file, output, "utf8"); });
32
+ const omitted = t.totalBytes - t.outputBytes;
33
+ const text = `${t.content}\n\n[Output truncated: ${t.outputLines} of ${t.totalLines} lines (${formatSize(t.outputBytes)} of ${formatSize(t.totalBytes)}). ${formatSize(omitted)} omitted. Full output: ${file}]`;
34
+ return { text, fullOutputPath: file, wasTruncated: true };
35
+ };
package/tsconfig.json CHANGED
@@ -4,9 +4,18 @@
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
6
  "strict": true,
7
+ "noImplicitAny": true,
8
+ "strictNullChecks": true,
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "noImplicitReturns": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "noUncheckedIndexedAccess": true,
14
+ "exactOptionalPropertyTypes": true,
15
+ "noPropertyAccessFromIndexSignature": true,
16
+ "forceConsistentCasingInFileNames": true,
7
17
  "esModuleInterop": true,
8
18
  "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true,
10
19
  "outDir": "./dist",
11
20
  "rootDir": "./src",
12
21
  "declaration": true,