@warlock.js/ai-workspace 5.45.0 → 5.47.0

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 (48) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -0
  3. package/cjs/index.cjs +514 -54
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts.map +1 -1
  6. package/esm/backends/local.mjs +24 -1
  7. package/esm/backends/local.mjs.map +1 -1
  8. package/esm/contracts/index.d.mts +4 -4
  9. package/esm/contracts/tool-io.type.d.mts +6 -0
  10. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  11. package/esm/contracts/workspace-backend.contract.d.mts +20 -0
  12. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  13. package/esm/contracts/workspace-edit.type.d.mts +30 -0
  14. package/esm/contracts/workspace-edit.type.d.mts.map +1 -1
  15. package/esm/contracts/workspace-ops.contract.d.mts +7 -2
  16. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  17. package/esm/contracts/workspace-policy.type.d.mts +23 -0
  18. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  19. package/esm/contracts/workspace.contract.d.mts +4 -1
  20. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  21. package/esm/hooks/shell-output.mjs +113 -0
  22. package/esm/hooks/shell-output.mjs.map +1 -0
  23. package/esm/index.d.mts +4 -4
  24. package/esm/ops.d.mts.map +1 -1
  25. package/esm/ops.mjs +108 -40
  26. package/esm/ops.mjs.map +1 -1
  27. package/esm/policy/shell-check.d.mts +2 -0
  28. package/esm/policy/shell-check.d.mts.map +1 -1
  29. package/esm/policy/shell-check.mjs +59 -5
  30. package/esm/policy/shell-check.mjs.map +1 -1
  31. package/esm/search/walk.mjs +209 -0
  32. package/esm/search/walk.mjs.map +1 -0
  33. package/esm/shell-jobs.d.mts +1 -1
  34. package/esm/shell-jobs.d.mts.map +1 -1
  35. package/esm/shell-jobs.mjs +1 -1
  36. package/esm/shell-jobs.mjs.map +1 -1
  37. package/esm/tools/glob.mjs +5 -4
  38. package/esm/tools/glob.mjs.map +1 -1
  39. package/esm/tools/grep.mjs +3 -2
  40. package/esm/tools/grep.mjs.map +1 -1
  41. package/esm/tools/run-shell.d.mts.map +1 -1
  42. package/esm/tools/run-shell.mjs +2 -1
  43. package/esm/tools/run-shell.mjs.map +1 -1
  44. package/esm/workspace.mjs +2 -2
  45. package/esm/workspace.mjs.map +1 -1
  46. package/llms-full.txt +36 -17
  47. package/package.json +3 -3
  48. package/skills/use-a-workspace/SKILL.md +36 -17
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walk.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/search/walk.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { WorkspaceBackend, WorkspaceBackendDirEntry } from \"../contracts\";\nimport { globToRegExp, isInside, matchesAnyGlob } from \"../policy/jail\";\n\n/** Directories `glob` and `grep` never descend into unless `search.skipDirs` says otherwise. */\nexport const DEFAULT_SEARCH_SKIP_DIRS: readonly string[] = [\".git\", \"node_modules\"];\n/** Default most files one `glob` or `grep` walk looks at. */\nexport const DEFAULT_SEARCH_MAX_FILES = 50_000;\n/** Default number of directories read at once during a walk. */\nexport const DEFAULT_SEARCH_CONCURRENCY = 16;\n\n/** One parsed `.gitignore` line. */\ninterface IgnoreRule {\n /** Workspace-relative directory the `.gitignore` sits in (\"\" for the root). */\n base: string;\n negate: boolean;\n dirOnly: boolean;\n /** Matches the entry's name only (the pattern had no slash). */\n nameOnly: boolean;\n /** Pattern began with `**\\/`: it may match at any depth under `base`. */\n anywhere: boolean;\n regex: RegExp;\n}\n\n/** What a walk looks at and how it is bounded. */\nexport interface WalkOptions {\n backend: WorkspaceBackend;\n /** Absolute jail root; every reported path is relative to it. */\n jailRoot: string;\n /** Absolute directory the walk starts in (inside the jail root). */\n startDir: string;\n /** `denyPaths` globs, checked before a directory is read. */\n denyPaths: readonly string[];\n skipDirs: readonly string[];\n gitignore: boolean;\n maxFiles: number;\n concurrency: number;\n signal?: AbortSignal;\n}\n\n/** Files found by a walk (absolute paths) and whether `maxFiles` cut it short. */\nexport interface WalkResult {\n files: string[];\n truncated: boolean;\n}\n\n/**\n * The directory a glob can be anchored at: its leading segments that hold no\n * wildcard, never counting the last segment (it may be a file). `\"\"` is the root.\n */\nexport function staticGlobPrefix(pattern: string): string {\n const segments = pattern.split(\"/\");\n const kept: string[] = [];\n\n for (let index = 0; index < segments.length - 1; index++) {\n const segment = segments[index] ?? \"\";\n\n if (segment === \"\" || segment === \".\" || segment === \"..\" || /[*?[\\]{}\\\\]/.test(segment)) {\n break;\n }\n\n kept.push(segment);\n }\n\n return kept.join(\"/\");\n}\n\n/** Parse the lines of a `.gitignore` located at workspace-relative `base`. */\nfunction parseGitignore(content: string, base: string): IgnoreRule[] {\n const rules: IgnoreRule[] = [];\n\n for (const raw of content.split(/\\r?\\n/)) {\n let line = raw.trimEnd();\n\n if (line === \"\" || line.startsWith(\"#\")) {\n continue;\n }\n\n const negate = line.startsWith(\"!\");\n\n if (negate) {\n line = line.slice(1);\n }\n\n const dirOnly = line.endsWith(\"/\");\n\n if (dirOnly) {\n line = line.slice(0, -1);\n }\n\n const anywhere = line.startsWith(\"**/\");\n\n if (anywhere) {\n line = line.slice(3);\n }\n\n if (line.startsWith(\"/\")) {\n line = line.slice(1);\n }\n\n if (line === \"\") {\n continue;\n }\n\n rules.push({\n base,\n negate,\n dirOnly,\n anywhere,\n nameOnly: !anywhere && !line.includes(\"/\") && !raw.trimStart().startsWith(\"/\"),\n regex: globToRegExp(line),\n });\n }\n\n return rules;\n}\n\n/** Whether the ordered rules (last match wins) ignore the entry at `rel`. */\nfunction isIgnored(\n rel: string,\n name: string,\n isDir: boolean,\n rules: readonly IgnoreRule[],\n): boolean {\n let ignored = false;\n\n for (const rule of rules) {\n if (rule.dirOnly && !isDir) {\n continue;\n }\n\n const local = rule.base === \"\" ? rel : rel.slice(rule.base.length + 1);\n let hit: boolean;\n\n if (rule.nameOnly) {\n hit = rule.regex.test(name);\n } else if (rule.anywhere) {\n const parts = local.split(\"/\");\n\n hit = parts.some((_, index) => rule.regex.test(parts.slice(index).join(\"/\")));\n } else {\n hit = rule.regex.test(local);\n }\n\n if (hit) {\n ignored = !rule.negate;\n }\n }\n\n return ignored;\n}\n\n/** Whether a directory name or relative path is one of the `skipDirs` globs. */\nfunction isSkipped(name: string, rel: string, skipDirs: readonly string[]): boolean {\n return skipDirs.some((glob) =>\n glob.includes(\"/\") ? matchesAnyGlob(rel, [glob]) : globToRegExp(glob).test(name),\n );\n}\n\n/**\n * List a directory with entry types: the backend's own typed listing when it has\n * one, else its plain listing plus `stat`, else (no `stat`) a second listing per\n * entry to tell directories from files.\n */\nexport async function listTyped(\n backend: WorkspaceBackend,\n absDir: string,\n): Promise<WorkspaceBackendDirEntry[]> {\n if (backend.listTyped !== undefined) {\n return backend.listTyped(absDir);\n }\n\n const entries = await backend.list(absDir);\n\n return Promise.all(\n entries.map(async (entry): Promise<WorkspaceBackendDirEntry> => {\n if (backend.stat !== undefined) {\n try {\n return { path: entry, type: (await backend.stat(entry)).type };\n } catch {\n return { path: entry, type: \"other\" };\n }\n }\n\n try {\n const children = await backend.list(entry);\n\n if (children.length === 0) {\n // An empty listing may be a file on a backend that lists files as empty.\n try {\n await backend.readFile(entry);\n\n return { path: entry, type: \"file\" };\n } catch {\n return { path: entry, type: \"directory\" };\n }\n }\n\n return { path: entry, type: \"directory\" };\n } catch {\n return { path: entry, type: \"file\" };\n }\n }),\n );\n}\n\n/** Read `.gitignore` at `absDir`, or nothing when there is none. */\nasync function readGitignore(\n backend: WorkspaceBackend,\n absDir: string,\n base: string,\n): Promise<IgnoreRule[]> {\n try {\n return parseGitignore(await backend.readFile(path.join(absDir, \".gitignore\")), base);\n } catch {\n return [];\n }\n}\n\n/** A queued directory: where it is and the `.gitignore` rules in force above it. */\ninterface PendingDir {\n abs: string;\n rel: string;\n rules: IgnoreRule[];\n}\n\n/**\n * Walk the files under `startDir` breadth first. Denied, skipped and\n * gitignored directories are dropped before they are listed, so a tree such as\n * `node_modules` is never read. Directories are read `concurrency` at a time,\n * the signal is checked between batches, and the walk stops at `maxFiles`.\n * Symlinks are never followed; a symlink to a file is reported only when its\n * target is inside the jail root.\n */\nexport async function walkFiles(options: WalkOptions): Promise<WalkResult> {\n const { backend, jailRoot, startDir, denyPaths, skipDirs, signal } = options;\n const files: string[] = [];\n let truncated = false;\n\n const isDenied = (rel: string): boolean => denyPaths.length > 0 && matchesAnyGlob(rel, denyPaths);\n const isDeniedDir = (rel: string): boolean =>\n isDenied(rel) ||\n denyPaths.some((glob) => glob.endsWith(\"/**\") && globToRegExp(glob.slice(0, -3)).test(rel));\n\n const startRel = path.relative(jailRoot, startDir).split(path.sep).join(\"/\");\n let inherited: IgnoreRule[] = [];\n\n if (options.gitignore) {\n // Rules from the folders between the jail root and the start folder.\n const segments = startRel === \"\" ? [] : startRel.split(\"/\");\n\n for (let depth = 0; depth < segments.length; depth++) {\n const base = segments.slice(0, depth).join(\"/\");\n const abs = base === \"\" ? jailRoot : path.join(jailRoot, ...segments.slice(0, depth));\n\n inherited = inherited.concat(await readGitignore(backend, abs, base));\n }\n }\n\n let level: PendingDir[] = [{ abs: startDir, rel: startRel, rules: inherited }];\n\n const readDir = async (dir: PendingDir, next: PendingDir[]): Promise<void> => {\n let entries: WorkspaceBackendDirEntry[];\n\n try {\n entries = await listTyped(backend, dir.abs);\n } catch {\n // A start folder that does not exist matches nothing; a folder that\n // vanished mid-walk is skipped.\n return;\n }\n\n let rules = dir.rules;\n\n if (\n options.gitignore &&\n entries.some(\n (entry) => path.basename(entry.path) === \".gitignore\" && entry.type !== \"directory\",\n )\n ) {\n rules = rules.concat(await readGitignore(backend, dir.abs, dir.rel));\n }\n\n for (const entry of entries) {\n if (files.length >= options.maxFiles) {\n truncated = true;\n\n return;\n }\n\n const name = path.basename(entry.path);\n const rel = dir.rel === \"\" ? name : `${dir.rel}/${name}`;\n let type = entry.type;\n let abs = entry.path;\n\n if (type === \"symlink\") {\n // Reported as a file only when it points at a file inside the jail.\n try {\n const target = await backend.realpath(entry.path);\n\n if (!isInside(target, jailRoot) || backend.stat === undefined) {\n continue;\n }\n\n if ((await backend.stat(target)).type !== \"file\") {\n continue;\n }\n\n type = \"file\";\n abs = entry.path;\n } catch {\n continue;\n }\n }\n\n if (type === \"directory\") {\n if (isDeniedDir(rel) || isSkipped(name, rel, skipDirs)) {\n continue;\n }\n\n if (options.gitignore && isIgnored(rel, name, true, rules)) {\n continue;\n }\n\n next.push({ abs, rel, rules });\n\n continue;\n }\n\n if (type !== \"file\" || isDenied(rel)) {\n continue;\n }\n\n if (options.gitignore && isIgnored(rel, name, false, rules)) {\n continue;\n }\n\n files.push(abs);\n }\n };\n\n while (level.length > 0 && !truncated) {\n const next: PendingDir[] = [];\n\n for (let index = 0; index < level.length && !truncated; index += options.concurrency) {\n signal?.throwIfAborted();\n\n await Promise.all(\n level.slice(index, index + options.concurrency).map((dir) => readDir(dir, next)),\n );\n }\n\n signal?.throwIfAborted();\n level = next;\n }\n\n return { files, truncated };\n}\n"],"mappings":"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"}
@@ -13,7 +13,7 @@ export interface ShellJobSpec {
13
13
  /** Wall-clock cap, if any. */
14
14
  timeoutMs?: number;
15
15
  /** Start the process; output must be delivered through `onOutput`. */
16
- launch(onOutput: (stream: WorkspaceOutputStream, chunk: string) => void): WorkspaceBackendProcess;
16
+ launch(onOutput: (stream: WorkspaceOutputStream, chunk: string) => void, jobId: string): WorkspaceBackendProcess;
17
17
  }
18
18
  /**
19
19
  * The background commands one workspace owns. Shared by every projection
@@ -1 +1 @@
1
- {"version":3,"file":"shell-jobs.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/shell-jobs.ts"],"mappings":";;;;;iBAgCiB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA,OAAO,WAAW,QAAQ,uBAAuB,yBAAyB;;;;;;;qBAoO/D;mBACM;UAET;UAEA;;MAGG;;EAKJ,MAAM,MAAM,eAAe;;UA0C1B;UAQA;;EAiBK,OAAO,YAAY,OAAO,qBAAqB,QAAQ;;EAavD,KAAK,aAAa,QAAQ;UAQzB;;EAWP,QAAQ;;EAKF,WAAW"}
1
+ {"version":3,"file":"shell-jobs.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/shell-jobs.ts"],"mappings":";;;;;iBAgCiB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA,OACE,WAAW,QAAQ,uBAAuB,wBAC1C,gBACC;;;;;;;qBAoOQ;mBACM;UAET;UAEA;;MAGG;;EAKJ,MAAM,MAAM,eAAe;;UA0C1B;UAQA;;EAiBK,OAAO,YAAY,OAAO,qBAAqB,QAAQ;;EAavD,KAAK,aAAa,QAAQ;UAQzB;;EAWP,QAAQ;;EAKF,WAAW"}
@@ -183,7 +183,7 @@ var ShellJobs = class {
183
183
  });
184
184
  const job = new ShellJob(`job-${this.nextId++}`, spec.command, spec.retainBytes);
185
185
  this.jobs.set(job.id, job);
186
- const handle = spec.launch((stream, chunk) => job.append(stream, chunk));
186
+ const handle = spec.launch((stream, chunk) => job.append(stream, chunk), job.id);
187
187
  job.process = handle;
188
188
  if (spec.timeoutMs !== void 0 && spec.timeoutMs > 0) {
189
189
  job.timer = setTimeout(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"shell-jobs.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/shell-jobs.ts"],"sourcesContent":["import { WorkspaceJobError, WorkspacePolicyError } from \"./errors\";\nimport type {\n ShellJobInfo,\n ShellJobStatus,\n ShellOutputOptions,\n ShellOutputResult,\n WorkspaceBackendProcess,\n WorkspaceOutputStream,\n} from \"./contracts\";\n\n/** Default most background commands running at once. */\nexport const DEFAULT_MAX_BACKGROUND_JOBS = 4;\n\n/** Finished commands kept for a last `shell_output` read before the oldest are pruned. */\nconst MAX_FINISHED_JOBS = 16;\n\n/** Longest `waitMs` a `shellOutput` call may ask for. */\nexport const MAX_OUTPUT_WAIT_MS = 30_000;\n\n/** How long `kill()` / `dispose()` wait for a killed tree to report its exit. */\nconst KILL_SETTLE_MS = 5_000;\n\n/** One retained piece of output. */\ninterface OutputChunk {\n stream: WorkspaceOutputStream;\n text: string;\n /** Cumulative byte offset of the chunk's first byte. */\n offset: number;\n bytes: number;\n}\n\n/** What {@link ShellJobs.start} needs to launch one command. */\nexport interface ShellJobSpec {\n /** The command line (already policy-checked). */\n command: string;\n /** Most bytes of output to keep (newest wins). */\n retainBytes: number;\n /** Most commands running at once. */\n maxJobs: number;\n /** Wall-clock cap, if any. */\n timeoutMs?: number;\n /** Start the process; output must be delivered through `onOutput`. */\n launch(onOutput: (stream: WorkspaceOutputStream, chunk: string) => void): WorkspaceBackendProcess;\n}\n\n/** Resolve after `ms`, without keeping the event loop alive. */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n\n timer.unref?.();\n });\n}\n\n/**\n * Keep the last `maxBytes` UTF-8 bytes of `text`, cutting forward to a\n * character boundary so the kept tail never starts mid-character.\n */\nfunction tailUtf8(text: string, maxBytes: number): { text: string; bytes: number } {\n const buffer = Buffer.from(text, \"utf8\");\n\n if (buffer.byteLength <= maxBytes) {\n return { text, bytes: buffer.byteLength };\n }\n\n let start = buffer.byteLength - maxBytes;\n\n while (start < buffer.byteLength && ((buffer[start] ?? 0) & 0xc0) === 0x80) {\n start++;\n }\n\n const kept = buffer.subarray(start);\n\n return { text: kept.toString(\"utf8\"), bytes: kept.byteLength };\n}\n\n/** Bytes of `text` from byte `skip` on, moved forward to a character boundary. */\nfunction skipUtf8(text: string, skip: number): string {\n const buffer = Buffer.from(text, \"utf8\");\n let start = Math.max(0, skip);\n\n while (start < buffer.byteLength && ((buffer[start] ?? 0) & 0xc0) === 0x80) {\n start++;\n }\n\n return buffer.subarray(start).toString(\"utf8\");\n}\n\n/** One background command and its retained output. */\nclass ShellJob {\n public status: ShellJobStatus = \"running\";\n\n public exitCode: number | null = null;\n\n public endedAt?: string;\n\n public readonly startedAt = new Date().toISOString();\n\n /** Total bytes produced so far (both streams). */\n public produced = 0;\n\n /** Where the next default read starts. */\n public readCursor = 0;\n\n /** Retained output, oldest first. */\n private readonly chunks: OutputChunk[] = [];\n\n private retained = 0;\n\n private readonly listeners = new Set<() => void>();\n\n public process?: WorkspaceBackendProcess;\n\n public killRequested = false;\n\n public timedOut = false;\n\n public timer?: ReturnType<typeof setTimeout>;\n\n public constructor(\n public readonly id: string,\n public readonly command: string,\n private readonly retainBytes: number,\n ) {}\n\n public append(stream: WorkspaceOutputStream, chunk: string): void {\n if (chunk === \"\") {\n return;\n }\n\n const bytes = Buffer.byteLength(chunk, \"utf8\");\n\n this.chunks.push({ stream, text: chunk, offset: this.produced, bytes });\n this.produced += bytes;\n this.retained += bytes;\n this.trim();\n this.notify();\n }\n\n /** Drop the oldest output until at most `retainBytes` are kept. */\n private trim(): void {\n while (this.retained > this.retainBytes && this.chunks.length > 0) {\n const oldest = this.chunks[0];\n\n if (oldest === undefined) {\n return;\n }\n\n const excess = this.retained - this.retainBytes;\n\n if (oldest.bytes <= excess) {\n this.chunks.shift();\n this.retained -= oldest.bytes;\n\n continue;\n }\n\n const kept = tailUtf8(oldest.text, oldest.bytes - excess);\n\n this.retained -= oldest.bytes - kept.bytes;\n oldest.offset += oldest.bytes - kept.bytes;\n oldest.text = kept.text;\n oldest.bytes = kept.bytes;\n }\n }\n\n public finish(exitCode: number): void {\n if (this.status !== \"running\") {\n return;\n }\n\n this.exitCode = exitCode;\n this.status = this.timedOut ? \"timed-out\" : this.killRequested ? \"killed\" : \"exited\";\n this.endedAt = new Date().toISOString();\n\n if (this.timer !== undefined) {\n clearTimeout(this.timer);\n }\n\n this.notify();\n }\n\n private notify(): void {\n for (const listener of [...this.listeners]) {\n listener();\n }\n }\n\n /** Resolve on the next output or state change (or after `ms`, or on abort). */\n public waitForChange(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve();\n\n return;\n }\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const done = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n\n this.listeners.delete(done);\n signal?.removeEventListener(\"abort\", done);\n resolve();\n };\n\n timer = setTimeout(done, ms);\n timer.unref?.();\n this.listeners.add(done);\n signal?.addEventListener(\"abort\", done, { once: true });\n });\n }\n\n public info(): ShellJobInfo {\n return {\n id: this.id,\n command: this.command,\n status: this.status,\n exitCode: this.exitCode,\n ...(this.process?.pid !== undefined ? { pid: this.process.pid } : {}),\n startedAt: this.startedAt,\n ...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),\n };\n }\n\n /** Output produced after byte `since`, plus how much of it is gone. */\n public read(since: number): ShellOutputResult {\n const from = Math.min(Math.max(0, Math.floor(since)), this.produced);\n const firstRetained = this.chunks[0]?.offset ?? this.produced;\n const droppedBytes = Math.max(0, firstRetained - from);\n let stdout = \"\";\n let stderr = \"\";\n\n for (const chunk of this.chunks) {\n const end = chunk.offset + chunk.bytes;\n\n if (end <= from) {\n continue;\n }\n\n const text = chunk.offset >= from ? chunk.text : skipUtf8(chunk.text, from - chunk.offset);\n\n if (chunk.stream === \"stdout\") {\n stdout += text;\n } else {\n stderr += text;\n }\n }\n\n this.readCursor = this.produced;\n\n return {\n ...this.info(),\n stdout,\n stderr,\n cursor: this.produced,\n droppedBytes,\n truncated: droppedBytes > 0,\n };\n }\n}\n\n/**\n * The background commands one workspace owns. Shared by every projection\n * (`readonly()`, `scope()`) of the workspace, so `dispose()` on any of them\n * stops them all.\n */\nexport class ShellJobs {\n private readonly jobs = new Map<string, ShellJob>();\n\n private nextId = 1;\n\n private disposed = false;\n\n /** Whether {@link dispose} has run. */\n public get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Start a command as a background job. */\n public start(spec: ShellJobSpec): ShellJobInfo {\n if (this.disposed) {\n throw new WorkspacePolicyError(\n \"The workspace has been disposed; it cannot start new commands.\",\n { type: \"denied-command\", reason: \"not-allowed\", command: spec.command },\n );\n }\n\n const running = [...this.jobs.values()].filter((job) => job.status === \"running\").length;\n\n if (running >= spec.maxJobs) {\n throw new WorkspacePolicyError(\n `${running} background commands are already running (the limit is ${spec.maxJobs}). Stop one with shell_kill first.`,\n { type: \"denied-command\", reason: \"not-allowed\", command: spec.command },\n );\n }\n\n const job = new ShellJob(`job-${this.nextId++}`, spec.command, spec.retainBytes);\n\n this.jobs.set(job.id, job);\n\n const handle = spec.launch((stream, chunk) => job.append(stream, chunk));\n\n job.process = handle;\n\n if (spec.timeoutMs !== undefined && spec.timeoutMs > 0) {\n job.timer = setTimeout(() => {\n if (job.status === \"running\") {\n job.timedOut = true;\n handle.kill();\n }\n }, spec.timeoutMs);\n job.timer.unref?.();\n }\n\n void handle.done.then(({ exitCode }) => job.finish(exitCode));\n this.prune();\n\n return job.info();\n }\n\n /** Forget the oldest finished jobs beyond {@link MAX_FINISHED_JOBS}. */\n private prune(): void {\n const finished = [...this.jobs.values()].filter((job) => job.status !== \"running\");\n\n for (const job of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_JOBS))) {\n this.jobs.delete(job.id);\n }\n }\n\n private get(id: string): ShellJob {\n const job = this.jobs.get(id);\n\n if (job === undefined) {\n throw new WorkspaceJobError(\n `No background command with id \"${id}\" exists in this workspace.`,\n {\n type: \"not-found\",\n jobId: id,\n },\n );\n }\n\n return job;\n }\n\n /** New output of a job (see {@link ShellOutputOptions}). */\n public async output(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n const job = this.get(id);\n const since = opts?.since ?? job.readCursor;\n const waitMs = Math.min(Math.max(0, opts?.waitMs ?? 0), MAX_OUTPUT_WAIT_MS);\n\n if (waitMs > 0 && job.status === \"running\" && job.produced <= since) {\n await job.waitForChange(waitMs, opts?.signal);\n }\n\n return job.read(since);\n }\n\n /** Kill a job's process tree and wait (bounded) for it to stop. */\n public async kill(id: string): Promise<ShellJobInfo> {\n const job = this.get(id);\n\n await this.stop(job);\n\n return job.info();\n }\n\n private async stop(job: ShellJob): Promise<void> {\n if (job.status !== \"running\" || job.process === undefined) {\n return;\n }\n\n job.killRequested = true;\n job.process.kill();\n await Promise.race([job.process.done, delay(KILL_SETTLE_MS)]);\n }\n\n /** Every job, oldest first. */\n public list(): ShellJobInfo[] {\n return [...this.jobs.values()].map((job) => job.info());\n }\n\n /** Kill every running job and refuse new ones. Idempotent. */\n public async dispose(): Promise<void> {\n this.disposed = true;\n await Promise.all([...this.jobs.values()].map((job) => this.stop(job)));\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shell-jobs.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/shell-jobs.ts"],"sourcesContent":["import { WorkspaceJobError, WorkspacePolicyError } from \"./errors\";\nimport type {\n ShellJobInfo,\n ShellJobStatus,\n ShellOutputOptions,\n ShellOutputResult,\n WorkspaceBackendProcess,\n WorkspaceOutputStream,\n} from \"./contracts\";\n\n/** Default most background commands running at once. */\nexport const DEFAULT_MAX_BACKGROUND_JOBS = 4;\n\n/** Finished commands kept for a last `shell_output` read before the oldest are pruned. */\nconst MAX_FINISHED_JOBS = 16;\n\n/** Longest `waitMs` a `shellOutput` call may ask for. */\nexport const MAX_OUTPUT_WAIT_MS = 30_000;\n\n/** How long `kill()` / `dispose()` wait for a killed tree to report its exit. */\nconst KILL_SETTLE_MS = 5_000;\n\n/** One retained piece of output. */\ninterface OutputChunk {\n stream: WorkspaceOutputStream;\n text: string;\n /** Cumulative byte offset of the chunk's first byte. */\n offset: number;\n bytes: number;\n}\n\n/** What {@link ShellJobs.start} needs to launch one command. */\nexport interface ShellJobSpec {\n /** The command line (already policy-checked). */\n command: string;\n /** Most bytes of output to keep (newest wins). */\n retainBytes: number;\n /** Most commands running at once. */\n maxJobs: number;\n /** Wall-clock cap, if any. */\n timeoutMs?: number;\n /** Start the process; output must be delivered through `onOutput`. */\n launch(\n onOutput: (stream: WorkspaceOutputStream, chunk: string) => void,\n jobId: string,\n ): WorkspaceBackendProcess;\n}\n\n/** Resolve after `ms`, without keeping the event loop alive. */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n\n timer.unref?.();\n });\n}\n\n/**\n * Keep the last `maxBytes` UTF-8 bytes of `text`, cutting forward to a\n * character boundary so the kept tail never starts mid-character.\n */\nfunction tailUtf8(text: string, maxBytes: number): { text: string; bytes: number } {\n const buffer = Buffer.from(text, \"utf8\");\n\n if (buffer.byteLength <= maxBytes) {\n return { text, bytes: buffer.byteLength };\n }\n\n let start = buffer.byteLength - maxBytes;\n\n while (start < buffer.byteLength && ((buffer[start] ?? 0) & 0xc0) === 0x80) {\n start++;\n }\n\n const kept = buffer.subarray(start);\n\n return { text: kept.toString(\"utf8\"), bytes: kept.byteLength };\n}\n\n/** Bytes of `text` from byte `skip` on, moved forward to a character boundary. */\nfunction skipUtf8(text: string, skip: number): string {\n const buffer = Buffer.from(text, \"utf8\");\n let start = Math.max(0, skip);\n\n while (start < buffer.byteLength && ((buffer[start] ?? 0) & 0xc0) === 0x80) {\n start++;\n }\n\n return buffer.subarray(start).toString(\"utf8\");\n}\n\n/** One background command and its retained output. */\nclass ShellJob {\n public status: ShellJobStatus = \"running\";\n\n public exitCode: number | null = null;\n\n public endedAt?: string;\n\n public readonly startedAt = new Date().toISOString();\n\n /** Total bytes produced so far (both streams). */\n public produced = 0;\n\n /** Where the next default read starts. */\n public readCursor = 0;\n\n /** Retained output, oldest first. */\n private readonly chunks: OutputChunk[] = [];\n\n private retained = 0;\n\n private readonly listeners = new Set<() => void>();\n\n public process?: WorkspaceBackendProcess;\n\n public killRequested = false;\n\n public timedOut = false;\n\n public timer?: ReturnType<typeof setTimeout>;\n\n public constructor(\n public readonly id: string,\n public readonly command: string,\n private readonly retainBytes: number,\n ) {}\n\n public append(stream: WorkspaceOutputStream, chunk: string): void {\n if (chunk === \"\") {\n return;\n }\n\n const bytes = Buffer.byteLength(chunk, \"utf8\");\n\n this.chunks.push({ stream, text: chunk, offset: this.produced, bytes });\n this.produced += bytes;\n this.retained += bytes;\n this.trim();\n this.notify();\n }\n\n /** Drop the oldest output until at most `retainBytes` are kept. */\n private trim(): void {\n while (this.retained > this.retainBytes && this.chunks.length > 0) {\n const oldest = this.chunks[0];\n\n if (oldest === undefined) {\n return;\n }\n\n const excess = this.retained - this.retainBytes;\n\n if (oldest.bytes <= excess) {\n this.chunks.shift();\n this.retained -= oldest.bytes;\n\n continue;\n }\n\n const kept = tailUtf8(oldest.text, oldest.bytes - excess);\n\n this.retained -= oldest.bytes - kept.bytes;\n oldest.offset += oldest.bytes - kept.bytes;\n oldest.text = kept.text;\n oldest.bytes = kept.bytes;\n }\n }\n\n public finish(exitCode: number): void {\n if (this.status !== \"running\") {\n return;\n }\n\n this.exitCode = exitCode;\n this.status = this.timedOut ? \"timed-out\" : this.killRequested ? \"killed\" : \"exited\";\n this.endedAt = new Date().toISOString();\n\n if (this.timer !== undefined) {\n clearTimeout(this.timer);\n }\n\n this.notify();\n }\n\n private notify(): void {\n for (const listener of [...this.listeners]) {\n listener();\n }\n }\n\n /** Resolve on the next output or state change (or after `ms`, or on abort). */\n public waitForChange(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve();\n\n return;\n }\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const done = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n\n this.listeners.delete(done);\n signal?.removeEventListener(\"abort\", done);\n resolve();\n };\n\n timer = setTimeout(done, ms);\n timer.unref?.();\n this.listeners.add(done);\n signal?.addEventListener(\"abort\", done, { once: true });\n });\n }\n\n public info(): ShellJobInfo {\n return {\n id: this.id,\n command: this.command,\n status: this.status,\n exitCode: this.exitCode,\n ...(this.process?.pid !== undefined ? { pid: this.process.pid } : {}),\n startedAt: this.startedAt,\n ...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),\n };\n }\n\n /** Output produced after byte `since`, plus how much of it is gone. */\n public read(since: number): ShellOutputResult {\n const from = Math.min(Math.max(0, Math.floor(since)), this.produced);\n const firstRetained = this.chunks[0]?.offset ?? this.produced;\n const droppedBytes = Math.max(0, firstRetained - from);\n let stdout = \"\";\n let stderr = \"\";\n\n for (const chunk of this.chunks) {\n const end = chunk.offset + chunk.bytes;\n\n if (end <= from) {\n continue;\n }\n\n const text = chunk.offset >= from ? chunk.text : skipUtf8(chunk.text, from - chunk.offset);\n\n if (chunk.stream === \"stdout\") {\n stdout += text;\n } else {\n stderr += text;\n }\n }\n\n this.readCursor = this.produced;\n\n return {\n ...this.info(),\n stdout,\n stderr,\n cursor: this.produced,\n droppedBytes,\n truncated: droppedBytes > 0,\n };\n }\n}\n\n/**\n * The background commands one workspace owns. Shared by every projection\n * (`readonly()`, `scope()`) of the workspace, so `dispose()` on any of them\n * stops them all.\n */\nexport class ShellJobs {\n private readonly jobs = new Map<string, ShellJob>();\n\n private nextId = 1;\n\n private disposed = false;\n\n /** Whether {@link dispose} has run. */\n public get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Start a command as a background job. */\n public start(spec: ShellJobSpec): ShellJobInfo {\n if (this.disposed) {\n throw new WorkspacePolicyError(\n \"The workspace has been disposed; it cannot start new commands.\",\n { type: \"denied-command\", reason: \"not-allowed\", command: spec.command },\n );\n }\n\n const running = [...this.jobs.values()].filter((job) => job.status === \"running\").length;\n\n if (running >= spec.maxJobs) {\n throw new WorkspacePolicyError(\n `${running} background commands are already running (the limit is ${spec.maxJobs}). Stop one with shell_kill first.`,\n { type: \"denied-command\", reason: \"not-allowed\", command: spec.command },\n );\n }\n\n const job = new ShellJob(`job-${this.nextId++}`, spec.command, spec.retainBytes);\n\n this.jobs.set(job.id, job);\n\n const handle = spec.launch((stream, chunk) => job.append(stream, chunk), job.id);\n\n job.process = handle;\n\n if (spec.timeoutMs !== undefined && spec.timeoutMs > 0) {\n job.timer = setTimeout(() => {\n if (job.status === \"running\") {\n job.timedOut = true;\n handle.kill();\n }\n }, spec.timeoutMs);\n job.timer.unref?.();\n }\n\n void handle.done.then(({ exitCode }) => job.finish(exitCode));\n this.prune();\n\n return job.info();\n }\n\n /** Forget the oldest finished jobs beyond {@link MAX_FINISHED_JOBS}. */\n private prune(): void {\n const finished = [...this.jobs.values()].filter((job) => job.status !== \"running\");\n\n for (const job of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_JOBS))) {\n this.jobs.delete(job.id);\n }\n }\n\n private get(id: string): ShellJob {\n const job = this.jobs.get(id);\n\n if (job === undefined) {\n throw new WorkspaceJobError(\n `No background command with id \"${id}\" exists in this workspace.`,\n {\n type: \"not-found\",\n jobId: id,\n },\n );\n }\n\n return job;\n }\n\n /** New output of a job (see {@link ShellOutputOptions}). */\n public async output(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n const job = this.get(id);\n const since = opts?.since ?? job.readCursor;\n const waitMs = Math.min(Math.max(0, opts?.waitMs ?? 0), MAX_OUTPUT_WAIT_MS);\n\n if (waitMs > 0 && job.status === \"running\" && job.produced <= since) {\n await job.waitForChange(waitMs, opts?.signal);\n }\n\n return job.read(since);\n }\n\n /** Kill a job's process tree and wait (bounded) for it to stop. */\n public async kill(id: string): Promise<ShellJobInfo> {\n const job = this.get(id);\n\n await this.stop(job);\n\n return job.info();\n }\n\n private async stop(job: ShellJob): Promise<void> {\n if (job.status !== \"running\" || job.process === undefined) {\n return;\n }\n\n job.killRequested = true;\n job.process.kill();\n await Promise.race([job.process.done, delay(KILL_SETTLE_MS)]);\n }\n\n /** Every job, oldest first. */\n public list(): ShellJobInfo[] {\n return [...this.jobs.values()].map((job) => job.info());\n }\n\n /** Kill every running job and refuse new ones. Idempotent. */\n public async dispose(): Promise<void> {\n this.disposed = true;\n await Promise.all([...this.jobs.values()].map((job) => this.stop(job)));\n }\n}\n"],"mappings":"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"}
@@ -32,12 +32,13 @@ const globInputSchema = objectSchema({ pattern: stringField() }, GLOB_JSON_SCHEM
32
32
  function makeGlobTool(ops, options) {
33
33
  return tool({
34
34
  name: options?.name ?? "glob",
35
- description: "Find files in the workspace whose path matches a glob pattern (supports `*`, `**`, and `?`). Returns the matching workspace-relative paths, sorted.",
35
+ description: "Find files in the workspace whose path matches a glob pattern (supports `*`, `**`, and `?`). Returns the matching workspace-relative paths, sorted. Skips node_modules, .git and gitignored folders; start the pattern with a folder (`src/**/*.ts`) to search faster.",
36
36
  action: (input) => `Finding files matching ${input.pattern}`,
37
37
  input: globInputSchema,
38
- async execute(input) {
39
- if (ops.globResult !== void 0) return ops.globResult(input.pattern);
40
- return { paths: await ops.glob(input.pattern) };
38
+ async execute(input, ctx) {
39
+ const opts = ctx?.signal ? { signal: ctx.signal } : void 0;
40
+ if (ops.globResult !== void 0) return ops.globResult(input.pattern, opts);
41
+ return { paths: await ops.glob(input.pattern, opts) };
41
42
  }
42
43
  });
43
44
  }
@@ -1 +1 @@
1
- {"version":3,"file":"glob.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/glob.ts"],"sourcesContent":["import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport { GLOB_JSON_SCHEMA } from \"./json-schemas\";\nimport type { GlobInput, GlobResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGlobTool} to customize the vended tool. */\nexport interface MakeGlobToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"glob\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * path-match surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GlobInput} — a single required `pattern`\n * string. Built on the package's shared, dependency-free schema builders\n * (no schema library, matching the validator idiom the `@warlock.js/ai`\n * tool runtime expects).\n */\nconst globInputSchema = objectSchema<GlobInput>(\n {\n pattern: stringField(),\n },\n GLOB_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `glob` tool — resolve a glob pattern to the\n * matching workspace-relative paths within the jail. The returned\n * {@link ToolContract} validates the LLM's arguments, delegates to\n * {@link WorkspaceOps.glob} (which returns a bare sorted `string[]`), and\n * wraps the result in a {@link GlobResult} so the agent always reads a\n * stable `{ paths }` envelope. The jail and `denyPaths` filtering are\n * enforced in the shared ops layer; a policy violation surfaces as typed\n * tool-error *data* via the runtime's `invoke()` wrapper.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `glob`.\n *\n * @example\n * const glob = makeGlobTool(ops);\n * const { data } = await glob.invoke({ pattern: \"src/models/**\\/*.ts\" });\n * console.log(data?.paths);\n */\nexport function makeGlobTool(\n ops: WorkspaceOps,\n options?: MakeGlobToolOptions,\n): ToolContract<GlobInput, GlobResult> {\n return tool<GlobInput, GlobResult>({\n name: options?.name ?? \"glob\",\n description:\n \"Find files in the workspace whose path matches a glob pattern \" +\n \"(supports `*`, `**`, and `?`). Returns the matching \" +\n \"workspace-relative paths, sorted.\",\n action: (input) => `Finding files matching ${input.pattern}`,\n input: globInputSchema,\n async execute(input) {\n if (ops.globResult !== undefined) {\n return ops.globResult(input.pattern);\n }\n\n const paths = await ops.glob(input.pattern);\n\n return { paths };\n },\n });\n}\n"],"mappings":";;;;;;;;;;;AAqBA,MAAM,kBAAkB,aACtB,EACE,SAAS,YAAY,EACvB,GACA,gBACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,aACd,KACA,SACqC;CACrC,OAAO,KAA4B;EACjC,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,SAAS,UAAU,0BAA0B,MAAM;EACnD,OAAO;EACP,MAAM,QAAQ,OAAO;GACnB,IAAI,IAAI,eAAe,QACrB,OAAO,IAAI,WAAW,MAAM,OAAO;GAKrC,OAAO,EAAE,aAFW,IAAI,KAAK,MAAM,OAAO,EAE3B;EACjB;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"glob.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/glob.ts"],"sourcesContent":["import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport { GLOB_JSON_SCHEMA } from \"./json-schemas\";\nimport type { GlobInput, GlobResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGlobTool} to customize the vended tool. */\nexport interface MakeGlobToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"glob\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * path-match surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GlobInput} — a single required `pattern`\n * string. Built on the package's shared, dependency-free schema builders\n * (no schema library, matching the validator idiom the `@warlock.js/ai`\n * tool runtime expects).\n */\nconst globInputSchema = objectSchema<GlobInput>(\n {\n pattern: stringField(),\n },\n GLOB_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `glob` tool — resolve a glob pattern to the\n * matching workspace-relative paths within the jail. The returned\n * {@link ToolContract} validates the LLM's arguments, delegates to\n * {@link WorkspaceOps.glob} (which returns a bare sorted `string[]`), and\n * wraps the result in a {@link GlobResult} so the agent always reads a\n * stable `{ paths }` envelope. The jail and `denyPaths` filtering are\n * enforced in the shared ops layer; a policy violation surfaces as typed\n * tool-error *data* via the runtime's `invoke()` wrapper.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `glob`.\n *\n * @example\n * const glob = makeGlobTool(ops);\n * const { data } = await glob.invoke({ pattern: \"src/models/**\\/*.ts\" });\n * console.log(data?.paths);\n */\nexport function makeGlobTool(\n ops: WorkspaceOps,\n options?: MakeGlobToolOptions,\n): ToolContract<GlobInput, GlobResult> {\n return tool<GlobInput, GlobResult>({\n name: options?.name ?? \"glob\",\n description:\n \"Find files in the workspace whose path matches a glob pattern \" +\n \"(supports `*`, `**`, and `?`). Returns the matching \" +\n \"workspace-relative paths, sorted. Skips node_modules, .git and \" +\n \"gitignored folders; start the pattern with a folder (`src/**/*.ts`) to search faster.\",\n action: (input) => `Finding files matching ${input.pattern}`,\n input: globInputSchema,\n async execute(input, ctx) {\n const opts = ctx?.signal ? { signal: ctx.signal } : undefined;\n\n if (ops.globResult !== undefined) {\n return ops.globResult(input.pattern, opts);\n }\n\n const paths = await ops.glob(input.pattern, opts);\n\n return { paths };\n },\n });\n}\n"],"mappings":";;;;;;;;;;;AAqBA,MAAM,kBAAkB,aACtB,EACE,SAAS,YAAY,EACvB,GACA,gBACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,aACd,KACA,SACqC;CACrC,OAAO,KAA4B;EACjC,MAAM,SAAS,QAAQ;EACvB,aACE;EAIF,SAAS,UAAU,0BAA0B,MAAM;EACnD,OAAO;EACP,MAAM,QAAQ,OAAO,KAAK;GACxB,MAAM,OAAO,KAAK,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI;GAEpD,IAAI,IAAI,eAAe,QACrB,OAAO,IAAI,WAAW,MAAM,SAAS,IAAI;GAK3C,OAAO,EAAE,aAFW,IAAI,KAAK,MAAM,SAAS,IAAI,EAEjC;EACjB;CACF,CAAC;AACH"}
@@ -38,10 +38,11 @@ function makeGrepTool(ops, options) {
38
38
  description: "Search file contents across the workspace for a regular-expression pattern. Optionally narrow the scanned files with a glob and match case-insensitively. Returns every matching line with its file path and 1-based line number.",
39
39
  action: (input) => `Searching for /${input.pattern}/`,
40
40
  input: grepInputSchema,
41
- async execute(input) {
41
+ async execute(input, ctx) {
42
42
  return ops.grep(input.pattern, {
43
43
  glob: input.glob,
44
- ignoreCase: input.ignoreCase
44
+ ignoreCase: input.ignoreCase,
45
+ ...ctx?.signal ? { signal: ctx.signal } : {}
45
46
  });
46
47
  }
47
48
  });
@@ -1 +1 @@
1
- {"version":3,"file":"grep.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/grep.ts"],"sourcesContent":["import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport { objectSchema, optionalBooleanField, optionalStringField, stringField } from \"./schema\";\nimport { GREP_JSON_SCHEMA } from \"./json-schemas\";\nimport type { GrepInput, GrepResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGrepTool} to customize the vended tool. */\nexport interface MakeGrepToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"grep\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * search surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GrepInput} — `pattern` is a required string;\n * `glob` and `ignoreCase` are optional. Built on the package's shared,\n * dependency-free schema builders (no schema library, matching the\n * validator idiom the `@warlock.js/ai` tool runtime expects).\n */\nconst grepInputSchema = objectSchema<GrepInput>(\n {\n pattern: stringField(),\n glob: optionalStringField(),\n ignoreCase: optionalBooleanField(),\n },\n GREP_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `grep` tool — a regex content search across the\n * jailed file set. The returned {@link ToolContract} validates the LLM's\n * arguments, then delegates verbatim to {@link WorkspaceOps.grep}, so the\n * policy jail, `denyPaths` filtering, and match cap are enforced in the\n * single shared ops layer rather than duplicated here. A policy violation\n * (e.g. a jail-resolution failure) surfaces as typed tool-error *data*\n * via the runtime's `invoke()` wrapper, never as a thrown run-killer.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `grep`.\n *\n * @example\n * const grep = makeGrepTool(ops);\n * const { data } = await grep.invoke({ pattern: \"TODO\", glob: \"src/*.ts\" });\n * console.log(data?.total, data?.matches);\n */\nexport function makeGrepTool(\n ops: WorkspaceOps,\n options?: MakeGrepToolOptions,\n): ToolContract<GrepInput, GrepResult> {\n return tool<GrepInput, GrepResult>({\n name: options?.name ?? \"grep\",\n description:\n \"Search file contents across the workspace for a regular-expression \" +\n \"pattern. Optionally narrow the scanned files with a glob and match \" +\n \"case-insensitively. Returns every matching line with its file path \" +\n \"and 1-based line number.\",\n action: (input) => `Searching for /${input.pattern}/`,\n input: grepInputSchema,\n async execute(input) {\n return ops.grep(input.pattern, {\n glob: input.glob,\n ignoreCase: input.ignoreCase,\n });\n },\n });\n}\n"],"mappings":";;;;;;;;;;;AAqBA,MAAM,kBAAkB,aACtB;CACE,SAAS,YAAY;CACrB,MAAM,oBAAoB;CAC1B,YAAY,qBAAqB;AACnC,GACA,gBACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aACd,KACA,SACqC;CACrC,OAAO,KAA4B;EACjC,MAAM,SAAS,QAAQ;EACvB,aACE;EAIF,SAAS,UAAU,kBAAkB,MAAM,QAAQ;EACnD,OAAO;EACP,MAAM,QAAQ,OAAO;GACnB,OAAO,IAAI,KAAK,MAAM,SAAS;IAC7B,MAAM,MAAM;IACZ,YAAY,MAAM;GACpB,CAAC;EACH;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"grep.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/grep.ts"],"sourcesContent":["import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport { objectSchema, optionalBooleanField, optionalStringField, stringField } from \"./schema\";\nimport { GREP_JSON_SCHEMA } from \"./json-schemas\";\nimport type { GrepInput, GrepResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGrepTool} to customize the vended tool. */\nexport interface MakeGrepToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"grep\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * search surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GrepInput} — `pattern` is a required string;\n * `glob` and `ignoreCase` are optional. Built on the package's shared,\n * dependency-free schema builders (no schema library, matching the\n * validator idiom the `@warlock.js/ai` tool runtime expects).\n */\nconst grepInputSchema = objectSchema<GrepInput>(\n {\n pattern: stringField(),\n glob: optionalStringField(),\n ignoreCase: optionalBooleanField(),\n },\n GREP_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `grep` tool — a regex content search across the\n * jailed file set. The returned {@link ToolContract} validates the LLM's\n * arguments, then delegates verbatim to {@link WorkspaceOps.grep}, so the\n * policy jail, `denyPaths` filtering, and match cap are enforced in the\n * single shared ops layer rather than duplicated here. A policy violation\n * (e.g. a jail-resolution failure) surfaces as typed tool-error *data*\n * via the runtime's `invoke()` wrapper, never as a thrown run-killer.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `grep`.\n *\n * @example\n * const grep = makeGrepTool(ops);\n * const { data } = await grep.invoke({ pattern: \"TODO\", glob: \"src/*.ts\" });\n * console.log(data?.total, data?.matches);\n */\nexport function makeGrepTool(\n ops: WorkspaceOps,\n options?: MakeGrepToolOptions,\n): ToolContract<GrepInput, GrepResult> {\n return tool<GrepInput, GrepResult>({\n name: options?.name ?? \"grep\",\n description:\n \"Search file contents across the workspace for a regular-expression \" +\n \"pattern. Optionally narrow the scanned files with a glob and match \" +\n \"case-insensitively. Returns every matching line with its file path \" +\n \"and 1-based line number.\",\n action: (input) => `Searching for /${input.pattern}/`,\n input: grepInputSchema,\n async execute(input, ctx) {\n return ops.grep(input.pattern, {\n glob: input.glob,\n ignoreCase: input.ignoreCase,\n ...(ctx?.signal ? { signal: ctx.signal } : {}),\n });\n },\n });\n}\n"],"mappings":";;;;;;;;;;;AAqBA,MAAM,kBAAkB,aACtB;CACE,SAAS,YAAY;CACrB,MAAM,oBAAoB;CAC1B,YAAY,qBAAqB;AACnC,GACA,gBACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aACd,KACA,SACqC;CACrC,OAAO,KAA4B;EACjC,MAAM,SAAS,QAAQ;EACvB,aACE;EAIF,SAAS,UAAU,kBAAkB,MAAM,QAAQ;EACnD,OAAO;EACP,MAAM,QAAQ,OAAO,KAAK;GACxB,OAAO,IAAI,KAAK,MAAM,SAAS;IAC7B,MAAM,MAAM;IACZ,YAAY,MAAM;IAClB,GAAI,KAAK,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;GAC9C,CAAC;EACH;CACF,CAAC;AACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"run-shell.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/run-shell.ts"],"mappings":";;;;;;iBA2EiB;;EAEf;;;;;;EAMA;;;;;;;EAOA;;;;;;EAMA;;EAEA;;EAEA,WAAW,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAmFJ,iBACd,KAAK,cACL,UAAU;EAA4B;IACrC,aAAa,eAAe;wBACf,iBACd,KAAK,cACL,SAAS;EAA4B;IACpC,aAAa,eAAe,iBAAiB;wBAChC,iBACd,KAAK,cACL,UAAU,0BACT,aAAa,eAAe,iBAAiB"}
1
+ {"version":3,"file":"run-shell.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/run-shell.ts"],"mappings":";;;;;;iBA2EiB;;EAEf;;;;;;EAMA;;;;;;;EAOA;;;;;;EAMA;;EAEA;;EAEA,WAAW,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAwFJ,iBACd,KAAK,cACL,UAAU;EAA4B;IACrC,aAAa,eAAe;wBACf,iBACd,KAAK,cACL,SAAS;EAA4B;IACpC,aAAa,eAAe,iBAAiB;wBAChC,iBACd,KAAK,cACL,UAAU,0BACT,aAAa,eAAe,iBAAiB"}
@@ -46,6 +46,7 @@ const DIALECT_RULES = {
46
46
  powershell: "Use PowerShell syntax: `;`, `&&`, `||`, pipes, `$( )`, the call operator `&` and the redirections `>`, `>>`, `2>` and `2>&1` are supported, and every command in the line is checked (aliases such as `rm` and `Remove-Item` are treated alike). Script blocks, `Invoke-Expression`, here-strings, splatting and `if` / `foreach` are not.",
47
47
  powershell5: "Use Windows PowerShell 5.1 syntax: `;`, pipes, `$( )`, the call operator `&` and the redirections `>`, `>>`, `2>` and `2>&1` are supported, and every command in the line is checked (aliases such as `rm` and `Remove-Item` are treated alike). This version cannot chain with `&&` or `||`: put one command per call, or join with `;` (which runs the next command whatever the exit code). Script blocks, `Invoke-Expression`, here-strings, splatting and `if` / `foreach` are not supported."
48
48
  };
49
+ const WINDOWS_BASH_PATH_RULE = " Write paths with forward slashes (`cd ab/terra4/client`, `C:/work/app`): bash reads an unquoted backslash as an escape, so a line with `ab\\terra4` is refused.";
49
50
  const PLATFORM_NAMES = {
50
51
  win32: "Windows",
51
52
  darwin: "macOS",
@@ -56,7 +57,7 @@ function shellSentence(options) {
56
57
  const dialect = options?.shellDialect;
57
58
  if (options?.shellLabel === void 0 || dialect === void 0 || dialect === "none") return "";
58
59
  const platform = options.platform ?? process.platform;
59
- return ` Commands run on ${PLATFORM_NAMES[platform] ?? platform} using ${options.shellLabel}. ` + DIALECT_RULES[dialect];
60
+ return ` Commands run on ${PLATFORM_NAMES[platform] ?? platform} using ${options.shellLabel}. ` + DIALECT_RULES[dialect] + (platform === "win32" && options.shellLabel === "bash" ? WINDOWS_BASH_PATH_RULE : "");
60
61
  }
61
62
  /** The lower of the model's timeout and the cap (either may be absent). */
62
63
  function cappedTimeout(requested, cap) {
@@ -1 +1 @@
1
- {"version":3,"file":"run-shell.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/run-shell.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { WorkspacePolicyError } from \"../errors\";\nimport type {\n RunShellBackgroundResult,\n RunShellInput,\n RunShellResult,\n WorkspaceOps,\n} from \"../contracts\";\nimport { runShellJsonSchema } from \"./json-schemas\";\nimport { withJsonSchema } from \"./schema\";\n\n/** The default tool name `run_shell` is exposed to the LLM under. */\nconst DEFAULT_RUN_SHELL_TOOL_NAME = \"run_shell\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunShellInput}. We validate the\n * model's arguments without a runtime schema dependency: `command` must be\n * a non-empty string, `timeoutMs` (when present) a positive number and\n * `background` (when present) a boolean. Invalid args surface as a\n * `SchemaValidationError` in the tool result's `error` field rather than\n * reaching `ops.exec`.\n */\nfunction runShellInputSchema(background: boolean): StandardSchemaV1<RunShellInput> {\n const schema: StandardSchemaV1<RunShellInput> = {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n if (typeof value !== \"object\" || value === null) {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.command !== \"string\" || candidate.command.length === 0) {\n return {\n issues: [{ message: \"command must be a non-empty string\", path: [\"command\"] }],\n };\n }\n\n if (\n candidate.timeoutMs !== undefined &&\n (typeof candidate.timeoutMs !== \"number\" || candidate.timeoutMs <= 0)\n ) {\n return {\n issues: [{ message: \"timeoutMs must be a positive number\", path: [\"timeoutMs\"] }],\n };\n }\n\n if (candidate.background !== undefined && typeof candidate.background !== \"boolean\") {\n return {\n issues: [{ message: \"background must be a boolean\", path: [\"background\"] }],\n };\n }\n\n const result: RunShellInput = { command: candidate.command };\n\n if (typeof candidate.timeoutMs === \"number\") {\n result.timeoutMs = candidate.timeoutMs;\n }\n\n if (candidate.background === true) {\n result.background = true;\n }\n\n return { value: result };\n },\n },\n };\n\n return withJsonSchema(schema, runShellJsonSchema(background));\n}\n\n/** Options for {@link makeRunShellTool}. */\nexport interface MakeRunShellToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_shell\"`). */\n name?: string;\n /**\n * Upper bound for a model-supplied `timeoutMs` on a foreground command —\n * the model can lower the timeout, never raise it past this. The\n * workspace passes its `shell.timeoutMs`.\n */\n maxTimeoutMs?: number;\n /**\n * Accept `background: true` and start the command as a background job\n * (`ops.startShell`), returning a {@link RunShellBackgroundResult}. The\n * workspace sets it when the policy has `shell.background`. Default\n * `false`: a background request is refused.\n */\n background?: boolean;\n /**\n * The name of the shell lines run in (for example `bash` or\n * `PowerShell 7 (pwsh)`). When set, the description tells the model which\n * shell and syntax rules apply. Absent ⇒ argv mode, no shell.\n */\n shellLabel?: string;\n /** The grammar of that shell; selects the syntax rules in the description. */\n shellDialect?: \"posix\" | \"cmd\" | \"powershell\" | \"powershell5\" | \"none\";\n /** The operating system named in the description. Default: the running one. */\n platform?: NodeJS.Platform;\n}\n\nconst DIALECT_RULES: Record<\"posix\" | \"cmd\" | \"powershell\" | \"powershell5\", string> = {\n posix:\n \"Use POSIX shell syntax: `;`, `&&`, `||`, pipes, quotes, `$( )` and redirections are supported, \" +\n \"and every command in the line is checked. Compound statements (`if`, `for`), brace expansion, \" +\n \"arithmetic and `&` are not.\",\n cmd:\n \"Use cmd.exe syntax: `&&`, `||`, `&`, `|` and simple redirections are supported, and every command \" +\n \"in the line is checked. `%` and `!` expansion, `^`, parentheses and `if` / `for` are not.\",\n powershell:\n \"Use PowerShell syntax: `;`, `&&`, `||`, pipes, `$( )`, the call operator `&` and the redirections \" +\n \"`>`, `>>`, `2>` and `2>&1` are supported, and every command in the line is checked (aliases such as \" +\n \"`rm` and `Remove-Item` are treated alike). Script blocks, `Invoke-Expression`, here-strings, \" +\n \"splatting and `if` / `foreach` are not.\",\n powershell5:\n \"Use Windows PowerShell 5.1 syntax: `;`, pipes, `$( )`, the call operator `&` and the redirections \" +\n \"`>`, `>>`, `2>` and `2>&1` are supported, and every command in the line is checked (aliases such as \" +\n \"`rm` and `Remove-Item` are treated alike). This version cannot chain with `&&` or `||`: put one \" +\n \"command per call, or join with `;` (which runs the next command whatever the exit code). Script \" +\n \"blocks, `Invoke-Expression`, here-strings, splatting and `if` / `foreach` are not supported.\",\n};\n\nconst PLATFORM_NAMES: Partial<Record<NodeJS.Platform, string>> = {\n win32: \"Windows\",\n darwin: \"macOS\",\n linux: \"Linux\",\n};\n\n/** The sentence that names the shell and its syntax, or `\"\"` in argv mode. */\nfunction shellSentence(options: MakeRunShellToolOptions | undefined): string {\n const dialect = options?.shellDialect;\n\n if (options?.shellLabel === undefined || dialect === undefined || dialect === \"none\") {\n return \"\";\n }\n\n const platform = options.platform ?? process.platform;\n\n return (\n ` Commands run on ${PLATFORM_NAMES[platform] ?? platform} using ${options.shellLabel}. ` +\n DIALECT_RULES[dialect]\n );\n}\n\n/** The lower of the model's timeout and the cap (either may be absent). */\nfunction cappedTimeout(requested: number | undefined, cap: number | undefined): number | undefined {\n if (requested === undefined) {\n return cap;\n }\n\n return cap === undefined ? requested : Math.min(requested, cap);\n}\n\n/**\n * Build the `run_shell` tool — a {@link ToolContract} that runs a command\n * line through the policy-enforced {@link WorkspaceOps} layer.\n *\n * Every command in the line is checked against the shell policy by\n * `ops.exec`; a refused line throws a `WorkspacePolicyError` (with a\n * `reason` and a message that says how to fix the command) which the\n * `tool()` runtime catches and surfaces in the result's `error` field\n * (never a thrown run-killer), so the agent reads the refusal as tool data\n * and self-corrects. A command that runs but exits non-zero is *not* an\n * error — its `exitCode`/`stderr` come back in `data` for the agent to\n * inspect.\n *\n * The agent run's cancellation `signal` (from the dispatch `ctx`) is passed\n * to `ops.exec` per invocation: a cancelled run never starts the command,\n * and one cancelled mid-command kills its process tree (`cancelled: true`).\n * A background command (`background: true`, when enabled) belongs to the\n * workspace instead and keeps running after the call returns.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Tool-name override, timeout cap and background support.\n *\n * @example\n * const runShell = makeRunShellTool(ops);\n * const { data, error } = await runShell.invoke({ command: \"npm run build\" });\n * if (error) handleDenied(error);\n * else console.log(data.exitCode, data.stdout);\n */\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions & { background?: false },\n): ToolContract<RunShellInput, RunShellResult>;\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options: MakeRunShellToolOptions & { background: true },\n): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult>;\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions,\n): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult>;\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions,\n): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult> {\n const background = options?.background === true;\n\n return tool<RunShellInput, RunShellResult | RunShellBackgroundResult>({\n name: options?.name ?? DEFAULT_RUN_SHELL_TOOL_NAME,\n description:\n \"Run a shell command inside the workspace. Every command in it must be \" +\n \"permitted by the shell policy; a refusal explains how to fix the \" +\n \"command. Output is byte-capped and the run is time-limited. A \" +\n \"non-zero exit code is returned as data, not an error.\" +\n shellSentence(options) +\n (background\n ? \" Pass background: true for a long-running command (dev server, \" +\n \"watcher): it returns an id at once; read its output with \" +\n \"shell_output and stop it with shell_kill.\"\n : \"\"),\n action: (input) => `Running \\`${input.command}\\``,\n input: runShellInputSchema(background),\n // The run's signal belongs to this one invocation: it is read from the\n // dispatch ctx on every call and never stored on the tool or workspace.\n execute: async (input, ctx) => {\n if (input.background === true) {\n if (!background) {\n throw new WorkspacePolicyError(\n \"Background commands are not enabled for this workspace; run the command without background.\",\n { type: \"denied-command\", reason: \"not-allowed\", command: input.command },\n );\n }\n\n const job = await ops.startShell(\n input.command,\n input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : undefined,\n );\n\n return { ...job, background: true };\n }\n\n const timeoutMs = cappedTimeout(input.timeoutMs, options?.maxTimeoutMs);\n\n return ops.exec(input.command, {\n ...(timeoutMs !== undefined ? { timeoutMs } : {}),\n ...(ctx?.signal ? { signal: ctx.signal } : {}),\n });\n },\n });\n}\n"],"mappings":";;;;;;;AAaA,MAAM,8BAA8B;;;;;;;;;AAUpC,SAAS,oBAAoB,YAAsD;CAgDjF,OAAO,eAAe,EA9CpB,aAAa;EACX,SAAS;EACT,QAAQ;EACR,WAAW,UAAU;GACnB,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,qBAAqB,CAAC,EAAE;GAGvD,MAAM,YAAY;GAElB,IAAI,OAAO,UAAU,YAAY,YAAY,UAAU,QAAQ,WAAW,GACxE,OAAO,EACL,QAAQ,CAAC;IAAE,SAAS;IAAsC,MAAM,CAAC,SAAS;GAAE,CAAC,EAC/E;GAGF,IACE,UAAU,cAAc,WACvB,OAAO,UAAU,cAAc,YAAY,UAAU,aAAa,IAEnE,OAAO,EACL,QAAQ,CAAC;IAAE,SAAS;IAAuC,MAAM,CAAC,WAAW;GAAE,CAAC,EAClF;GAGF,IAAI,UAAU,eAAe,UAAa,OAAO,UAAU,eAAe,WACxE,OAAO,EACL,QAAQ,CAAC;IAAE,SAAS;IAAgC,MAAM,CAAC,YAAY;GAAE,CAAC,EAC5E;GAGF,MAAM,SAAwB,EAAE,SAAS,UAAU,QAAQ;GAE3D,IAAI,OAAO,UAAU,cAAc,UACjC,OAAO,YAAY,UAAU;GAG/B,IAAI,UAAU,eAAe,MAC3B,OAAO,aAAa;GAGtB,OAAO,EAAE,OAAO,OAAO;EACzB;CACF,EAGyB,GAAG,mBAAmB,UAAU,CAAC;AAC9D;AA+BA,MAAM,gBAAgF;CACpF,OACE;CAGF,KACE;CAEF,YACE;CAIF,aACE;AAKJ;AAEA,MAAM,iBAA2D;CAC/D,OAAO;CACP,QAAQ;CACR,OAAO;AACT;;AAGA,SAAS,cAAc,SAAsD;CAC3E,MAAM,UAAU,SAAS;CAEzB,IAAI,SAAS,eAAe,UAAa,YAAY,UAAa,YAAY,QAC5E,OAAO;CAGT,MAAM,WAAW,QAAQ,YAAY,QAAQ;CAE7C,OACE,oBAAoB,eAAe,aAAa,SAAS,SAAS,QAAQ,WAAW,MACrF,cAAc;AAElB;;AAGA,SAAS,cAAc,WAA+B,KAA6C;CACjG,IAAI,cAAc,QAChB,OAAO;CAGT,OAAO,QAAQ,SAAY,YAAY,KAAK,IAAI,WAAW,GAAG;AAChE;AA0CA,SAAgB,iBACd,KACA,SACwE;CACxE,MAAM,aAAa,SAAS,eAAe;CAE3C,OAAO,KAA+D;EACpE,MAAM,SAAS,QAAQ;EACvB,aACE,+PAIA,cAAc,OAAO,KACpB,aACG,sKAGA;EACN,SAAS,UAAU,aAAa,MAAM,QAAQ;EAC9C,OAAO,oBAAoB,UAAU;EAGrC,SAAS,OAAO,OAAO,QAAQ;GAC7B,IAAI,MAAM,eAAe,MAAM;IAC7B,IAAI,CAAC,YACH,MAAM,IAAI,qBACR,+FACA;KAAE,MAAM;KAAkB,QAAQ;KAAe,SAAS,MAAM;IAAQ,CAC1E;IAQF,OAAO;KAAE,GAAG,MALM,IAAI,WACpB,MAAM,SACN,MAAM,cAAc,SAAY,EAAE,WAAW,MAAM,UAAU,IAAI,MACnE;KAEiB,YAAY;IAAK;GACpC;GAEA,MAAM,YAAY,cAAc,MAAM,WAAW,SAAS,YAAY;GAEtE,OAAO,IAAI,KAAK,MAAM,SAAS;IAC7B,GAAI,cAAc,SAAY,EAAE,UAAU,IAAI,CAAC;IAC/C,GAAI,KAAK,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;GAC9C,CAAC;EACH;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"run-shell.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/run-shell.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { WorkspacePolicyError } from \"../errors\";\nimport type {\n RunShellBackgroundResult,\n RunShellInput,\n RunShellResult,\n WorkspaceOps,\n} from \"../contracts\";\nimport { runShellJsonSchema } from \"./json-schemas\";\nimport { withJsonSchema } from \"./schema\";\n\n/** The default tool name `run_shell` is exposed to the LLM under. */\nconst DEFAULT_RUN_SHELL_TOOL_NAME = \"run_shell\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunShellInput}. We validate the\n * model's arguments without a runtime schema dependency: `command` must be\n * a non-empty string, `timeoutMs` (when present) a positive number and\n * `background` (when present) a boolean. Invalid args surface as a\n * `SchemaValidationError` in the tool result's `error` field rather than\n * reaching `ops.exec`.\n */\nfunction runShellInputSchema(background: boolean): StandardSchemaV1<RunShellInput> {\n const schema: StandardSchemaV1<RunShellInput> = {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n if (typeof value !== \"object\" || value === null) {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.command !== \"string\" || candidate.command.length === 0) {\n return {\n issues: [{ message: \"command must be a non-empty string\", path: [\"command\"] }],\n };\n }\n\n if (\n candidate.timeoutMs !== undefined &&\n (typeof candidate.timeoutMs !== \"number\" || candidate.timeoutMs <= 0)\n ) {\n return {\n issues: [{ message: \"timeoutMs must be a positive number\", path: [\"timeoutMs\"] }],\n };\n }\n\n if (candidate.background !== undefined && typeof candidate.background !== \"boolean\") {\n return {\n issues: [{ message: \"background must be a boolean\", path: [\"background\"] }],\n };\n }\n\n const result: RunShellInput = { command: candidate.command };\n\n if (typeof candidate.timeoutMs === \"number\") {\n result.timeoutMs = candidate.timeoutMs;\n }\n\n if (candidate.background === true) {\n result.background = true;\n }\n\n return { value: result };\n },\n },\n };\n\n return withJsonSchema(schema, runShellJsonSchema(background));\n}\n\n/** Options for {@link makeRunShellTool}. */\nexport interface MakeRunShellToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_shell\"`). */\n name?: string;\n /**\n * Upper bound for a model-supplied `timeoutMs` on a foreground command —\n * the model can lower the timeout, never raise it past this. The\n * workspace passes its `shell.timeoutMs`.\n */\n maxTimeoutMs?: number;\n /**\n * Accept `background: true` and start the command as a background job\n * (`ops.startShell`), returning a {@link RunShellBackgroundResult}. The\n * workspace sets it when the policy has `shell.background`. Default\n * `false`: a background request is refused.\n */\n background?: boolean;\n /**\n * The name of the shell lines run in (for example `bash` or\n * `PowerShell 7 (pwsh)`). When set, the description tells the model which\n * shell and syntax rules apply. Absent ⇒ argv mode, no shell.\n */\n shellLabel?: string;\n /** The grammar of that shell; selects the syntax rules in the description. */\n shellDialect?: \"posix\" | \"cmd\" | \"powershell\" | \"powershell5\" | \"none\";\n /** The operating system named in the description. Default: the running one. */\n platform?: NodeJS.Platform;\n}\n\nconst DIALECT_RULES: Record<\"posix\" | \"cmd\" | \"powershell\" | \"powershell5\", string> = {\n posix:\n \"Use POSIX shell syntax: `;`, `&&`, `||`, pipes, quotes, `$( )` and redirections are supported, \" +\n \"and every command in the line is checked. Compound statements (`if`, `for`), brace expansion, \" +\n \"arithmetic and `&` are not.\",\n cmd:\n \"Use cmd.exe syntax: `&&`, `||`, `&`, `|` and simple redirections are supported, and every command \" +\n \"in the line is checked. `%` and `!` expansion, `^`, parentheses and `if` / `for` are not.\",\n powershell:\n \"Use PowerShell syntax: `;`, `&&`, `||`, pipes, `$( )`, the call operator `&` and the redirections \" +\n \"`>`, `>>`, `2>` and `2>&1` are supported, and every command in the line is checked (aliases such as \" +\n \"`rm` and `Remove-Item` are treated alike). Script blocks, `Invoke-Expression`, here-strings, \" +\n \"splatting and `if` / `foreach` are not.\",\n powershell5:\n \"Use Windows PowerShell 5.1 syntax: `;`, pipes, `$( )`, the call operator `&` and the redirections \" +\n \"`>`, `>>`, `2>` and `2>&1` are supported, and every command in the line is checked (aliases such as \" +\n \"`rm` and `Remove-Item` are treated alike). This version cannot chain with `&&` or `||`: put one \" +\n \"command per call, or join with `;` (which runs the next command whatever the exit code). Script \" +\n \"blocks, `Invoke-Expression`, here-strings, splatting and `if` / `foreach` are not supported.\",\n};\n\nconst WINDOWS_BASH_PATH_RULE =\n \" Write paths with forward slashes (`cd ab/terra4/client`, `C:/work/app`): bash reads an unquoted \" +\n \"backslash as an escape, so a line with `ab\\\\terra4` is refused.\";\n\nconst PLATFORM_NAMES: Partial<Record<NodeJS.Platform, string>> = {\n win32: \"Windows\",\n darwin: \"macOS\",\n linux: \"Linux\",\n};\n\n/** The sentence that names the shell and its syntax, or `\"\"` in argv mode. */\nfunction shellSentence(options: MakeRunShellToolOptions | undefined): string {\n const dialect = options?.shellDialect;\n\n if (options?.shellLabel === undefined || dialect === undefined || dialect === \"none\") {\n return \"\";\n }\n\n const platform = options.platform ?? process.platform;\n\n return (\n ` Commands run on ${PLATFORM_NAMES[platform] ?? platform} using ${options.shellLabel}. ` +\n DIALECT_RULES[dialect] +\n (platform === \"win32\" && options.shellLabel === \"bash\" ? WINDOWS_BASH_PATH_RULE : \"\")\n );\n}\n\n/** The lower of the model's timeout and the cap (either may be absent). */\nfunction cappedTimeout(requested: number | undefined, cap: number | undefined): number | undefined {\n if (requested === undefined) {\n return cap;\n }\n\n return cap === undefined ? requested : Math.min(requested, cap);\n}\n\n/**\n * Build the `run_shell` tool — a {@link ToolContract} that runs a command\n * line through the policy-enforced {@link WorkspaceOps} layer.\n *\n * Every command in the line is checked against the shell policy by\n * `ops.exec`; a refused line throws a `WorkspacePolicyError` (with a\n * `reason` and a message that says how to fix the command) which the\n * `tool()` runtime catches and surfaces in the result's `error` field\n * (never a thrown run-killer), so the agent reads the refusal as tool data\n * and self-corrects. A command that runs but exits non-zero is *not* an\n * error — its `exitCode`/`stderr` come back in `data` for the agent to\n * inspect.\n *\n * The agent run's cancellation `signal` (from the dispatch `ctx`) is passed\n * to `ops.exec` per invocation: a cancelled run never starts the command,\n * and one cancelled mid-command kills its process tree (`cancelled: true`).\n * A background command (`background: true`, when enabled) belongs to the\n * workspace instead and keeps running after the call returns.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Tool-name override, timeout cap and background support.\n *\n * @example\n * const runShell = makeRunShellTool(ops);\n * const { data, error } = await runShell.invoke({ command: \"npm run build\" });\n * if (error) handleDenied(error);\n * else console.log(data.exitCode, data.stdout);\n */\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions & { background?: false },\n): ToolContract<RunShellInput, RunShellResult>;\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options: MakeRunShellToolOptions & { background: true },\n): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult>;\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions,\n): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult>;\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions,\n): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult> {\n const background = options?.background === true;\n\n return tool<RunShellInput, RunShellResult | RunShellBackgroundResult>({\n name: options?.name ?? DEFAULT_RUN_SHELL_TOOL_NAME,\n description:\n \"Run a shell command inside the workspace. Every command in it must be \" +\n \"permitted by the shell policy; a refusal explains how to fix the \" +\n \"command. Output is byte-capped and the run is time-limited. A \" +\n \"non-zero exit code is returned as data, not an error.\" +\n shellSentence(options) +\n (background\n ? \" Pass background: true for a long-running command (dev server, \" +\n \"watcher): it returns an id at once; read its output with \" +\n \"shell_output and stop it with shell_kill.\"\n : \"\"),\n action: (input) => `Running \\`${input.command}\\``,\n input: runShellInputSchema(background),\n // The run's signal belongs to this one invocation: it is read from the\n // dispatch ctx on every call and never stored on the tool or workspace.\n execute: async (input, ctx) => {\n if (input.background === true) {\n if (!background) {\n throw new WorkspacePolicyError(\n \"Background commands are not enabled for this workspace; run the command without background.\",\n { type: \"denied-command\", reason: \"not-allowed\", command: input.command },\n );\n }\n\n const job = await ops.startShell(\n input.command,\n input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : undefined,\n );\n\n return { ...job, background: true };\n }\n\n const timeoutMs = cappedTimeout(input.timeoutMs, options?.maxTimeoutMs);\n\n return ops.exec(input.command, {\n ...(timeoutMs !== undefined ? { timeoutMs } : {}),\n ...(ctx?.signal ? { signal: ctx.signal } : {}),\n });\n },\n });\n}\n"],"mappings":";;;;;;;AAaA,MAAM,8BAA8B;;;;;;;;;AAUpC,SAAS,oBAAoB,YAAsD;CAgDjF,OAAO,eAAe,EA9CpB,aAAa;EACX,SAAS;EACT,QAAQ;EACR,WAAW,UAAU;GACnB,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,qBAAqB,CAAC,EAAE;GAGvD,MAAM,YAAY;GAElB,IAAI,OAAO,UAAU,YAAY,YAAY,UAAU,QAAQ,WAAW,GACxE,OAAO,EACL,QAAQ,CAAC;IAAE,SAAS;IAAsC,MAAM,CAAC,SAAS;GAAE,CAAC,EAC/E;GAGF,IACE,UAAU,cAAc,WACvB,OAAO,UAAU,cAAc,YAAY,UAAU,aAAa,IAEnE,OAAO,EACL,QAAQ,CAAC;IAAE,SAAS;IAAuC,MAAM,CAAC,WAAW;GAAE,CAAC,EAClF;GAGF,IAAI,UAAU,eAAe,UAAa,OAAO,UAAU,eAAe,WACxE,OAAO,EACL,QAAQ,CAAC;IAAE,SAAS;IAAgC,MAAM,CAAC,YAAY;GAAE,CAAC,EAC5E;GAGF,MAAM,SAAwB,EAAE,SAAS,UAAU,QAAQ;GAE3D,IAAI,OAAO,UAAU,cAAc,UACjC,OAAO,YAAY,UAAU;GAG/B,IAAI,UAAU,eAAe,MAC3B,OAAO,aAAa;GAGtB,OAAO,EAAE,OAAO,OAAO;EACzB;CACF,EAGyB,GAAG,mBAAmB,UAAU,CAAC;AAC9D;AA+BA,MAAM,gBAAgF;CACpF,OACE;CAGF,KACE;CAEF,YACE;CAIF,aACE;AAKJ;AAEA,MAAM,yBACJ;AAGF,MAAM,iBAA2D;CAC/D,OAAO;CACP,QAAQ;CACR,OAAO;AACT;;AAGA,SAAS,cAAc,SAAsD;CAC3E,MAAM,UAAU,SAAS;CAEzB,IAAI,SAAS,eAAe,UAAa,YAAY,UAAa,YAAY,QAC5E,OAAO;CAGT,MAAM,WAAW,QAAQ,YAAY,QAAQ;CAE7C,OACE,oBAAoB,eAAe,aAAa,SAAS,SAAS,QAAQ,WAAW,MACrF,cAAc,YACb,aAAa,WAAW,QAAQ,eAAe,SAAS,yBAAyB;AAEtF;;AAGA,SAAS,cAAc,WAA+B,KAA6C;CACjG,IAAI,cAAc,QAChB,OAAO;CAGT,OAAO,QAAQ,SAAY,YAAY,KAAK,IAAI,WAAW,GAAG;AAChE;AA0CA,SAAgB,iBACd,KACA,SACwE;CACxE,MAAM,aAAa,SAAS,eAAe;CAE3C,OAAO,KAA+D;EACpE,MAAM,SAAS,QAAQ;EACvB,aACE,+PAIA,cAAc,OAAO,KACpB,aACG,sKAGA;EACN,SAAS,UAAU,aAAa,MAAM,QAAQ;EAC9C,OAAO,oBAAoB,UAAU;EAGrC,SAAS,OAAO,OAAO,QAAQ;GAC7B,IAAI,MAAM,eAAe,MAAM;IAC7B,IAAI,CAAC,YACH,MAAM,IAAI,qBACR,+FACA;KAAE,MAAM;KAAkB,QAAQ;KAAe,SAAS,MAAM;IAAQ,CAC1E;IAQF,OAAO;KAAE,GAAG,MALM,IAAI,WACpB,MAAM,SACN,MAAM,cAAc,SAAY,EAAE,WAAW,MAAM,UAAU,IAAI,MACnE;KAEiB,YAAY;IAAK;GACpC;GAEA,MAAM,YAAY,cAAc,MAAM,WAAW,SAAS,YAAY;GAEtE,OAAO,IAAI,KAAK,MAAM,SAAS;IAC7B,GAAI,cAAc,SAAY,EAAE,UAAU,IAAI,CAAC;IAC/C,GAAI,KAAK,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;GAC9C,CAAC;EACH;CACF,CAAC;AACH"}
package/esm/workspace.mjs CHANGED
@@ -236,8 +236,8 @@ var WorkspaceImpl = class WorkspaceImpl {
236
236
  grep(pattern, opts) {
237
237
  return this.ops.grep(pattern, opts);
238
238
  }
239
- glob(pattern) {
240
- return this.ops.glob(pattern);
239
+ glob(pattern, opts) {
240
+ return this.ops.glob(pattern, opts);
241
241
  }
242
242
  exists(filePath) {
243
243
  return this.ops.exists(filePath);
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"sourcesContent":["import path from \"node:path\";\nimport { ai, type ToolContract } from \"@warlock.js/ai\";\nimport { createLocalBackend } from \"./backends/local\";\nimport { createMockBackend } from \"./backends/mock\";\nimport { WorkspacePolicyError } from \"./errors\";\nimport { createOps } from \"./ops\";\nimport { samePattern } from \"./policy/approval-helpers\";\nimport { classifyShellCommand } from \"./policy/shell-check\";\nimport { resolveShell, type ResolvedShell } from \"./policy/shell-interpreter\";\nimport { ShellJobs } from \"./shell-jobs\";\nimport { TodoStore } from \"./todos\";\nimport { applyPatchWithOps } from \"./patch/apply-patch-generic\";\nimport { makeApplyPatchTool } from \"./tools/apply-patch\";\nimport { makeEditFileTool } from \"./tools/edit-file\";\nimport { makeGlobTool } from \"./tools/glob\";\nimport { makeGrepTool } from \"./tools/grep\";\nimport { makeListDirTool } from \"./tools/list-dir\";\nimport { makeMultiEditTool } from \"./tools/multi-edit\";\nimport { makeReadFileTool } from \"./tools/read-file\";\nimport { makeRunShellTool } from \"./tools/run-shell\";\nimport { makeRunTestsTool } from \"./tools/run-tests\";\nimport { makeShellKillTool } from \"./tools/shell-kill\";\nimport { makeShellOutputTool } from \"./tools/shell-output\";\nimport { makeTodoReadTool, makeTodoWriteTool } from \"./tools/todo\";\nimport { makeWriteFileTool } from \"./tools/write-file\";\nimport type {\n ApplyPatchInput,\n ApplyPatchResult,\n EditFileInput,\n EditFileResult,\n ExecOptions,\n GrepResult,\n ListDirOptions,\n ListDirResult,\n MultiEditInput,\n MultiEditResult,\n ReadFileResult,\n RunShellResult,\n ShellJobInfo,\n ShellOutputOptions,\n ShellOutputResult,\n StartShellOptions,\n TodoListResult,\n Workspace,\n WorkspaceBackend,\n WorkspaceCommandPattern,\n WorkspaceOps,\n WorkspaceShellClassification,\n WorkspacePolicy,\n WorkspaceTodoInput,\n WorkspaceToolName,\n WorkspaceTools,\n} from \"./contracts\";\n\n/**\n * The full set of tool names a writable workspace vends, in a stable\n * canonical order so `tools.all()` is deterministic.\n */\nconst ALL_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"editFile\",\n \"multiEdit\",\n \"applyPatch\",\n \"writeFile\",\n \"listDir\",\n \"runShell\",\n \"shellOutput\",\n \"shellKill\",\n \"runTests\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/**\n * The subset a {@link Workspace.readonly} projection exposes — the\n * tools that cannot change the tree. `editFile` / `multiEdit` /\n * `writeFile` / the shell tools / `runTests` are deliberately omitted so a\n * reviewer agent has no path to change it. The todo tools stay: they only\n * touch the in-memory plan.\n */\nconst READONLY_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"listDir\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/** Tools vended only when the policy enables background commands. */\nconst BACKGROUND_TOOL_NAMES: ReadonlySet<WorkspaceToolName> = new Set([\"shellOutput\", \"shellKill\"]);\n\n/**\n * State one workspace shares with every projection made from it: its\n * background commands and its todo list.\n */\ninterface WorkspaceState {\n jobs: ShellJobs;\n todos: TodoStore;\n /** Command patterns the host approved with `remember`. */\n approvals: WorkspaceCommandPattern[];\n}\n\n/**\n * Choose the dumb IO executor for a policy. `\"mock\"` selects the\n * in-memory backend (hermetic tests); anything else — including the\n * `\"local\"` default and an absent `backend` — selects the real-disk\n * local backend.\n */\nfunction selectBackend(policy: WorkspacePolicy): WorkspaceBackend {\n if (policy.backend === \"mock\") {\n return createMockBackend();\n }\n\n return createLocalBackend();\n}\n\n/**\n * The internal {@link Workspace} implementation. Holds the resolved\n * backend, the policy, and the single shared {@link WorkspaceOps} seam\n * that both the agent-facing `.tools.*` factories and the human-facing\n * direct methods funnel through — one jail, one rule set, two callers.\n *\n * The `allowedTools` set narrows what `tools.*` will vend and which\n * mutating direct methods are permitted: a full workspace allows every\n * name; a {@link WorkspaceImpl.readonly} projection allows only the\n * read/grep/glob subset and rejects writes/edits/shell/mkdir/remove.\n *\n * Constructed via {@link workspace}; the class itself is internal.\n */\nclass WorkspaceImpl implements Workspace {\n /** The shared, policy-enforced operation layer (jail + guards). */\n private readonly ops: WorkspaceOps;\n\n /** Tool names this projection is permitted to vend / mutate through. */\n private readonly allowedTools: ReadonlySet<WorkspaceToolName>;\n\n public readonly policy: WorkspacePolicy;\n\n public readonly tools: WorkspaceTools;\n\n public readonly shell: {\n classify(line: string): WorkspaceShellClassification;\n remembered(): readonly WorkspaceCommandPattern[];\n forget(pattern: WorkspaceCommandPattern): boolean;\n };\n\n /** The shell the policy's interpreter resolves to on this host. */\n private readonly resolvedShell: ResolvedShell;\n\n /** Background commands + todo list, shared with projections. */\n private readonly state: WorkspaceState;\n\n public constructor(\n policy: WorkspacePolicy,\n allowedTools: readonly WorkspaceToolName[] = ALL_TOOL_NAMES,\n state?: WorkspaceState,\n ) {\n this.policy = policy;\n this.allowedTools = new Set(allowedTools);\n this.state = state ?? { jobs: new ShellJobs(), todos: new TodoStore(), approvals: [] };\n this.resolvedShell = resolveShell(policy.shell);\n const approvals = this.state.approvals;\n\n this.shell = {\n classify: (line) => classifyShellCommand(line, policy),\n remembered: () => [...approvals],\n forget: (pattern) => {\n const index = approvals.findIndex((entry) => samePattern(entry, pattern));\n\n if (index === -1) {\n return false;\n }\n\n approvals.splice(index, 1);\n\n return true;\n },\n };\n\n const backend = selectBackend(policy);\n this.ops = createOps(backend, policy, {\n jobs: this.state.jobs,\n approvals: this.state.approvals,\n });\n this.tools = this.buildTools();\n }\n\n /**\n * Assemble the agent-facing tool namespace. Each factory builds its\n * tool over the shared `ops`; `all()` returns every *allowed* tool in\n * canonical order and `pick(...)` returns the named subset (silently\n * dropping any name this projection does not allow, so a `readonly()`\n * workspace can never be coaxed into vending a mutating tool).\n */\n private buildTools(): WorkspaceTools {\n // Each `make*Tool` returns a precisely-typed\n // `ToolContract<SpecificInput, SpecificOutput>`, but the agent-facing\n // `WorkspaceTools` surface vends the type-erased `ToolContract`\n // (`ToolContract<unknown, unknown>`). Because `ToolContract` puts its\n // input in a contravariant position (`execute(input)` / `action(input)`),\n // a specific contract is not assignable to the erased one — so erase it\n // once, here, through `unknown`. The runtime object is identical; only\n // the static input type is widened for the shared surface.\n const erase = <TInput, TOutput>(contract: ToolContract<TInput, TOutput>): ToolContract =>\n contract as unknown as ToolContract;\n\n const shell = this.policy.shell;\n const background = shell?.background !== undefined;\n const resolvedShell = this.resolvedShell;\n\n const factories: Record<\n WorkspaceToolName,\n (opts?: { name?: string; command?: string }) => ToolContract\n > = {\n readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),\n editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),\n multiEdit: (opts) => erase(makeMultiEditTool(this.ops, opts)),\n applyPatch: (opts) => erase(makeApplyPatchTool(this.ops, opts)),\n writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),\n listDir: (opts) => erase(makeListDirTool(this.ops, opts)),\n runShell: (opts) =>\n erase(\n makeRunShellTool(this.ops, {\n ...(opts?.name !== undefined ? { name: opts.name } : {}),\n ...(shell?.timeoutMs !== undefined ? { maxTimeoutMs: shell.timeoutMs } : {}),\n background,\n ...(resolvedShell.interpreter !== \"none\" ? { shellLabel: resolvedShell.label } : {}),\n shellDialect: resolvedShell.interpreter === \"none\" ? \"none\" : resolvedShell.dialect,\n }),\n ),\n shellOutput: (opts) => erase(makeShellOutputTool(this.ops, opts)),\n shellKill: (opts) => erase(makeShellKillTool(this.ops, opts)),\n runTests: (opts) =>\n erase(\n makeRunTestsTool(this.ops, {\n ...opts,\n interpreter: resolvedShell.interpreter,\n dialect: resolvedShell.dialect,\n }),\n ),\n grep: (opts) => erase(makeGrepTool(this.ops, opts)),\n glob: (opts) => erase(makeGlobTool(this.ops, opts)),\n todoWrite: (opts) => erase(makeTodoWriteTool(this.state.todos, opts)),\n todoRead: (opts) => erase(makeTodoReadTool(this.state.todos, opts)),\n };\n\n const build = (name: WorkspaceToolName, opts?: { name?: string; command?: string }) =>\n factories[name](opts);\n\n // `shell_output` / `shell_kill` only mean something when background\n // commands can be started, so `all()` leaves them out otherwise.\n const vendedByAll = (name: WorkspaceToolName): boolean =>\n this.allowedTools.has(name) && (background || !BACKGROUND_TOOL_NAMES.has(name));\n\n return {\n all: () => ALL_TOOL_NAMES.filter(vendedByAll).map((name) => build(name)),\n pick: (...names: WorkspaceToolName[]) =>\n names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),\n readFile: (opts) => build(\"readFile\", opts),\n editFile: (opts) => build(\"editFile\", opts),\n multiEdit: (opts) => build(\"multiEdit\", opts),\n applyPatch: (opts) => build(\"applyPatch\", opts),\n writeFile: (opts) => build(\"writeFile\", opts),\n listDir: (opts) => build(\"listDir\", opts),\n runShell: (opts) => build(\"runShell\", opts),\n shellOutput: (opts) => build(\"shellOutput\", opts),\n shellKill: (opts) => build(\"shellKill\", opts),\n runTests: (opts) => build(\"runTests\", opts),\n grep: (opts) => build(\"grep\", opts),\n glob: (opts) => build(\"glob\", opts),\n todoWrite: (opts) => build(\"todoWrite\", opts),\n todoRead: (opts) => build(\"todoRead\", opts),\n };\n }\n\n /**\n * Reject a mutating direct method on a read-only projection — surfaced\n * as a {@link WorkspacePolicyError} (the same typed error a denied\n * command produces) so a caller branches on `error.type`.\n */\n private assertWritable(operation: string): void {\n if (this.allowedTools.has(\"writeFile\")) {\n return;\n }\n\n throw new WorkspacePolicyError(\n `Operation \"${operation}\" is not permitted on a read-only workspace.`,\n { type: \"denied-command\", command: operation },\n );\n }\n\n public readFile(\n filePath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<ReadFileResult> {\n return this.ops.readFile(filePath, opts);\n }\n\n public async writeFile(\n filePath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n this.assertWritable(\"writeFile\");\n\n return this.ops.writeFile(filePath, content);\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n this.assertWritable(\"editFile\");\n\n return this.ops.editFile(input);\n }\n\n public async multiEdit(input: MultiEditInput): Promise<MultiEditResult> {\n this.assertWritable(\"multiEdit\");\n\n return this.ops.multiEdit(input);\n }\n\n public async applyPatch(input: ApplyPatchInput): Promise<ApplyPatchResult> {\n this.assertWritable(\"applyPatch\");\n\n return this.ops.applyPatch ? this.ops.applyPatch(input) : applyPatchWithOps(this.ops, input);\n }\n\n public listDir(dirPath?: string, opts?: ListDirOptions): Promise<ListDirResult> {\n return this.ops.listDir(dirPath, opts);\n }\n\n public async exec(command: string, opts?: ExecOptions): Promise<RunShellResult> {\n this.assertWritable(\"exec\");\n\n return this.ops.exec(command, opts);\n }\n\n public async startShell(command: string, opts?: StartShellOptions): Promise<ShellJobInfo> {\n this.assertWritable(\"startShell\");\n\n return this.ops.startShell(command, opts);\n }\n\n public shellOutput(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n return this.ops.shellOutput(id, opts);\n }\n\n public async shellKill(id: string): Promise<ShellJobInfo> {\n this.assertWritable(\"shellKill\");\n\n return this.ops.shellKill(id);\n }\n\n public listShells(): ShellJobInfo[] {\n return this.ops.listShells();\n }\n\n public readTodos(): TodoListResult {\n return this.state.todos.read();\n }\n\n public writeTodos(todos: WorkspaceTodoInput[]): TodoListResult {\n return this.state.todos.write(todos);\n }\n\n public dispose(): Promise<void> {\n return this.ops.dispose();\n }\n\n public grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n return this.ops.grep(pattern, opts);\n }\n\n public glob(pattern: string): Promise<string[]> {\n return this.ops.glob(pattern);\n }\n\n public exists(filePath: string): Promise<boolean> {\n return this.ops.exists(filePath);\n }\n\n public async mkdir(filePath: string): Promise<void> {\n this.assertWritable(\"mkdir\");\n\n return this.ops.mkdir(filePath);\n }\n\n public async remove(filePath: string): Promise<void> {\n this.assertWritable(\"remove\");\n\n return this.ops.remove(filePath);\n }\n\n /**\n * A read-only projection over the SAME policy — only the read / list /\n * grep / glob / todo tools are vended and every mutating direct method\n * rejects with a {@link WorkspacePolicyError}. A fresh ops/backend is\n * built from the identical policy, so the projection sees the same jailed\n * tree; it shares this workspace's background commands and todo list.\n */\n public readonly(): Workspace {\n return new WorkspaceImpl(\n this.policy,\n READONLY_TOOL_NAMES.filter((name) => this.allowedTools.has(name)),\n this.state,\n );\n }\n\n /**\n * A sub-jailed view rooted at `subdir` (relative to this workspace's\n * `cwd`). Returns a brand-new workspace whose policy is this policy\n * with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,\n * same allow/deny/shell/read sub-policies, but a tighter jail root. It\n * shares this workspace's background commands and todo list.\n */\n public scope(subdir: string): Workspace {\n return new WorkspaceImpl(\n { ...this.policy, cwd: path.join(this.policy.cwd, subdir) },\n [...this.allowedTools],\n this.state,\n );\n }\n}\n\n/**\n * Build a {@link Workspace} — the integrator that wires a\n * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops\n * layer, and the agent-facing tool factories.\n *\n * The backend is chosen from `policy.backend`: `\"mock\"` runs in memory\n * (hermetic tests); the `\"local\"` default (and any absent value) runs\n * over the real disk via `@warlock.js/fs` + `node:child_process`. The\n * returned workspace exposes:\n *\n * - **`tools.*`** — `readFile` / `editFile` / `multiEdit` / `writeFile` /\n * `listDir` / `runShell` / `shellOutput` / `shellKill` / `runTests` /\n * `grep` / `glob` / `todoWrite` / `todoRead`, plus `all()` (every tool)\n * and `pick(...)` (a least-privilege subset).\n * - **direct methods** — `readFile` / `writeFile` / `editFile` /\n * `multiEdit` / `listDir` / `exec` / `startShell` / `shellOutput` /\n * `shellKill` / `listShells` / `grep` / `glob` / `exists` / `mkdir` /\n * `remove` / `readTodos` / `writeTodos`, each delegating to the shared\n * ops layer, and `dispose()`.\n * - **`readonly()`** — a projection that vends only the read-side tools and\n * rejects every mutating direct method.\n * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.\n *\n * Available at runtime as `ai.workspace(policy)` once this module is\n * imported (it registers the verb on the shared `ai` object).\n *\n * @param policy - The policy bounding the workspace (its `cwd` is the jail root).\n * @returns A fully-wired {@link Workspace}.\n *\n * @example\n * const ws = workspace({ cwd: \"/srv/acme-api\", shell: { allow: [\"npm\"], inheritEnv: [\"PATH\"] } });\n * const dev = ai.agent({ model, tools: ws.tools.all() });\n * await dev.execute(\"Make the failing cart-total suite green.\");\n *\n * @example\n * // Least-privilege reviewer — no write, no shell.\n * const reviewer = ai.agent({ model, tools: ws.readonly().tools.all() });\n */\nexport function workspace(policy: WorkspacePolicy): Workspace {\n return new WorkspaceImpl(policy);\n}\n\n/**\n * Attach the `workspace` verb to the `ai` namespace via module augmentation,\n * per the `ai.`-namespace convention. `@warlock.js/ai` now exposes a named `Ai`\n * interface for exactly this, so after a bare `import \"@warlock.js/ai-workspace\"`,\n * `ai.workspace(...)` is globally typed — no view/cast needed.\n */\ndeclare module \"@warlock.js/ai\" {\n interface Ai {\n /** Build a policy-jailed filesystem + shell {@link Workspace}. */\n workspace(policy: WorkspacePolicy): Workspace;\n }\n}\n\n// Runtime registration: attach `workspace` onto the shared `ai` object the\n// moment this package is imported (the augmentation above types it).\nai.workspace = workspace;\n"],"mappings":"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"}
1
+ {"version":3,"file":"workspace.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"sourcesContent":["import path from \"node:path\";\nimport { ai, type ToolContract } from \"@warlock.js/ai\";\nimport { createLocalBackend } from \"./backends/local\";\nimport { createMockBackend } from \"./backends/mock\";\nimport { WorkspacePolicyError } from \"./errors\";\nimport { createOps } from \"./ops\";\nimport { samePattern } from \"./policy/approval-helpers\";\nimport { classifyShellCommand } from \"./policy/shell-check\";\nimport { resolveShell, type ResolvedShell } from \"./policy/shell-interpreter\";\nimport { ShellJobs } from \"./shell-jobs\";\nimport { TodoStore } from \"./todos\";\nimport { applyPatchWithOps } from \"./patch/apply-patch-generic\";\nimport { makeApplyPatchTool } from \"./tools/apply-patch\";\nimport { makeEditFileTool } from \"./tools/edit-file\";\nimport { makeGlobTool } from \"./tools/glob\";\nimport { makeGrepTool } from \"./tools/grep\";\nimport { makeListDirTool } from \"./tools/list-dir\";\nimport { makeMultiEditTool } from \"./tools/multi-edit\";\nimport { makeReadFileTool } from \"./tools/read-file\";\nimport { makeRunShellTool } from \"./tools/run-shell\";\nimport { makeRunTestsTool } from \"./tools/run-tests\";\nimport { makeShellKillTool } from \"./tools/shell-kill\";\nimport { makeShellOutputTool } from \"./tools/shell-output\";\nimport { makeTodoReadTool, makeTodoWriteTool } from \"./tools/todo\";\nimport { makeWriteFileTool } from \"./tools/write-file\";\nimport type {\n ApplyPatchInput,\n ApplyPatchResult,\n EditFileInput,\n EditFileResult,\n ExecOptions,\n GrepResult,\n ListDirOptions,\n ListDirResult,\n MultiEditInput,\n MultiEditResult,\n ReadFileResult,\n RunShellResult,\n ShellJobInfo,\n ShellOutputOptions,\n ShellOutputResult,\n StartShellOptions,\n TodoListResult,\n Workspace,\n WorkspaceBackend,\n WorkspaceCommandPattern,\n WorkspaceOps,\n WorkspaceShellClassification,\n WorkspacePolicy,\n WorkspaceTodoInput,\n WorkspaceToolName,\n WorkspaceTools,\n} from \"./contracts\";\n\n/**\n * The full set of tool names a writable workspace vends, in a stable\n * canonical order so `tools.all()` is deterministic.\n */\nconst ALL_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"editFile\",\n \"multiEdit\",\n \"applyPatch\",\n \"writeFile\",\n \"listDir\",\n \"runShell\",\n \"shellOutput\",\n \"shellKill\",\n \"runTests\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/**\n * The subset a {@link Workspace.readonly} projection exposes — the\n * tools that cannot change the tree. `editFile` / `multiEdit` /\n * `writeFile` / the shell tools / `runTests` are deliberately omitted so a\n * reviewer agent has no path to change it. The todo tools stay: they only\n * touch the in-memory plan.\n */\nconst READONLY_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"listDir\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/** Tools vended only when the policy enables background commands. */\nconst BACKGROUND_TOOL_NAMES: ReadonlySet<WorkspaceToolName> = new Set([\"shellOutput\", \"shellKill\"]);\n\n/**\n * State one workspace shares with every projection made from it: its\n * background commands and its todo list.\n */\ninterface WorkspaceState {\n jobs: ShellJobs;\n todos: TodoStore;\n /** Command patterns the host approved with `remember`. */\n approvals: WorkspaceCommandPattern[];\n}\n\n/**\n * Choose the dumb IO executor for a policy. `\"mock\"` selects the\n * in-memory backend (hermetic tests); anything else — including the\n * `\"local\"` default and an absent `backend` — selects the real-disk\n * local backend.\n */\nfunction selectBackend(policy: WorkspacePolicy): WorkspaceBackend {\n if (policy.backend === \"mock\") {\n return createMockBackend();\n }\n\n return createLocalBackend();\n}\n\n/**\n * The internal {@link Workspace} implementation. Holds the resolved\n * backend, the policy, and the single shared {@link WorkspaceOps} seam\n * that both the agent-facing `.tools.*` factories and the human-facing\n * direct methods funnel through — one jail, one rule set, two callers.\n *\n * The `allowedTools` set narrows what `tools.*` will vend and which\n * mutating direct methods are permitted: a full workspace allows every\n * name; a {@link WorkspaceImpl.readonly} projection allows only the\n * read/grep/glob subset and rejects writes/edits/shell/mkdir/remove.\n *\n * Constructed via {@link workspace}; the class itself is internal.\n */\nclass WorkspaceImpl implements Workspace {\n /** The shared, policy-enforced operation layer (jail + guards). */\n private readonly ops: WorkspaceOps;\n\n /** Tool names this projection is permitted to vend / mutate through. */\n private readonly allowedTools: ReadonlySet<WorkspaceToolName>;\n\n public readonly policy: WorkspacePolicy;\n\n public readonly tools: WorkspaceTools;\n\n public readonly shell: {\n classify(line: string): WorkspaceShellClassification;\n remembered(): readonly WorkspaceCommandPattern[];\n forget(pattern: WorkspaceCommandPattern): boolean;\n };\n\n /** The shell the policy's interpreter resolves to on this host. */\n private readonly resolvedShell: ResolvedShell;\n\n /** Background commands + todo list, shared with projections. */\n private readonly state: WorkspaceState;\n\n public constructor(\n policy: WorkspacePolicy,\n allowedTools: readonly WorkspaceToolName[] = ALL_TOOL_NAMES,\n state?: WorkspaceState,\n ) {\n this.policy = policy;\n this.allowedTools = new Set(allowedTools);\n this.state = state ?? { jobs: new ShellJobs(), todos: new TodoStore(), approvals: [] };\n this.resolvedShell = resolveShell(policy.shell);\n const approvals = this.state.approvals;\n\n this.shell = {\n classify: (line) => classifyShellCommand(line, policy),\n remembered: () => [...approvals],\n forget: (pattern) => {\n const index = approvals.findIndex((entry) => samePattern(entry, pattern));\n\n if (index === -1) {\n return false;\n }\n\n approvals.splice(index, 1);\n\n return true;\n },\n };\n\n const backend = selectBackend(policy);\n this.ops = createOps(backend, policy, {\n jobs: this.state.jobs,\n approvals: this.state.approvals,\n });\n this.tools = this.buildTools();\n }\n\n /**\n * Assemble the agent-facing tool namespace. Each factory builds its\n * tool over the shared `ops`; `all()` returns every *allowed* tool in\n * canonical order and `pick(...)` returns the named subset (silently\n * dropping any name this projection does not allow, so a `readonly()`\n * workspace can never be coaxed into vending a mutating tool).\n */\n private buildTools(): WorkspaceTools {\n // Each `make*Tool` returns a precisely-typed\n // `ToolContract<SpecificInput, SpecificOutput>`, but the agent-facing\n // `WorkspaceTools` surface vends the type-erased `ToolContract`\n // (`ToolContract<unknown, unknown>`). Because `ToolContract` puts its\n // input in a contravariant position (`execute(input)` / `action(input)`),\n // a specific contract is not assignable to the erased one — so erase it\n // once, here, through `unknown`. The runtime object is identical; only\n // the static input type is widened for the shared surface.\n const erase = <TInput, TOutput>(contract: ToolContract<TInput, TOutput>): ToolContract =>\n contract as unknown as ToolContract;\n\n const shell = this.policy.shell;\n const background = shell?.background !== undefined;\n const resolvedShell = this.resolvedShell;\n\n const factories: Record<\n WorkspaceToolName,\n (opts?: { name?: string; command?: string }) => ToolContract\n > = {\n readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),\n editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),\n multiEdit: (opts) => erase(makeMultiEditTool(this.ops, opts)),\n applyPatch: (opts) => erase(makeApplyPatchTool(this.ops, opts)),\n writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),\n listDir: (opts) => erase(makeListDirTool(this.ops, opts)),\n runShell: (opts) =>\n erase(\n makeRunShellTool(this.ops, {\n ...(opts?.name !== undefined ? { name: opts.name } : {}),\n ...(shell?.timeoutMs !== undefined ? { maxTimeoutMs: shell.timeoutMs } : {}),\n background,\n ...(resolvedShell.interpreter !== \"none\" ? { shellLabel: resolvedShell.label } : {}),\n shellDialect: resolvedShell.interpreter === \"none\" ? \"none\" : resolvedShell.dialect,\n }),\n ),\n shellOutput: (opts) => erase(makeShellOutputTool(this.ops, opts)),\n shellKill: (opts) => erase(makeShellKillTool(this.ops, opts)),\n runTests: (opts) =>\n erase(\n makeRunTestsTool(this.ops, {\n ...opts,\n interpreter: resolvedShell.interpreter,\n dialect: resolvedShell.dialect,\n }),\n ),\n grep: (opts) => erase(makeGrepTool(this.ops, opts)),\n glob: (opts) => erase(makeGlobTool(this.ops, opts)),\n todoWrite: (opts) => erase(makeTodoWriteTool(this.state.todos, opts)),\n todoRead: (opts) => erase(makeTodoReadTool(this.state.todos, opts)),\n };\n\n const build = (name: WorkspaceToolName, opts?: { name?: string; command?: string }) =>\n factories[name](opts);\n\n // `shell_output` / `shell_kill` only mean something when background\n // commands can be started, so `all()` leaves them out otherwise.\n const vendedByAll = (name: WorkspaceToolName): boolean =>\n this.allowedTools.has(name) && (background || !BACKGROUND_TOOL_NAMES.has(name));\n\n return {\n all: () => ALL_TOOL_NAMES.filter(vendedByAll).map((name) => build(name)),\n pick: (...names: WorkspaceToolName[]) =>\n names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),\n readFile: (opts) => build(\"readFile\", opts),\n editFile: (opts) => build(\"editFile\", opts),\n multiEdit: (opts) => build(\"multiEdit\", opts),\n applyPatch: (opts) => build(\"applyPatch\", opts),\n writeFile: (opts) => build(\"writeFile\", opts),\n listDir: (opts) => build(\"listDir\", opts),\n runShell: (opts) => build(\"runShell\", opts),\n shellOutput: (opts) => build(\"shellOutput\", opts),\n shellKill: (opts) => build(\"shellKill\", opts),\n runTests: (opts) => build(\"runTests\", opts),\n grep: (opts) => build(\"grep\", opts),\n glob: (opts) => build(\"glob\", opts),\n todoWrite: (opts) => build(\"todoWrite\", opts),\n todoRead: (opts) => build(\"todoRead\", opts),\n };\n }\n\n /**\n * Reject a mutating direct method on a read-only projection — surfaced\n * as a {@link WorkspacePolicyError} (the same typed error a denied\n * command produces) so a caller branches on `error.type`.\n */\n private assertWritable(operation: string): void {\n if (this.allowedTools.has(\"writeFile\")) {\n return;\n }\n\n throw new WorkspacePolicyError(\n `Operation \"${operation}\" is not permitted on a read-only workspace.`,\n { type: \"denied-command\", command: operation },\n );\n }\n\n public readFile(\n filePath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<ReadFileResult> {\n return this.ops.readFile(filePath, opts);\n }\n\n public async writeFile(\n filePath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n this.assertWritable(\"writeFile\");\n\n return this.ops.writeFile(filePath, content);\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n this.assertWritable(\"editFile\");\n\n return this.ops.editFile(input);\n }\n\n public async multiEdit(input: MultiEditInput): Promise<MultiEditResult> {\n this.assertWritable(\"multiEdit\");\n\n return this.ops.multiEdit(input);\n }\n\n public async applyPatch(input: ApplyPatchInput): Promise<ApplyPatchResult> {\n this.assertWritable(\"applyPatch\");\n\n return this.ops.applyPatch ? this.ops.applyPatch(input) : applyPatchWithOps(this.ops, input);\n }\n\n public listDir(dirPath?: string, opts?: ListDirOptions): Promise<ListDirResult> {\n return this.ops.listDir(dirPath, opts);\n }\n\n public async exec(command: string, opts?: ExecOptions): Promise<RunShellResult> {\n this.assertWritable(\"exec\");\n\n return this.ops.exec(command, opts);\n }\n\n public async startShell(command: string, opts?: StartShellOptions): Promise<ShellJobInfo> {\n this.assertWritable(\"startShell\");\n\n return this.ops.startShell(command, opts);\n }\n\n public shellOutput(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n return this.ops.shellOutput(id, opts);\n }\n\n public async shellKill(id: string): Promise<ShellJobInfo> {\n this.assertWritable(\"shellKill\");\n\n return this.ops.shellKill(id);\n }\n\n public listShells(): ShellJobInfo[] {\n return this.ops.listShells();\n }\n\n public readTodos(): TodoListResult {\n return this.state.todos.read();\n }\n\n public writeTodos(todos: WorkspaceTodoInput[]): TodoListResult {\n return this.state.todos.write(todos);\n }\n\n public dispose(): Promise<void> {\n return this.ops.dispose();\n }\n\n public grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean; signal?: AbortSignal },\n ): Promise<GrepResult> {\n return this.ops.grep(pattern, opts);\n }\n\n public glob(pattern: string, opts?: { signal?: AbortSignal }): Promise<string[]> {\n return this.ops.glob(pattern, opts);\n }\n\n public exists(filePath: string): Promise<boolean> {\n return this.ops.exists(filePath);\n }\n\n public async mkdir(filePath: string): Promise<void> {\n this.assertWritable(\"mkdir\");\n\n return this.ops.mkdir(filePath);\n }\n\n public async remove(filePath: string): Promise<void> {\n this.assertWritable(\"remove\");\n\n return this.ops.remove(filePath);\n }\n\n /**\n * A read-only projection over the SAME policy — only the read / list /\n * grep / glob / todo tools are vended and every mutating direct method\n * rejects with a {@link WorkspacePolicyError}. A fresh ops/backend is\n * built from the identical policy, so the projection sees the same jailed\n * tree; it shares this workspace's background commands and todo list.\n */\n public readonly(): Workspace {\n return new WorkspaceImpl(\n this.policy,\n READONLY_TOOL_NAMES.filter((name) => this.allowedTools.has(name)),\n this.state,\n );\n }\n\n /**\n * A sub-jailed view rooted at `subdir` (relative to this workspace's\n * `cwd`). Returns a brand-new workspace whose policy is this policy\n * with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,\n * same allow/deny/shell/read sub-policies, but a tighter jail root. It\n * shares this workspace's background commands and todo list.\n */\n public scope(subdir: string): Workspace {\n return new WorkspaceImpl(\n { ...this.policy, cwd: path.join(this.policy.cwd, subdir) },\n [...this.allowedTools],\n this.state,\n );\n }\n}\n\n/**\n * Build a {@link Workspace} — the integrator that wires a\n * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops\n * layer, and the agent-facing tool factories.\n *\n * The backend is chosen from `policy.backend`: `\"mock\"` runs in memory\n * (hermetic tests); the `\"local\"` default (and any absent value) runs\n * over the real disk via `@warlock.js/fs` + `node:child_process`. The\n * returned workspace exposes:\n *\n * - **`tools.*`** — `readFile` / `editFile` / `multiEdit` / `writeFile` /\n * `listDir` / `runShell` / `shellOutput` / `shellKill` / `runTests` /\n * `grep` / `glob` / `todoWrite` / `todoRead`, plus `all()` (every tool)\n * and `pick(...)` (a least-privilege subset).\n * - **direct methods** — `readFile` / `writeFile` / `editFile` /\n * `multiEdit` / `listDir` / `exec` / `startShell` / `shellOutput` /\n * `shellKill` / `listShells` / `grep` / `glob` / `exists` / `mkdir` /\n * `remove` / `readTodos` / `writeTodos`, each delegating to the shared\n * ops layer, and `dispose()`.\n * - **`readonly()`** — a projection that vends only the read-side tools and\n * rejects every mutating direct method.\n * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.\n *\n * Available at runtime as `ai.workspace(policy)` once this module is\n * imported (it registers the verb on the shared `ai` object).\n *\n * @param policy - The policy bounding the workspace (its `cwd` is the jail root).\n * @returns A fully-wired {@link Workspace}.\n *\n * @example\n * const ws = workspace({ cwd: \"/srv/acme-api\", shell: { allow: [\"npm\"], inheritEnv: [\"PATH\"] } });\n * const dev = ai.agent({ model, tools: ws.tools.all() });\n * await dev.execute(\"Make the failing cart-total suite green.\");\n *\n * @example\n * // Least-privilege reviewer — no write, no shell.\n * const reviewer = ai.agent({ model, tools: ws.readonly().tools.all() });\n */\nexport function workspace(policy: WorkspacePolicy): Workspace {\n return new WorkspaceImpl(policy);\n}\n\n/**\n * Attach the `workspace` verb to the `ai` namespace via module augmentation,\n * per the `ai.`-namespace convention. `@warlock.js/ai` now exposes a named `Ai`\n * interface for exactly this, so after a bare `import \"@warlock.js/ai-workspace\"`,\n * `ai.workspace(...)` is globally typed — no view/cast needed.\n */\ndeclare module \"@warlock.js/ai\" {\n interface Ai {\n /** Build a policy-jailed filesystem + shell {@link Workspace}. */\n workspace(policy: WorkspacePolicy): Workspace;\n }\n}\n\n// Runtime registration: attach `workspace` onto the shared `ai` object the\n// moment this package is imported (the augmentation above types it).\nai.workspace = workspace;\n"],"mappings":"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"}
package/llms-full.txt CHANGED
@@ -168,22 +168,23 @@ Paths are resolved against `cwd` with symlinks collapsed and accepted inside `cw
168
168
 
169
169
  ## The policy surface
170
170
 
171
- | Field | What it does |
172
- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
173
- | `cwd` | **Absolute** jail root. Every path resolves against it and must stay inside. |
174
- | `allowPaths` | Extra readable roots outside `cwd`. |
175
- | `denyPaths` | Globs (`*`, `**`, `?`) blocked **even inside `cwd`** — e.g. `[".git/**", ".env*"]`. A bare dir name (`"node_modules"`) also blocks its contents. |
176
- | `shell.allow` / `shell.deny` | Executable **basenames** matched against every command's argv (after tokenizing — see below). **Deny wins**, case-insensitively. An `allow` list is exhaustive (fail-closed); no `shell` block at all ⇒ nothing may run. |
177
- | `shell.denyPatterns` / `shell.allowCommand` | Argument rules on every command: string prefix patterns (`"git push --force"`) or RegExps, and a predicate `(argv, ctx) => true \| false \| "why"`. |
178
- | `shell.interpreter` | `"none"` (default, argv), `"sh"`, `"bash"`, `"powershell"`, `"cmd"` or `"auto"` (opt-in shell syntax, every command checked); `shell.interpreterPath` sets the shell executable. |
179
- | `shell.rules` / `shell.onAsk` | Ordered `{ match, effect: "allow" \| "ask" \| "deny", reason? }` rules (last match wins, a deny anywhere refuses the line); `onAsk({ command, asks, signal })` returns `{ allow, remember? }`. Refusals: `"approval-required"`, `"approval-denied"`. `ws.shell.classify(line)` lists a line's commands. `remember` takes one pattern or an array and `reason` (≤ 500 chars) reaches the model in the refusal; `ws.shell.remembered()` / `forget(pattern)` list and remove remembered patterns. |
180
- | `shell.background` | `{ maxJobs?, timeoutMs? }` — enables background commands. Absent ⇒ refused. |
181
- | `list.ignore` / `list.maxDepth` / `list.maxEntries` | `list_dir` bounds: hidden globs (default `node_modules`, `.git`), depth cap (5), entry cap (500). |
182
- | `shell.inheritEnv` | Opt-in `process.env` keys to pass through (e.g. `["PATH"]`). **Nothing leaks in otherwise** — a command can't find `node`/`npm` without `PATH`. |
183
- | `shell.env` | Explicit env vars injected into every spawned process (override inherited on collision). |
184
- | `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (process tree SIGKILLed on expiry) and stdout/stderr byte cap (clipped on a character boundary). |
185
- | `read.defaultLines` / `read.maxBytes` | Default read window + a ceiling on the UTF-8 bytes of the returned `content` (line numbers included). See [Read windows and the byte ceiling](#read-windows-and-the-byte-ceiling). |
186
- | `backend` | `"local"` (default — real disk) or `"mock"` (in-memory, for tests). |
171
+ | Field | What it does |
172
+ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
173
+ | `cwd` | **Absolute** jail root. Every path resolves against it and must stay inside. |
174
+ | `allowPaths` | Extra readable roots outside `cwd`. |
175
+ | `denyPaths` | Globs (`*`, `**`, `?`) blocked **even inside `cwd`** — e.g. `[".git/**", ".env*"]`. A bare dir name (`"node_modules"`) also blocks its contents. |
176
+ | `shell.allow` / `shell.deny` | Executable **basenames** matched against every command's argv (after tokenizing — see below). **Deny wins**, case-insensitively. An `allow` list is exhaustive (fail-closed); no `shell` block at all ⇒ nothing may run. |
177
+ | `shell.denyPatterns` / `shell.allowCommand` | Argument rules on every command: string prefix patterns (`"git push --force"`) or RegExps, and a predicate `(argv, ctx) => true \| false \| "why"`. |
178
+ | `shell.interpreter` | `"none"` (default, argv), `"sh"`, `"bash"`, `"powershell"`, `"cmd"` or `"auto"` (opt-in shell syntax, every command checked); `shell.interpreterPath` sets the shell executable. |
179
+ | `shell.rules` / `shell.onAsk` | Ordered `{ match, effect: "allow" \| "ask" \| "deny", reason? }` rules (last match wins, a deny anywhere refuses the line); `onAsk({ command, asks, signal })` returns `{ allow, remember? }`. Refusals: `"approval-required"`, `"approval-denied"`. `ws.shell.classify(line)` lists a line's commands. `remember` takes one pattern or an array and `reason` (≤ 500 chars) reaches the model in the refusal; `ws.shell.remembered()` / `forget(pattern)` list and remove remembered patterns. |
180
+ | `shell.background` | `{ maxJobs?, timeoutMs? }` — enables background commands. Absent ⇒ refused. |
181
+ | `list.ignore` / `list.maxDepth` / `list.maxEntries` | `list_dir` bounds: hidden globs (default `node_modules`, `.git`), depth cap (5), entry cap (500). |
182
+ | `search.skipDirs` / `search.gitignore` / `search.maxFiles` / `search.concurrency` | What `glob` and `grep` walk: folders never entered (default `[".git", "node_modules"]`, matched by name at any depth or by path glob), whether `.gitignore` files are honoured (default `true`), the most files one search looks at before it stops and sets `truncated` + `maxFilesReached` (default 50000), and how many folders are read at once (default 16). `denyPaths` and the jail apply regardless. A pattern whose leading folder is skipped (`node_modules/pkg/*.json`) still searches it. |
183
+ | `shell.inheritEnv` | Opt-in `process.env` keys to pass through (e.g. `["PATH"]`). **Nothing leaks in otherwise** — a command can't find `node`/`npm` without `PATH`. |
184
+ | `shell.env` | Explicit env vars injected into every spawned process (override inherited on collision). |
185
+ | `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (process tree SIGKILLed on expiry) and stdout/stderr byte cap (clipped on a character boundary). |
186
+ | `read.defaultLines` / `read.maxBytes` | Default read window + a ceiling on the UTF-8 bytes of the returned `content` (line numbers included). See [Read windows and the byte ceiling](#read-windows-and-the-byte-ceiling). |
187
+ | `backend` | `"local"` (default — real disk) or `"mock"` (in-memory, for tests). |
187
188
 
188
189
  > **The most common gotcha:** `run_shell` fails to find `node`/`npm` because the env is empty. Add `shell: { allow: ["npm", "node"], inheritEnv: ["PATH"] }`.
189
190
 
@@ -236,6 +237,7 @@ export async function commitWork(cwd: string) {
236
237
 
237
238
  - Shell mode parses the line and checks every simple command — inside `;`, `&&`, `||`, pipes, `( )`, `$( )`, backticks, here-documents and `NAME=value cmd` prefixes. Redirection targets and `cd` directories must be literal paths inside the jail (`/dev/null` is fine), re-checked with symlinks resolved.
238
239
  - It refuses what it can't parse with confidence (`reason: "unsupported-syntax"`): compound commands, arithmetic, brace expansion, unusual `${…}` forms, bash-only redirections, a command name decided at run time, `&` in a line (use `background: true`), assignments to `IFS`/`PS4`/`CDPATH`/`BASH_ENV`…; it drops shell start-up variables (`ENV`, `BASH_ENV`, `SHELLOPTS`, `BASH_FUNC_*`, …) from the environment.
240
+ - Bash on Windows reads an unquoted backslash as an escape: a line like `cd ab\x\y` is refused (`"unsupported-syntax"`) with the forward-slash form (`ab/x/y`) in the message, and `run_shell`'s description says to write paths with forward slashes. Quoted paths and `a\ b` pass; Linux and macOS are unchanged.
239
241
  - `reason` values: `"metacharacter"`, `"unbalanced-quotes"`, `"denied-executable"`, `"not-allowed"`, `"denied-pattern"`, `"nested-command"`, `"unsupported-syntax"`, `"path-escape"`. `checkShellCommand(policy, line)` returns the verdict without running anything; `classifyShellCommand(line, policy?)` returns `{ commands, opaque? }` with the same parser a host approval gate can reuse; `resolveShell(policy.shell)` reports which interpreter and executable `"auto"` (or a named interpreter) resolves to on this host, plus the PowerShell `edition` (`"desktop"` = Windows PowerShell 5.1, `"core"` = 7+) and `version`; the 5.1 dialect (`"powershell5"`) refuses `&&` / `||`, and on Windows `"auto"` prefers `pwsh`, then Git Bash, then 5.1, then `cmd`. The `PowerShellEdition` type is exported.
240
242
  - Pair shell mode with an exhaustive `allow`: `deny` alone is a best-effort filter.
241
243
 
@@ -314,7 +316,7 @@ export async function devLoop(cwd: string) {
314
316
  - `run_shell` → `{ exitCode, stdout, stderr, truncated, timedOut, cancelled, truncation?: { stdout?, stderr? }, spilledTo? }`; one path per spilled stream.
315
317
  - `shell_output` → the fields above under Background, plus `truncation?: { stdout?, stderr? }` and `spilledTo?`.
316
318
  - `read_file` → `{ content, ..., truncated, bytesCapped, truncation?, spilledTo? }`; spills only when `read.maxBytes` cut the window.
317
- - `grep` → `{ matches, total, truncated?, truncation?, spilledTo? }`; `glob` → `{ paths, truncated?, truncation?, spilledTo? }`.
319
+ - `grep` → `{ matches, total, truncated?, truncation?, spilledTo? }`; `glob` → `{ paths, truncated?, truncation?, spilledTo?, maxFilesReached? }`. Both start at the pattern's static folder prefix (`src/api/**/*.ts` reads only `src/api`), never enter `node_modules`, `.git`, `denyPaths` folders or gitignored paths (see `search.*`), and take an optional `signal` that stops the walk.
318
320
  - `truncation` is `{ shown: { lines }, total: { lines, bytes }, path? }`; without `spillDir` there is no `path` and no `spilledTo`.
319
321
  - `list_dir({ path?, depth?, ignore? })` → `{ path, entries: [{ path, type, size?, depth }], truncated }`; `denyPaths` always hidden, `list.ignore` (default `node_modules`, `.git`) hidden from listings, symlinks reported but never followed.
320
322
  - `apply_patch({ patch, dryRun? })` → `{ files: [{ path, action, oldPath?, hunks, hash? }], dryRun }`; a unified diff that modifies, creates, deletes and renames files, all or none; paths jailed, hunks matched by context, line endings kept, binary files refused (`binary-file`); `dryRun` checks only. Not in `readonly()`.
@@ -397,6 +399,23 @@ ai.workspace({
397
399
 
398
400
  After `write_file`, `edit_file`, `multi_edit` or `apply_patch` writes files, the hook runs once per call with the written paths (not on a patch `dryRun`, not for deleted files). Text it returns comes back on the result as `diagnostics`, between `[diagnostics]` and `[/diagnostics]` lines; output over `afterEditMaxChars` is cut with a marker. A hook that throws or does not answer within `afterEditTimeoutMs` yields `diagnostics unavailable` and never fails the edit.
399
401
 
402
+ ## Live shell output — `hooks.onShellOutput`
403
+
404
+ ```ts
405
+ workspace({
406
+ cwd,
407
+ shell: { allow: ["npm"], env: { API_KEY: process.env.API_KEY! } },
408
+ hooks: {
409
+ onShellOutput: ({ command, stream, chunk, jobId, callId }) => ui.append(chunk),
410
+ redactShellOutput: (text) => text.replace(/sk-[a-z0-9]+/g, "[redacted]"),
411
+ },
412
+ });
413
+ ```
414
+
415
+ `run_shell` returns its output when the command ends; `onShellOutput` (event type `WorkspaceShellOutputEvent`) lets a host that shows the session to a person see it while the command runs. It fires for foreground commands and for background jobs (`jobId` is set; it keeps firing after `run_shell` has returned). Text is decoded, in order per stream, delivered at most about 20 times a second (faster output is merged), and never changes the result the agent gets (truncation and spilling are as before). A hook that throws is ignored. Pass `callId` to `ws.exec(command, { callId })` to have it echoed on a foreground command's events. A backend that cannot stream delivers the whole capture once, when the command ends.
416
+
417
+ The workspace redacts nothing in results, so live text is raw unless you mask it. Redaction needs a secret in one piece but a process writes in arbitrary pieces, so output is buffered to line boundaries first and then redacted: a secret split across two reads is whole by the time `redactShellOutput(text)` sees it. The unfinished last line is held until its newline arrives or the command ends (a line over 16 KB is cut, keeping its last 512 characters back). Values in `shell.env` of eight or more characters are masked as `[redacted]` before `redactShellOutput` runs. If the redactor throws, that text is dropped, never delivered unmasked. A secret that contains a newline cannot be matched this way.
418
+
400
419
  ## Oversized output — `output`
401
420
 
402
421
  ```ts
package/package.json CHANGED
@@ -19,13 +19,13 @@
19
19
  "directory": "packages/ai-workspace"
20
20
  },
21
21
  "peerDependencies": {
22
- "@warlock.js/ai": "5.45.0"
22
+ "@warlock.js/ai": "5.47.0"
23
23
  },
24
24
  "dependencies": {
25
25
  "@standard-schema/spec": "^1.0.0",
26
- "@warlock.js/fs": "5.45.0"
26
+ "@warlock.js/fs": "5.47.0"
27
27
  },
28
- "version": "5.45.0",
28
+ "version": "5.47.0",
29
29
  "main": "./cjs/index.cjs",
30
30
  "module": "./esm/index.mjs",
31
31
  "types": "./esm/index.d.mts",