@zaly/agent 0.0.12 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/{agent-Dp4LZDSo.mjs → agent-BRENvuvx.mjs} +8 -11
  2. package/dist/{agent-Dp4LZDSo.mjs.map → agent-BRENvuvx.mjs.map} +1 -1
  3. package/dist/{agent-B7cQOuMZ.mjs → agent-sHtwQDAk.mjs} +2 -3
  4. package/dist/{agent-B7cQOuMZ.mjs.map → agent-sHtwQDAk.mjs.map} +1 -1
  5. package/dist/{bash-COgq4W7U.mjs → bash-DSy03sAj.mjs} +8 -9
  6. package/dist/bash-DSy03sAj.mjs.map +1 -0
  7. package/dist/compactions-HFjEgrv3.mjs +150 -0
  8. package/dist/compactions-HFjEgrv3.mjs.map +1 -0
  9. package/dist/{edit-D4yaBK1K.mjs → edit-DT672zIy.mjs} +2 -3
  10. package/dist/edit-DT672zIy.mjs.map +1 -0
  11. package/dist/{env-UUFv5Mye.mjs → env-C8p8i1E0.mjs} +2 -3
  12. package/dist/{env-UUFv5Mye.mjs.map → env-C8p8i1E0.mjs.map} +1 -1
  13. package/dist/fetch-BrtIqrid.mjs +158 -0
  14. package/dist/fetch-BrtIqrid.mjs.map +1 -0
  15. package/dist/{find-CimUGn-L.mjs → find-B-vvdlHu.mjs} +30 -13
  16. package/dist/find-B-vvdlHu.mjs.map +1 -0
  17. package/dist/{grep-6O3mjSvV.mjs → grep-rvZ5S319.mjs} +19 -10
  18. package/dist/grep-rvZ5S319.mjs.map +1 -0
  19. package/dist/html-VrAsoGBK.mjs +32950 -0
  20. package/dist/html-VrAsoGBK.mjs.map +1 -0
  21. package/dist/{index-node-esm-CJsN8G51.mjs → index-node-esm-BwaOzhXB.mjs} +2 -3
  22. package/dist/{index-node-esm-CJsN8G51.mjs.map → index-node-esm-BwaOzhXB.mjs.map} +1 -1
  23. package/dist/index.d.mts +72 -100
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +3 -3
  26. package/dist/{jsonl-CvgEOakL.mjs → jsonl-BGt7eqPK.mjs} +2 -3
  27. package/dist/{jsonl-CvgEOakL.mjs.map → jsonl-BGt7eqPK.mjs.map} +1 -1
  28. package/dist/{manager-BqlbCGYV.mjs → manager-1zEE5WiA.mjs} +4 -6
  29. package/dist/{manager-BqlbCGYV.mjs.map → manager-1zEE5WiA.mjs.map} +1 -1
  30. package/dist/{markdown-Bv4J8cg3.mjs → markdown-8ICz5aFw.mjs} +2 -3
  31. package/dist/{markdown-Bv4J8cg3.mjs.map → markdown-8ICz5aFw.mjs.map} +1 -1
  32. package/dist/{masker-ZoP381jN.mjs → masker-DEgR_ntk.mjs} +4 -6
  33. package/dist/{masker-ZoP381jN.mjs.map → masker-DEgR_ntk.mjs.map} +1 -1
  34. package/dist/{model-_3ZA2xqm.mjs → model-DqGqxfAc.mjs} +2 -3
  35. package/dist/{model-_3ZA2xqm.mjs.map → model-DqGqxfAc.mjs.map} +1 -1
  36. package/dist/{notify-DpG_m9iP.mjs → notify-Ds0-4BeI.mjs} +2 -3
  37. package/dist/{notify-DpG_m9iP.mjs.map → notify-Ds0-4BeI.mjs.map} +1 -1
  38. package/dist/{read-B-hwcc2Q.mjs → read-C5keKntU.mjs} +4 -44
  39. package/dist/read-C5keKntU.mjs.map +1 -0
  40. package/dist/{registry-L9GDccS0.mjs → registry-CntkeXlo.mjs} +45 -45
  41. package/dist/registry-CntkeXlo.mjs.map +1 -0
  42. package/dist/rolldown-runtime-DYf_mQCB.mjs +34 -0
  43. package/dist/{search-CRw247ES.mjs → search-B-89W3SM.mjs} +20 -17
  44. package/dist/search-B-89W3SM.mjs.map +1 -0
  45. package/dist/session/claude.d.mts +0 -2
  46. package/dist/session/claude.d.mts.map +1 -1
  47. package/dist/session/claude.mjs +4 -6
  48. package/dist/session/claude.mjs.map +1 -1
  49. package/dist/{session-BSvUrr12.mjs → session-C8zqqbPE.mjs} +6 -8
  50. package/dist/{session-BSvUrr12.mjs.map → session-C8zqqbPE.mjs.map} +1 -1
  51. package/dist/{session-Ds5nJz5A.d.mts → session-Cq52cam5.d.mts} +1 -11
  52. package/dist/session-Cq52cam5.d.mts.map +1 -0
  53. package/dist/session.d.mts +1 -3
  54. package/dist/session.d.mts.map +1 -1
  55. package/dist/session.mjs +4 -5
  56. package/dist/{src-C852Q6gy.mjs → src-C04Qk9KE.mjs} +35 -39
  57. package/dist/{src-C852Q6gy.mjs.map → src-C04Qk9KE.mjs.map} +1 -1
  58. package/dist/{subagent-C8XCEecM.mjs → subagent-BbcxLzgC.mjs} +4 -5
  59. package/dist/{subagent-C8XCEecM.mjs.map → subagent-BbcxLzgC.mjs.map} +1 -1
  60. package/dist/{swarm-BJ3gQA1Z.mjs → swarm-B_s9Qpbt.mjs} +2 -3
  61. package/dist/{swarm-BJ3gQA1Z.mjs.map → swarm-B_s9Qpbt.mjs.map} +1 -1
  62. package/dist/{swarm-BuFx_xeY.mjs → swarm-D823Yumk.mjs} +2 -3
  63. package/dist/{swarm-BuFx_xeY.mjs.map → swarm-D823Yumk.mjs.map} +1 -1
  64. package/dist/{tasks-WdRAHsOL.mjs → tasks-D76WXWr2.mjs} +3 -4
  65. package/dist/{tasks-WdRAHsOL.mjs.map → tasks-D76WXWr2.mjs.map} +1 -1
  66. package/dist/{tasks-BA-PCeGa.mjs → tasks-Dgsiuh0Q.mjs} +3 -4
  67. package/dist/{tasks-BA-PCeGa.mjs.map → tasks-Dgsiuh0Q.mjs.map} +1 -1
  68. package/dist/{tools-wvkkuXWH.mjs → tools-DmoUYORG.mjs} +7 -42
  69. package/dist/{tools-wvkkuXWH.mjs.map → tools-DmoUYORG.mjs.map} +1 -1
  70. package/dist/{truncate-BSMXgiXj.mjs → truncate-CChlZ113.mjs} +2 -3
  71. package/dist/{truncate-BSMXgiXj.mjs.map → truncate-CChlZ113.mjs.map} +1 -1
  72. package/dist/{uuid-CAdJg4l8.mjs → uuid-rfbQ9ad2.mjs} +2 -3
  73. package/dist/{uuid-CAdJg4l8.mjs.map → uuid-rfbQ9ad2.mjs.map} +1 -1
  74. package/dist/{wakeup-BYqTnfSp.mjs → wakeup-CFr1nu_L.mjs} +2 -3
  75. package/dist/{wakeup-BYqTnfSp.mjs.map → wakeup-CFr1nu_L.mjs.map} +1 -1
  76. package/dist/{write-DwGiDNeG.mjs → write-BRQFfWAn.mjs} +2 -3
  77. package/dist/write-BRQFfWAn.mjs.map +1 -0
  78. package/package.json +46 -8
  79. package/dist/bash-COgq4W7U.mjs.map +0 -1
  80. package/dist/compactions-CvQs64fS.mjs +0 -191
  81. package/dist/compactions-CvQs64fS.mjs.map +0 -1
  82. package/dist/edit-D4yaBK1K.mjs.map +0 -1
  83. package/dist/fetch-VKhSWdEh.mjs +0 -111
  84. package/dist/fetch-VKhSWdEh.mjs.map +0 -1
  85. package/dist/find-CimUGn-L.mjs.map +0 -1
  86. package/dist/grep-6O3mjSvV.mjs.map +0 -1
  87. package/dist/read-B-hwcc2Q.mjs.map +0 -1
  88. package/dist/registry-L9GDccS0.mjs.map +0 -1
  89. package/dist/search-CRw247ES.mjs.map +0 -1
  90. package/dist/session-Ds5nJz5A.d.mts.map +0 -1
  91. package/dist/write-DwGiDNeG.mjs.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"fetch-VKhSWdEh.mjs","names":[],"sources":["../src/tools/fetch.ts"],"sourcesContent":["import type { Attachment, MetaPart, TextPart } from \"@zaly/ai\"\n\nimport { defineTool, toAttachment } from \"@zaly/ai\"\nimport { Type } from \"typebox\"\n\nconst DEFAULT_MAX_BODY_BYTES = 256 * 1024 // 256 KB — generous for APIs, caps runaway HTML pages\n\n/**\n * Fetch a URL and return the response — JSON when the server speaks JSON,\n * raw text otherwise. Optional `jsonpath` filters the parsed JSON to a\n * subset, which keeps tokens down on chatty APIs.\n *\n * Scope: API endpoints. For HTML pages (browsing, content extraction),\n * the optional `@zaly/browser` package provides browser-backed tools that\n * return a structured a11y tree — much better than raw HTML through this\n * tool. Pointing this at an HTML URL works but the response is a string\n * blob the model has to slog through.\n *\n * Permissions: routes through the `fetch` scope when the agent wires up\n * tool-side permission checks. Until then, callers should restrict via\n * a wrapping permission layer (rule-based or explicit allowlist).\n */\n// oxlint-disable-next-line sort-keys\nexport const fetchTool = defineTool({\n desc:\n `Fetch a URL and return the response. JSON responses are parsed; use \\`jsonpath\\` (e.g. \\`$.items[*].name\\`) to extract a subset and minimise tokens.` +\n `Image & PDF are returned as attachments. Best for APIs — for web pages, use the browser tool.`,\n name: \"fetch\",\n parallel: true,\n\n // oxlint-disable-next-line sort-keys -- semantic param order: url, method, headers, query, body, jsonpath\n params: Type.Object({\n url: Type.String({ description: \"Absolute URL.\" }),\n method: Type.Optional(\n Type.Union(\n [\n Type.Literal(\"GET\"),\n Type.Literal(\"POST\"),\n Type.Literal(\"PUT\"),\n Type.Literal(\"PATCH\"),\n Type.Literal(\"DELETE\"),\n ],\n { default: \"GET\", description: \"HTTP method.\" }\n )\n ),\n headers: Type.Optional(\n Type.Record(Type.String(), Type.String(), {\n description: \"Request headers.\",\n })\n ),\n query: Type.Optional(\n Type.Record(Type.String(), Type.String(), {\n description: \"Query string params, appended to the URL.\",\n })\n ),\n body: Type.Optional(\n Type.String({\n description:\n \"Request body as a string. For JSON, stringify it yourself and \" +\n \"set `headers: { 'Content-Type': 'application/json' }`.\",\n })\n ),\n jsonpath: Type.Optional(\n Type.String({\n description:\n \"JSONPath expression applied to a JSON response, e.g. \" +\n \"`$.items[*].name`. Returns the matched values as an array.\",\n })\n ),\n }),\n async call(args): Promise<(Attachment | MetaPart | TextPart)[]> {\n const t0 = Date.now()\n const url = new URL(args.url)\n if (args.query) {\n for (const [k, v] of Object.entries(args.query)) url.searchParams.set(k, v)\n }\n\n const res = await fetch(url, {\n body: args.body,\n headers: args.headers,\n method: args.method,\n })\n\n const contentType = res.headers.get(\"content-type\") ?? \"\"\n const bytes = new Uint8Array(await res.arrayBuffer())\n\n const meta: Record<string, unknown> = {\n contentType: contentType || undefined,\n durationMs: Date.now() - t0,\n ok: res.ok,\n status: res.status,\n statusText: res.statusText,\n }\n\n // Magic-byte detection on the response bytes — catches mislabeled\n // content (HTML served at /file.pdf, images served as\n // application/octet-stream, etc.). Inlines as base64 (same shape as\n // the `read` tool) so the model gets the bytes without the provider\n // re-fetching from its own IP. Only attach kinds the model accepts;\n // unsupported kinds fall through to the text branch (rendered via\n // `<image>` / `<pdf>` placeholders by the wire pipeline).\n const { fileDetect } = await import(\"@zaly/shared/detect\")\n const file = await fileDetect({\n data: bytes,\n mime: contentType || undefined,\n url: url.toString(),\n })\n if (file) {\n const att = await toAttachment(file)\n if (att) return [{ data: meta, tag: \"fetch\", type: \"meta\" }, att]\n }\n\n const text = new TextDecoder().decode(bytes)\n // `detectTextFormat` (inside fileDetect) already weighs MIME,\n // extension, and content sniff — reuse its verdict instead of a\n // parallel `looksLikeJson` heuristic.\n const isJson = file?.type === \"text\" && file.format === \"json\"\n\n let body: unknown = text\n if (isJson) {\n try {\n body = JSON.parse(text)\n } catch {\n // Server lied about content-type; keep as text.\n }\n }\n\n if (args.jsonpath !== undefined && body !== text) {\n // Lazy-import — `jsonpath-plus` is only paid for when actually used,\n // and it bundles to a few hundred KB which we'd rather not load on\n // every fetch that doesn't filter.\n const { JSONPath } = await import(\"jsonpath-plus\")\n // oxlint-disable-next-line new-cap -- jsonpath-plus exports as TitleCase\n body = JSONPath({ json: body as object, path: args.jsonpath })\n }\n\n // Stringify the body for display. JSON pretty-prints; raw text is\n // used as-is.\n const bodyText = typeof body === \"string\" ? body : JSON.stringify(body, undefined, 2)\n\n // Cap the body before it reaches the model. Total bytes received is\n // surfaced via the meta so the model can decide whether to refetch\n // with `jsonpath` or use a different tool (browser for HTML, etc.).\n const totalBytes = Buffer.byteLength(bodyText, \"utf8\")\n const truncated = totalBytes > DEFAULT_MAX_BODY_BYTES\n const displayed = truncated ? bodyText.slice(0, DEFAULT_MAX_BODY_BYTES) : bodyText\n\n if (truncated) {\n meta.truncated = {\n bytes: totalBytes,\n hint:\n args.jsonpath === undefined\n ? \"body exceeded limit; pass `jsonpath` to filter, or use the browser tool for HTML.\"\n : \"filtered body still exceeded limit; tighten the JSONPath expression.\",\n limit: DEFAULT_MAX_BODY_BYTES,\n }\n }\n\n const parts: (MetaPart | TextPart)[] = [{ data: meta, tag: \"fetch\", type: \"meta\" }]\n if (displayed !== \"\")\n parts.push({ format: isJson ? \"json\" : undefined, text: displayed, type: \"text\" })\n return parts\n },\n})\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"find-CimUGn-L.mjs","names":[],"sources":["../src/tools/find.ts"],"sourcesContent":["import type { MetaPart, TextPart, ToolContext } from \"@zaly/ai\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { find } from \"@zaly/shared/find\"\nimport { Type } from \"typebox\"\nimport { truncate } from \"../utils/truncate.ts\"\n\nconst DEFAULT_LIMIT = 200\nconst MAX_LIMIT = 1000\n\nexport type FindTool = typeof findTool\nexport type FindToolMeta = {\n cwd: string\n glob: string | string[]\n matches: number\n truncated: boolean\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const findTool = defineTool({\n name: \"find\",\n desc:\n \"Find files using fd/fdfind or rg --files. `glob` is a filename/path glob, \" +\n \"array of globs, or plain substring; empty, `*`, or `**/*` lists all files. \" +\n \"Includes hidden files and respects .gitignore by default. Does not follow directory symlinks; \" +\n \"prefer this over bash find for file discovery.\",\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n glob: Type.Optional(\n Type.Union([Type.String(), Type.Array(Type.String())], {\n default: \"\",\n description:\n \"Filename/path glob, array of globs, or plain substring. Empty, `*`, or `**/*` lists all files.\",\n })\n ),\n paths: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Optional directories to search, relative to cwd or absolute.\",\n })\n ),\n hidden: Type.Optional(\n Type.Boolean({\n default: true,\n description: \"Include files in hidden directories and dotfiles.\",\n })\n ),\n ignore: Type.Optional(\n Type.Boolean({ default: true, description: \"Respect .gitignore/.ignore rules.\" })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: DEFAULT_LIMIT,\n description: \"Maximum paths to keep inline.\",\n maximum: MAX_LIMIT,\n minimum: 1,\n })\n ),\n }),\n\n async preflight(args, ctx: ToolContext<FindToolMeta>) {\n const cwd = normPath(ctx.cwd, \".\")\n await ctx.need?.(\"read\", cwd)\n const paths = (args.paths?.length ? args.paths : [\".\"]).map((p) => normPath(cwd, p))\n await Promise.all(paths.map((path) => Promise.resolve(ctx.need?.(\"read\", path))))\n },\n\n async call(args, ctx: ToolContext<FindToolMeta>): Promise<(MetaPart | TextPart)[]> {\n const cwd = normPath(ctx.cwd, \".\")\n\n let result: string[] = []\n try {\n const batch = await Array.fromAsync(\n find({ ...args, cwd, pattern: args.glob, signal: ctx.signal })\n )\n result = batch.flat()\n } catch (error) {\n throw AiError.from(error)\n }\n\n const limit = args.limit ?? DEFAULT_LIMIT\n const summary = truncate(result.join(\"\\n\"), {\n maxLines: limit + 1,\n strategy: \"head\",\n })\n\n const truncated = summary.truncated\n ctx.meta = {\n cwd,\n glob: args.glob ?? \"\",\n matches: summary.origLines,\n truncated,\n }\n\n return [{ text: summary.text || \"No files found.\", type: \"text\" }]\n },\n})\n"],"mappings":";;;;;;AAQA,MAAM,gBAAgB;AAYtB,MAAa,WAAW,WAAW;CACjC,MAAM;CACN,MACE;CAIF,UAAU;CAEV,QAAQ,KAAK,OAAO;EAClB,MAAM,KAAK,SACT,KAAK,MAAM,CAAC,KAAK,OAAO,GAAG,KAAK,MAAM,KAAK,OAAO,CAAC,CAAC,GAAG;GACrD,SAAS;GACT,aACE;EACJ,CAAC,CACH;EACA,OAAO,KAAK,SACV,KAAK,MAAM,KAAK,OAAO,GAAG,EACxB,aAAa,+DACf,CAAC,CACH;EACA,QAAQ,KAAK,SACX,KAAK,QAAQ;GACX,SAAS;GACT,aAAa;EACf,CAAC,CACH;EACA,QAAQ,KAAK,SACX,KAAK,QAAQ;GAAE,SAAS;GAAM,aAAa;EAAoC,CAAC,CAClF;EACA,OAAO,KAAK,SACV,KAAK,QAAQ;GACX,SAAS;GACT,aAAa;GACb,SAAS;GACT,SAAS;EACX,CAAC,CACH;CACF,CAAC;CAED,MAAM,UAAU,MAAM,KAAgC;EACpD,MAAM,MAAM,SAAS,IAAI,KAAK,GAAG;EACjC,MAAM,IAAI,OAAO,QAAQ,GAAG;EAC5B,MAAM,SAAS,KAAK,OAAO,SAAS,KAAK,QAAQ,CAAC,GAAG,EAAA,CAAG,KAAK,MAAM,SAAS,KAAK,CAAC,CAAC;EACnF,MAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,QAAQ,QAAQ,IAAI,OAAO,QAAQ,IAAI,CAAC,CAAC,CAAC;CAClF;CAEA,MAAM,KAAK,MAAM,KAAkE;EACjF,MAAM,MAAM,SAAS,IAAI,KAAK,GAAG;EAEjC,IAAI,SAAmB,CAAC;EACxB,IAAI;GAIF,UAAS,MAHW,MAAM,UACxB,KAAK;IAAE,GAAG;IAAM;IAAK,SAAS,KAAK;IAAM,QAAQ,IAAI;GAAO,CAAC,CAC/D,EAAA,CACe,KAAK;EACtB,SAAS,OAAO;GACd,MAAM,QAAQ,KAAK,KAAK;EAC1B;EAEA,MAAM,QAAQ,KAAK,SAAS;EAC5B,MAAM,UAAU,SAAS,OAAO,KAAK,IAAI,GAAG;GAC1C,UAAU,QAAQ;GAClB,UAAU;EACZ,CAAC;EAED,MAAM,YAAY,QAAQ;EAC1B,IAAI,OAAO;GACT;GACA,MAAM,KAAK,QAAQ;GACnB,SAAS,QAAQ;GACjB;EACF;EAEA,OAAO,CAAC;GAAE,MAAM,QAAQ,QAAQ;GAAmB,MAAM;EAAO,CAAC;CACnE;AACF,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"grep-6O3mjSvV.mjs","names":[],"sources":["../src/tools/grep.ts"],"sourcesContent":["import type { MetaPart, TextPart, ToolContext } from \"@zaly/ai\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { defaultExcludes } from \"@zaly/shared/find\"\nimport { Spawn, TextStream, which } from \"@zaly/shared/process\"\nimport { cleanTextTui } from \"@zaly/shared/text\"\nimport { Type } from \"typebox\"\nimport { truncate } from \"../utils/truncate.ts\"\n\nconst MAX_BUFFER = 512 * 1024\nconst DEFAULT_LIMIT = 100\nconst MAX_LIMIT = 1000\nconst MAX_COLUMNS = 200\n\nexport type GrepTool = typeof grepTool\nexport type GrepToolMeta = {\n cwd: string\n pattern: string\n truncated: boolean\n cmd: string[]\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const grepTool = defineTool({\n name: \"grep\",\n desc:\n `Search file contents using rg/grep. Respects .gitignore by default when ripgrep is available; ` +\n \"prefer this over bash grep/find for code search. Returns human-readable grouped matches with line numbers.\",\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n pattern: Type.String({\n description:\n \"Text or regex pattern. Uses smart case: case-insensitive unless uppercase is present.\",\n minLength: 1,\n }),\n paths: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Optional files/directories to search, relative to cwd or absolute.\",\n })\n ),\n glob: Type.Optional(\n Type.Array(Type.String(), {\n description: \"Ripgrep glob filters, e.g. [`*.ts`, `!dist/**`].\",\n })\n ),\n fixed_strings: Type.Optional(\n Type.Boolean({\n default: false,\n description: \"Treat all patterns as literals instead of as regular expressions\",\n })\n ),\n context: Type.Optional(\n Type.Integer({\n default: 0,\n description: \"Context lines before/after each match. 0-10.\",\n maximum: 10,\n minimum: 0,\n })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: DEFAULT_LIMIT,\n description: \"Maximum output lines to keep inline, including file headings and context.\",\n maximum: MAX_LIMIT,\n minimum: 1,\n })\n ),\n }),\n\n async preflight(args, ctx: ToolContext<GrepToolMeta>) {\n const cwd = normPath(ctx.cwd, \".\")\n await ctx.need?.(\"read\", cwd)\n\n const paths = (args.paths?.length ? args.paths : [\".\"]).map((p) => normPath(cwd, p))\n await Promise.all(paths.map((path) => Promise.resolve(ctx.need?.(\"read\", path))))\n },\n\n async call(args, ctx: ToolContext<GrepToolMeta>): Promise<(MetaPart | TextPart)[]> {\n const command = resolveGrep()\n if (!command) throw new AiError({ code: \"MISSING_TOOL\", message: \"grep requires rg or grep\" })\n\n const cwd = normPath(ctx.cwd, \".\")\n const paths = args.paths?.length ? args.paths : []\n\n const cmdArgs = buildArgs(command.kind, args, paths)\n const proc = new Spawn(command.cmd, cmdArgs, {\n cwd,\n maxBuffer: MAX_BUFFER,\n signal: ctx.signal,\n stderr: new TextStream(),\n stdout: new TextStream(),\n timeout: 60_000,\n })\n const result = await proc.result.catch((error: unknown) => {\n throw new AiError({ cause: error, code: \"GREP_FAILED\", message: String(error) })\n })\n\n // rg/grep: 0 matches found, 1 no matches, >1 error.\n if (result.code > 1 && result.killReason !== \"maxBuffer\") {\n throw new AiError({\n code: \"GREP_FAILED\",\n data: { code: result.code, stderr: result.stderr },\n message: `${command.cmd} failed (${result.code}): ${result.stderr.slice(0, 500)}`,\n })\n }\n\n const text = cleanTextTui(result.stdout)\n const limit = args.limit ?? DEFAULT_LIMIT\n const summary = truncate(text, {\n maxLineChars: command.kind === \"rg\" ? 500 : MAX_COLUMNS,\n maxLines: limit + 1,\n strategy: \"head\",\n })\n\n const truncated = summary.truncated || result.killReason === \"maxBuffer\"\n ctx.meta = {\n cmd: [command.cmd, ...cmdArgs],\n cwd,\n pattern: args.pattern,\n truncated,\n }\n\n const parts: (MetaPart | TextPart)[] = []\n\n const stderr = result.stderr.trim()\n if (stderr || result.killReason) {\n const meta: Record<string, unknown> = {\n command: command.cmd,\n cwd,\n truncated,\n }\n if (result.killReason) meta.killReason = result.killReason\n if (stderr) meta.stderr = stderr.slice(0, 500)\n parts.push({ data: meta, tag: \"grep\", type: \"meta\" })\n }\n parts.push({ text: summary.text || \"No matches found.\", type: \"text\" })\n return parts\n },\n})\n\ntype GrepArgs = Parameters<GrepTool[\"call\"]>[0]\ntype GrepBackend = { cmd: string; kind: \"rg\" | \"grep\" }\n\nfunction resolveGrep(): GrepBackend | undefined {\n const rg = which(\"rg\")\n if (rg) return { cmd: rg, kind: \"rg\" }\n const grep = which(\"grep\")\n return grep ? { cmd: grep, kind: \"grep\" } : undefined\n}\n\nfunction buildArgs(kind: GrepBackend[\"kind\"], args: GrepArgs, paths: string[]): string[] {\n return kind === \"rg\" ? buildRgArgs(args, paths) : buildGrepArgs(args, paths)\n}\n\nfunction buildRgArgs(args: GrepArgs, paths: string[]): string[] {\n const ret = [\n \"--color=always\",\n \"--heading\",\n \"--with-filename\",\n \"--line-number\",\n `--max-columns=${MAX_COLUMNS}`,\n \"--max-columns-preview\",\n \"--path-separator=/\",\n ]\n\n if (args.fixed_strings) ret.push(\"--fixed-strings\")\n ret.push(\"--smart-case\", \"--no-hidden\")\n const context = args.context ?? 0\n if (context > 0) ret.push(`--context=${context}`)\n\n for (const e of defaultExcludes(paths)) ret.push(\"--glob\", `!${e}`)\n for (const g of args.glob ?? []) ret.push(\"--glob\", g)\n\n ret.push(\"--\", args.pattern, ...paths)\n return ret\n}\n\nfunction buildGrepArgs(args: GrepArgs, paths: string[]): string[] {\n const ret = [\n \"--recursive\",\n \"--line-number\",\n \"--with-filename\",\n \"--binary-files=without-match\",\n \"--color=always\",\n ]\n if (args.fixed_strings) ret.push(\"--fixed-strings\")\n else ret.push(\"--extended-regexp\")\n const hasUpper = /[A-Z]/.test(args.pattern)\n if (!hasUpper) ret.push(\"--ignore-case\")\n const context = args.context ?? 0\n if (context > 0) ret.push(\"--context\", String(context))\n for (const e of defaultExcludes(paths)) ret.push(\"--exclude-dir\", e)\n for (const g of args.glob ?? []) ret.push(...grepGlobArgs(g))\n ret.push(\"--\", args.pattern, ...paths)\n return ret\n}\n\nfunction grepGlobArgs(glob: string): string[] {\n if (!glob.startsWith(\"!\")) return [\"--include\", glob]\n const exclude = glob.slice(1)\n if (exclude.endsWith(\"/**\") && !exclude.slice(0, -3).includes(\"*\")) {\n return [\"--exclude-dir\", exclude.slice(0, -3)]\n }\n return [\"--exclude\", exclude]\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"read-B-hwcc2Q.mjs","names":[],"sources":["../src/utils/file.ts","../src/tools/read.ts"],"sourcesContent":["import type { Tool, ToolContext } from \"@zaly/ai\"\n\nimport { AiError, extractToolResults } from \"@zaly/ai\"\nimport { normPath, safeStat } from \"@zaly/shared\"\n\nexport type FileMeta = {\n path: string\n mtime: number\n kind: \"read\" | \"write\" | \"edit\"\n /** True when the result reflects the whole-file content — set by\n * un-sliced `read` and by `write` (which always replaces the\n * full file). Used by the masker to know whether this result\n * subsumes earlier reads of the same path. `edit` never sets it\n * because the result is patch-relative. */\n full?: boolean\n unchanged?: boolean\n}\n\nexport type FileTool = Tool<{ path: string }, unknown, FileMeta>\n\nfunction isFileMeta(meta: unknown): meta is FileMeta {\n const m = meta as Partial<Record<string, unknown>> | undefined | null\n return (\n m !== undefined &&\n m !== null &&\n typeof m === \"object\" &&\n typeof m.path === \"string\" &&\n typeof m.mtime === \"number\" &&\n (m.kind === \"read\" || m.kind === \"write\" || m.kind === \"edit\")\n )\n}\n\nexport function assertFresh(path: string, ctx: ToolContext) {\n const err = checkFresh(path, ctx)\n if (err !== true) throw err\n}\n\nexport function isUnchanged(path: string, ctx: ToolContext) {\n return checkFresh(path, ctx, { full: true }) === true\n}\n\nexport function checkFresh(\n path: string,\n ctx: ToolContext,\n opts: { full?: boolean } = {}\n): AiError | true {\n path = normPath(ctx.cwd, path)\n const mtime = safeStat(path)?.mtimeMs\n if (mtime === undefined)\n return new AiError({ code: \"NOT_FOUND\", message: `${path}: file not found` })\n const messages = ctx.messages ?? []\n let ret: AiError = freshnessError(path, \"NOT_READ\")\n\n for (const { m, $p, p } of extractToolResults<FileMeta>(messages)) {\n const id = m.id\n if (!id || !isFileMeta(p.meta) || p.meta.path !== path || ctx.isMasked?.(id, $p)) continue\n if (opts.full && !p.meta.full) continue\n if (p.meta.mtime === mtime) return true // Fresh! The file's mtime matches what we saw at read time.\n ret = freshnessError(path, \"STALE\")\n }\n return ret\n}\n\n/** Build the canonical freshness error. `data.reason` distinguishes\n * never-read from changed-since-read for callers and renderers. */\nexport function freshnessError(path: string, reason: \"NOT_READ\" | \"STALE\"): AiError {\n return new AiError({\n code: \"FILE_NOT_FRESH\",\n data: { path, reason },\n message:\n reason === \"NOT_READ\"\n ? `${path}: read this file before mutating it.`\n : `${path}: file changed since last read. Re-read before mutating.`,\n })\n}\n","import type { Content, ToolContext } from \"@zaly/ai\"\nimport type { FileMeta } from \"../utils/file.ts\"\n\nimport { AiError, defineTool, toAttachment } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { fileDetect } from \"@zaly/shared/detect\"\nimport { normalizeEol } from \"@zaly/shared/text\"\nimport { stat } from \"node:fs/promises\"\nimport { Type } from \"typebox\"\nimport { isUnchanged } from \"../utils/file.ts\"\n\n/**\n * Read a file from disk.\n *\n * Output shape:\n * - Text files → plain text plus a `<slice>` metadata part describing\n * the returned line range, including when reading the whole file.\n * - Image files (png, jpeg, webp, gif, avif, …) → `ImagePart` attachment.\n * The model sees the image natively if its modality supports it.\n * - Other binary files → `BINARY_FILE` error with a hint.\n *\n * Path resolution: relative paths resolve against the agent's session\n * cwd via `process.cwd()` for now. When the permission system gates this\n * via a tool-side hook, the cwd from `PermissionContext` will be used\n * instead so resolution stays consistent across the loop.\n */\nconst DEFAULT_LIMIT = 2000\nconst MAX_LINE_LENGTH = 2000\n\nexport type ReadTool = typeof readTool\n\nexport type ReadToolMeta = FileMeta & {\n offset: number\n limit: number\n}\n\n/** Build the `read` tool with model-aware schema description. The\n * attachment-shape blurb is included only when the loaded model\n * actually accepts the relevant modality — text-only models see a\n * description that promises only text output, so they don't reach\n * for the tool expecting images they can't process. */\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const readTool = defineTool({\n name: \"read\",\n desc: `Read a file. Text files return plain text with line-range metadata. Image & PDF files are sent as attachments. Use \\`offset\\`/\\`limit\\` to slice large files.`,\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n path: Type.String({ description: \"Path to the file. Absolute or cwd-relative.\" }),\n offset: Type.Optional(\n Type.Integer({\n default: 1,\n description:\n \"1-based line number to start at. Negative values count from \" +\n \"the end: -50 starts 50 lines before EOF (so `offset: -50` \" +\n \"with the default limit returns the last 50 lines, like \" +\n \"`tail -n 50`). `offset: -50, limit: 20` reads 20 lines \" +\n \"starting 50 from the end.\",\n })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: DEFAULT_LIMIT,\n description: \"Maximum number of lines to return.\",\n minimum: 1,\n })\n ),\n }),\n\n async preflight(args, ctx: ToolContext<ReadToolMeta>) {\n const path = normPath(ctx.cwd, args.path)\n await ctx.need?.(\"read\", path)\n },\n\n async call(args, ctx: ToolContext<ReadToolMeta>): Promise<Content> {\n const path = normPath(ctx.cwd, args.path)\n\n const fileStat = await stat(path).catch((error: unknown) => {\n throw new AiError({\n cause: error,\n code: \"NOT_FOUND\",\n message: `cannot read ${path}: file not found`,\n })\n })\n if (!fileStat.isFile()) {\n throw new AiError({ code: \"NOT_A_FILE\", message: `${path} is not a regular file` })\n }\n\n if (isUnchanged(path, ctx)) {\n // Fresh! We've seen this file's current bytes, so we can skip\n // returning the content again.\n ctx.meta = {\n full: false,\n kind: \"read\",\n limit: 0,\n mtime: fileStat.mtimeMs,\n offset: 0,\n path,\n unchanged: true,\n }\n return [\n { content: `file unchanged since last read: ${path}`, tag: \"unchanged\", type: \"meta\" },\n ]\n }\n\n const file = await fileDetect(path)\n if (!file) {\n throw new AiError({\n code: \"READ_ERROR\",\n message: `${path}: could not read file`,\n })\n }\n\n // Threshold-based binary check — files with sporadic control bytes\n // (logs with ANSI styling, source with form feeds) still read as\n // text; only when binary content dominates the sample do we bail.\n if (file.type === \"binary\") {\n throw new AiError({\n code: \"BINARY_FILE\",\n data: { bytes: file.data.length, path },\n message: `${path}: binary file (${file.data.length} bytes); not displayable as text`,\n })\n }\n\n const att = await toAttachment(file)\n if (att) return [att]\n\n if (file.type !== \"text\") {\n throw new AiError({\n code: \"UNSUPPORTED_FILE\",\n data: { path, type: file.type },\n message: `${path}: unsupported file type ${file.type}`,\n })\n }\n\n const text = new TextDecoder().decode(file.data)\n const slice = formatTextSlice(text, {\n limit: args.limit ?? DEFAULT_LIMIT,\n offset: args.offset ?? 1,\n })\n\n // Record the observed mtime for unchanged-read detection. The masker\n // uses `full` to know whether this read subsumes earlier reads.\n ctx.meta = {\n full: slice.full,\n kind: \"read\",\n limit: slice.limit,\n mtime: fileStat.mtimeMs,\n offset: slice.offset,\n path,\n }\n\n return slice.content\n },\n})\n\n/** Return plain text and line-range metadata for both full and sliced reads. */\nfunction formatTextSlice(\n content: string,\n { offset, limit }: { offset: number; limit: number }\n): { content: Content; full?: boolean; offset: number; limit: number } {\n // Normalize line endings to LF for display. The model always sees LF;\n // edit/write detect the file's actual style and re-apply it on disk.\n content = normalizeEol(content)\n const lines = content.split(\"\\n\")\n // `split` on a final newline produces a trailing empty string — drop\n // it so a 3-line file with trailing LF reads as 3 lines, not 4.\n if (lines.at(-1) === \"\") lines.pop()\n\n // Negative offset = \"from the end\" — Python-slice / `tail -n` semantic.\n // `-1000` on a 30-line file clamps to 0 (read whole file) rather than\n // erroring; `0` is treated as `1` (head, off-by-one tolerance).\n let start = offset < 0 ? Math.max(0, lines.length + offset) : Math.max(0, offset - 1)\n // Overshoot clamps to `lines.length` so the slice loop emits nothing.\n // The agent gets an empty text + a `<slice>` meta that reports the\n // real file size, then can re-issue with a sensible offset.\n if (start >= lines.length) start = lines.length\n const end = Math.min(lines.length, start + limit)\n\n const out: string[] = []\n let full = start === 0 && end === lines.length\n for (let i = start; i < end; i++) {\n let line = lines[i]\n if (line.length > MAX_LINE_LENGTH) {\n full = false\n line = `${line.slice(0, MAX_LINE_LENGTH)}… [line truncated, ${line.length} chars]`\n }\n out.push(line)\n }\n\n const text = out.join(\"\\n\")\n\n // Empty slice (overshot offset): tell the agent the offset they asked\n // for and the real file size, rather than emitting a nonsensical\n // \"showing 6-5 of 5\"-style range.\n const sliceMsg =\n start === end && lines.length > 1\n ? `offset ${offset} past end of file (${lines.length} lines)`\n : `showing ${start + 1}-${end} of ${lines.length}`\n\n return {\n content: [\n { content: sliceMsg, tag: \"slice\", type: \"meta\" },\n { text, type: \"text\" },\n ],\n full,\n limit: end - start,\n offset: start + 1,\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"registry-L9GDccS0.mjs","names":["resolveRule","ignore","ignore"],"sources":["../src/permissions/handlers/bash.ts","../../../node_modules/.bun/ignore@7.0.12/node_modules/ignore/index.js","../src/permissions/handlers/files.ts","../src/permissions/handlers/tool.ts","../src/permissions/handlers/registry.ts"],"sourcesContent":["import type { Segment } from \"../../utils/bash/parser.ts\"\nimport type { CheckResult, PermissionHandler, Rule, Suggestion, Verdict } from \"../types.ts\"\n\nimport { parseBash } from \"../../utils/bash/parser.ts\"\nimport { TOOLS } from \"../../utils/bash/tools.ts\"\n\n/**\n * Handler for `Rule<\"bash\">` — evaluates a bash command line against\n * pattern rules, then delegates each segment's inferred and explicit\n * file paths to the file handler via `ctx.validate(\"read\"|\"write\", …)`.\n *\n * Composition keeps file-rule logic in one place: the bash handler\n * doesn't know about workspaces, sensitive files, or path-pattern\n * matching — it just asks the registered handler for those scopes.\n *\n * Sources of file paths per segment:\n * - `seg.reads` / `seg.writes` — explicit `<` / `>` / `>>` redirects\n * extracted by the parser.\n * - `TOOLS[cmd].reads/writes(args)` — inferred from built-in tool\n * specs (e.g. `cat foo.txt` reads `foo.txt`, `tee out` writes `out`).\n *\n * Suggestions and reasons from inner `validate` calls bubble up so the\n * prompt UI can offer \"allow `Bash(cat:*)`\" alongside \"add ~/Documents\n * as workspace\" in a single ask.\n */\nexport const bashHandler: PermissionHandler<\"bash\"> = {\n validate(input, ctx): CheckResult {\n const parsed = parseBash(input)\n if (!parsed.ok) {\n if ([\"bash\", \"read\", \"write\"].every((scope) => ctx.allowsAll?.(scope))) {\n return { verdict: \"allow\" }\n }\n return {\n ask: `Allow this bash command? (${parsed.reason})`,\n reason: parsed.reason,\n verdict: \"ask\",\n }\n }\n\n let verdict: Verdict = \"allow\"\n let reason: string | undefined\n const suggestions: Suggestion[] = []\n\n for (const seg of parsed.segments) {\n // Bash rule match for the command itself. Resolve this before the\n // conservative safety checks so explicit allow-all presets like yolo can\n // mean exactly that instead of degrading to an unimplemented ask prompt.\n const cmdVerdict = resolveRules(ctx.rules, seg) ?? \"ask\"\n if (seg.hasCommandSubst && cmdVerdict !== \"allow\") {\n return {\n ask: `Allow command substitution in \\`${seg.cmd}\\`?`,\n reason: \"command substitution not allowed\",\n verdict: \"ask\",\n }\n }\n const spec = TOOLS[seg.cmd]\n if (spec?.unsafe?.(seg.args) && cmdVerdict !== \"allow\") {\n return {\n ask: `Allow invocation of \\`${seg.cmd}\\`? (unsafe)`,\n reason: `${seg.cmd}: invocation mode not safely modelled`,\n verdict: \"ask\",\n }\n }\n\n let segVerdict: Verdict = cmdVerdict\n let segReason: string | undefined =\n cmdVerdict === \"allow\" ? undefined : `segment \"${seg.cmd}\" → ${cmdVerdict}`\n if (cmdVerdict === \"ask\") {\n suggestions.push({\n kind: \"rule\",\n pattern: seg.args.length > 0 ? `${seg.cmd}:*` : seg.cmd,\n scope: \"bash\",\n })\n }\n\n // Delegate file paths to the file handler. Reads first, then\n // writes; both contribute to this segment's verdict via combine.\n const reads = [...(spec?.reads?.(seg.args) ?? []), ...seg.reads]\n const writes = [...(spec?.writes?.(seg.args) ?? []), ...seg.writes.map((w) => w.path)]\n\n for (const path of reads) {\n if (seg.dyn?.has(path) && !ctx.allowsAll?.(\"read\")) {\n segVerdict = combine(segVerdict, \"ask\")\n segReason ??= `${seg.cmd}: read ${path}: dynamic path`\n suggestions.push({\n kind: \"rule\",\n pattern: `${seg.cmd}:*`,\n scope: \"bash\",\n })\n continue\n }\n const r = ctx.validate(\"read\", path)\n segVerdict = combine(segVerdict, r.verdict)\n if (r.verdict !== \"allow\") {\n segReason ??= `${seg.cmd}: read ${path}: ${r.reason}`\n if (r.suggestions) suggestions.push(...r.suggestions)\n }\n if (segVerdict === \"deny\") break\n }\n if (segVerdict !== \"deny\") {\n for (const path of writes) {\n if (seg.dyn?.has(path) && !ctx.allowsAll?.(\"write\")) {\n segVerdict = combine(segVerdict, \"ask\")\n segReason ??= `${seg.cmd}: write ${path}: dynamic path`\n suggestions.push({\n kind: \"rule\",\n pattern: `${seg.cmd}:*`,\n scope: \"bash\",\n })\n continue\n }\n const w = ctx.validate(\"write\", path)\n segVerdict = combine(segVerdict, w.verdict)\n if (w.verdict !== \"allow\") {\n segReason ??= `${seg.cmd}: write ${path}: ${w.reason}`\n if (w.suggestions) suggestions.push(...w.suggestions)\n }\n if (segVerdict === \"deny\") break\n }\n }\n\n verdict = combine(verdict, segVerdict)\n if (verdict !== \"allow\" && reason === undefined) reason = segReason\n if (verdict === \"deny\") break\n }\n\n return verdict === \"allow\"\n ? { verdict: \"allow\" }\n : {\n ask: `Allow this bash command?`,\n reason: reason ?? \"no rule matched\",\n suggestions: suggestions.length > 0 ? suggestions : undefined,\n verdict,\n }\n },\n}\n\n// ── Pattern matching ─────────────────────────────────────────────────────\n\n/** A command-pattern rule.\n *\n * Pattern syntax:\n * \"cmd\" → cmd alone, no args\n * \"cmd:*\" → cmd with any args (trailing wildcard)\n * \"cmd a\" → cmd with arg \"a\", no other args\n * \"cmd a:*\" → cmd with arg \"a\" plus any others\n * \"cmd a b\" → cmd with exact args [a, b]\n * \"cmd --flag*\" → cmd with arg starting with \"--flag\" (per-arg prefix)\n * \"bun test:node:*\" → cmd \"bun\", arg \"test:node\", any others\n *\n * Whitespace separates the cmd from its args, and args from each\n * other. The `:` character is only meaningful as the trailing `:*`\n * wildcard suffix — anywhere else (including inside an arg like\n * `test:node`) it's a literal character. */\n\ninterface ParsedPattern {\n cmd: string\n /** Arg-position match tokens. Each may end with `*` for prefix match. */\n parts: string[]\n /** Set when the pattern ends with `:*`, allowing arbitrary further args. */\n trailingWildcard: boolean\n}\n\n/** Combine two verdicts: deny dominates, ask beats allow. */\nfunction combine(a: Verdict, b: Verdict): Verdict {\n if (a === \"deny\" || b === \"deny\") return \"deny\"\n if (a === \"ask\" || b === \"ask\") return \"ask\"\n return \"allow\"\n}\n\nconst patternCache = new Map<string, ParsedPattern>()\n\nfunction parsePattern(pattern: string): ParsedPattern {\n const cached = patternCache.get(pattern)\n if (cached) return cached\n\n let body = pattern.trim()\n let trailingWildcard = false\n if (body.endsWith(\":*\")) {\n trailingWildcard = true\n body = body.slice(0, -2)\n }\n\n const tokens = body.split(/\\s+/).filter((s) => s.length > 0)\n const cmd = tokens[0] ?? \"\"\n const result: ParsedPattern = {\n cmd,\n parts: tokens.slice(1),\n trailingWildcard,\n }\n patternCache.set(pattern, result)\n return result\n}\n\nfunction matchPart(part: string, value: string): boolean {\n if (part.endsWith(\"*\")) {\n return value.startsWith(part.slice(0, -1))\n }\n return part === value\n}\n\n/** Test whether a parsed segment matches a rule pattern. */\nexport function matchRule(rule: Rule, seg: Segment): boolean {\n const p = parsePattern(rule.pattern)\n // Bare `*` (e.g. from a `Bash` rule with no parens) matches any command.\n if (p.cmd === \"*\" && p.parts.length === 0) return true\n if (p.cmd !== seg.cmd) return false\n if (seg.args.length < p.parts.length) return false\n for (let i = 0; i < p.parts.length; i++) {\n if (!matchPart(p.parts[i], seg.args[i])) return false\n }\n if (p.trailingWildcard) return true\n return seg.args.length === p.parts.length\n}\n\n/** First-match-wins resolution. Returns `undefined` if no rule matches. */\nfunction resolveRules(rules: readonly Rule[], seg: Segment): Verdict | undefined {\n for (const r of rules) {\n if (matchRule(r, seg)) return r.policy\n }\n return undefined\n}\n","// A simple implementation of make-array\nfunction makeArray (subject) {\n return Array.isArray(subject)\n ? subject\n : [subject]\n}\n\nconst UNDEFINED = undefined\nconst EMPTY = ''\nconst SPACE = ' '\nconst ESCAPE = '\\\\'\n\n// The characters that carry a meaning of their own inside a regular\n// expression, so a literal one has to be escaped before it is emitted.\nconst REGEX_LITERAL_SPECIAL = /[.*+?()[\\]{}^$|\\\\/]/\nconst BOM = '\\uFEFF'\nconst REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\\\]|^)\\\\$/\nconst REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\\\!/\nconst REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\\\#/\nconst REGEX_SPLITALL_CRLF = /\\r?\\n/g\n\n// Invalid:\n// - /foo,\n// - ./foo,\n// - ../foo,\n// - .\n// - ..\n// Valid:\n// - .foo\nconst DOUBLE_SLASH = '//'\nconst SLASH_CODE = 47\nconst DOT_CODE = 46\n\n\nconst SLASH = '/'\n\n// Do not use ternary expression here, since \"istanbul ignore next\" is buggy\nlet TMP_KEY_IGNORE = 'node-ignore'\n/* istanbul ignore else */\nif (typeof Symbol !== 'undefined') {\n TMP_KEY_IGNORE = Symbol.for('node-ignore')\n}\nconst KEY_IGNORE = TMP_KEY_IGNORE\n\nconst define = (object, key, value) => {\n Object.defineProperty(object, key, {value})\n return value\n}\n\nconst RETURN_FALSE = () => false\n\n// See fixtures #59\nconst cleanRangeBackSlash = slashes => {\n const {length} = slashes\n return slashes.slice(0, length - length % 2)\n}\n\n// > The range notation, e.g. [a-zA-Z],\n// > can be used to match one of the characters in a range.\n//\n// gitignore(5) defers to fnmatch(3) for this, and git implements it in\n// `wildmatch.c`. A bracket expression has a sub-grammar of its own, which\n// is neither the surrounding pattern grammar nor the JavaScript one:\n//\n// - a `]` right after `[` or `[!` is a literal member, not the terminator\n// - `[:alpha:]` names one of twelve POSIX classes\n// - `\\` escapes the next member, `]` included\n// - `*`, `?` and `.` are plain literal members\n// - an unterminated expression makes the whole pattern match nothing\n//\n// which means the expression can not be located -- let alone translated -- by\n// a regular expression. It is scanned out of the pattern before the\n// replacers below run, and put back once they are done, so that neither the\n// metacharacter escaper nor the `?` / `*` replacers ever see its body.\n\n// git classifies with its own ASCII-only ctype macros (`wildmatch.c`), never\n// with the C library ones, so these must not be mapped onto `\\d` / `\\w` /\n// `\\s`, which are wider. `/` is left out of every expansion, because a\n// bracket expression never matches a path separator.\nconst POSIX_CLASSES = {\n alnum: '0-9A-Za-z',\n alpha: 'A-Za-z',\n blank: ' \\\\t',\n cntrl: '\\\\x00-\\\\x1f\\\\x7f',\n digit: '0-9',\n graph: '!-.0-~',\n lower: 'a-z',\n print: ' -.0-~',\n punct: '!-.:-@\\\\[-`{-~',\n // git's `sane-ctype.h` classifies \\v and \\f as control, not space,\n // unlike C's `isspace`\n space: ' \\\\t\\\\n\\\\r',\n upper: 'A-Z',\n xdigit: '0-9A-Fa-f'\n}\n\nconst CLASS_MEMBERS_TO_ESCAPE = '\\\\]^-['\n\nconst escapeMember = char => CLASS_MEMBERS_TO_ESCAPE.indexOf(char) < 0\n ? char\n : ESCAPE + char\n\n// > if (matched == negated || ((flags & WM_PATHNAME) && text_ch == '/'))\n// > return WM_ABORT_TO_STARSTAR; (git, `wildmatch.c`)\n// A bracket expression never matches a path separator, whatever its members\n// say, so a negated class gets `/` as one more excluded character, while a\n// plain one -- where a literal member or a range could still let `/` in --\n// is guarded with a lookahead, `/` being impossible to subtract from a\n// JavaScript character class.\nconst NON_SLASH = '(?!\\\\/)'\n\nconst classSource = (negated, body) => {\n if (negated) {\n return `[^\\\\/${body}]`\n }\n\n const source = `[${body}]`\n\n return new RegExp(source).test('/')\n ? NON_SLASH + source\n : source\n}\n\n// Scan the bracket expression that starts at `pattern[start] === '['`,\n// mirroring the member loop of git's `wildmatch.c`.\n// @returns {{end: number, source: string} | null} `null` if the expression is\n// never terminated, which makes the whole pattern match nothing.\nconst scanBracket = (pattern, start) => {\n const {length} = pattern\n let index = start + 1\n let negated = EMPTY\n\n const lead = pattern[index]\n if (lead === '!' || lead === '^') {\n negated = '^'\n index ++\n }\n\n let body = EMPTY\n\n // The member a `-` could start a range from, or EMPTY when the previous\n // member can not open one (start of the body, or a range / POSIX class\n // that has just closed)\n let prev = EMPTY\n\n // git scans the members with a do-while, so the first one is consumed\n // unconditionally. That is the whole reason a leading `]` is a member\n // and not the terminator.\n for (;;) {\n const char = pattern[index]\n\n if (char === UNDEFINED) {\n return null\n }\n\n if (char === ESCAPE) {\n const escaped = pattern[index + 1]\n if (escaped === UNDEFINED) {\n return null\n }\n body += escapeMember(escaped)\n prev = escaped\n index ++\n } else if (\n char === '-'\n && prev\n && index + 1 < length\n && pattern[index + 1] !== ']'\n ) {\n index ++\n let to = pattern[index]\n if (to === ESCAPE) {\n // A pattern can not end on a lone backslash -- `checkPattern` has\n // already thrown it away -- so there is an upper bound to read.\n to = pattern[index += 1]\n }\n // An out-of-order range matches nothing in git but is a syntax error in\n // JavaScript, so it is dropped. Its lower bound stays: git tests it\n // as a plain member before it ever looks at the `-`, so `[c-a]` does\n // match `c`.\n if (prev <= to) {\n body += `-${escapeMember(to)}`\n }\n prev = EMPTY\n } else if (char === '[' && pattern[index + 1] === ':') {\n const nameStart = index + 2\n let end = nameStart\n while (end < length && pattern[end] !== ']') {\n end ++\n }\n\n if (end === length) {\n return null\n }\n\n if (end > nameStart && pattern[end - 1] === ':') {\n const expanded = POSIX_CLASSES[pattern.slice(nameStart, end - 1)]\n\n // An unknown class name makes the whole pattern match nothing\n if (expanded === UNDEFINED) {\n return null\n }\n\n body += expanded\n prev = EMPTY\n index = end\n } else {\n // No `:]` to close it, so the `[` is a plain member and scanning\n // resumes right after it.\n body += escapeMember('[')\n prev = '['\n index = nameStart - 2\n }\n } else {\n body += escapeMember(char)\n prev = char\n }\n\n index ++\n\n if (pattern[index] === ']') {\n return {\n end: index,\n source: classSource(negated, body)\n }\n }\n }\n}\n\n// An empty JavaScript class can never match, which is how a pattern that git\n// gives up on (`WM_ABORT_ALL`) is expressed here.\nconst NEVER_MATCH = '[]'\n\n// A NUL can appear in neither a `.gitignore` line nor a path, which makes it\n// the one safe placeholder character. A literal one in the pattern is\n// held aside all the same, so a collision is impossible by construction.\nconst PLACEHOLDER = '\\u0000'\nconst REGEX_RESTORE_PLACEHOLDER = new RegExp(\n `${PLACEHOLDER}(\\\\d+)${PLACEHOLDER}`, 'g'\n)\n\n// The one wildcard the chain does not expand for itself is a trailing `*`. How\n// it expands depends on the mode (`regex` vs `checkRegex`), so `_make` is\n// left to do it, and until then the pending wildcard travels as this marker.\n// That is what keeps it apart from a user-escaped literal `\\*`: the unescape\n// steps collapse the escaped one to the exact `\\*` a wildcard would leave\n// behind, so by the time `_make` runs the two are otherwise the same string\n// and a literal star gets wrongly rewritten into a wildcard. This marker is a\n// private-use character no compiled pattern carries, and it never survives\n// into a `RegExp` -- `_make` always turns it back into a real wildcard first.\nconst TRAILING_WILDCARD = '\\uE000'\n\n// A trailing \"/**\" is deferred the same way, because `checkRegex` has to tell\n// it apart from a trailing \"/*\" (see `checkSourceOf`). For `.ignores()` it\n// still never matches the folder itself (#21).\nconst TRAILING_DOUBLESTAR = '\\uE001'\n\n// Replace every bracket expression with a placeholder the replacers below\n// leave alone, and translate it separately.\nconst extractBrackets = pattern => {\n const sources = []\n const hold = source =>\n `${PLACEHOLDER}${sources.push(source) - 1}${PLACEHOLDER}`\n\n const {length} = pattern\n let out = EMPTY\n let index = 0\n\n while (index < length) {\n const char = pattern[index]\n\n if (char === ESCAPE) {\n // > Put a backslash (\"\\\") in front of ... a character to make it literal.\n // (gitignore(5) -> fnmatch(3))\n // A backslash quotes the next character, whatever it is, so `\\d` is a\n // literal `d`, not the regex digit class, and `\\?` is a literal `?`,\n // not a wildcard. Held aside as its literal here, the escape never\n // reaches the replacers below, which would otherwise let `\\d`, `\\b`,\n // `\\1`, `\\/` keep their regular-expression meaning, and would turn a\n // `\\?` into `[^\\/]`.\n //\n // Four escapes are left for the chain, each with dedicated handling it\n // would be wrong to bypass: `\\*` (a literal star, told apart from a\n // wildcard by `TRAILING_WILDCARD` and the wildcard replacers), `\\[`\n // (a literal bracket, the one `[` the bracket replacer still expects),\n // `\\ ` (a quoted trailing space), and `\\\\` (a literal backslash). A\n // lone trailing backslash can reach here after line-ending cleanup.\n const escaped = pattern[index + 1]\n\n if (\n escaped === UNDEFINED\n || escaped === SLASH && index + 2 === length\n ) {\n // Git removes a final directory slash before matching. An escape\n // without a character after that removal makes the pattern invalid.\n out += hold(NEVER_MATCH)\n } else if (\n escaped === '*'\n || escaped === '['\n || escaped === SPACE\n || escaped === ESCAPE\n ) {\n out += pattern.slice(index, index + 2)\n } else {\n out += hold(\n REGEX_LITERAL_SPECIAL.test(escaped)\n ? ESCAPE + escaped\n : escaped\n )\n }\n\n index += 2\n } else if (char === PLACEHOLDER) {\n // Hold a literal placeholder character aside as well, so that pattern\n // text can never be mistaken for a placeholder we emitted.\n out += hold(`[${PLACEHOLDER}]`)\n index ++\n } else if (char === '[') {\n const scanned = scanBracket(pattern, index)\n\n if (scanned === null) {\n // git gives up on the whole pattern (`WM_ABORT_ALL`), so whatever\n // follows can not make it match either.\n out += hold(NEVER_MATCH)\n index = length\n } else {\n out += hold(scanned.source)\n index = scanned.end + 1\n }\n } else {\n out += char\n index ++\n }\n }\n\n return {\n source: out,\n sources\n }\n}\n\n// A step of the chain below is normally a `[matcher, replacer]` pair handed\n// to `String.replace`. `DIRECT` marks the ones that are plain string work\n// instead, and take `(source, pattern)`.\n//\n// Anchoring the two ends is exactly that -- a test of one character and a\n// concatenation -- and putting it through the regular expression engine\n// cost a third of this chain for nothing: 167ns where 10ns does the same\n// job. The two are still steps in the same list, in the same places,\n// because their position in the order is part of what they mean.\nconst DIRECT = null\n\n// A separator at the beginning or in the middle of a pattern, as opposed to\n// one at the very end.\nconst REGEX_INNER_SLASH = /\\/(?!$)/\n\n// > If the pattern ends with a slash,\n// > it is removed for the purpose of the following description,\n// > but it would only find a match with a directory.\n// > In other words, foo/ will match a directory foo and paths underneath it,\n// > but will not match a regular file or a symbolic link foo\n// > (this is consistent with the way how pathspec works in general in Git).\n// '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`'\n// -> ignore-rules will not deal with it, because it costs extra `fs.stat` call\n// you could use option `mark: true` with `glob`\n\n// '`foo/`' should not continue with the '`..`'\n// The chain that turns one gitignore pattern into a regular expression\n// source, in order. A step is either\n//\n// [matcher, replacer] handed to `String.replace`\n// [matcher, replacer, required] the same, but skipped outright unless\n// `required` appears in the string, which\n// the matcher cannot match without\n// [DIRECT, transform] plain string work, taking (source, pattern)\n//\n// The `required` character is only ever a shortcut: finding it does not mean\n// the matcher will match, and not finding it means it cannot. It is there\n// because scanning for one character costs a fraction of running a matcher\n// that then finds nothing -- for the wildcard step, whose `[^\\\\]+`\n// backtracks its way through the whole string before giving up, 11ns\n// against 400ns.\nconst REPLACERS = [\n\n // Replace (\\ ) with ' '\n // Only a space: an escaped tab or other whitespace is already a literal by\n // the time it reaches here, and a bare tab must be left as one, not turned\n // into a space.\n // (\\ ) -> ' '\n // (\\\\ ) -> '\\\\ '\n // (\\\\\\ ) -> '\\\\ '\n [\n // A run of backslashes is taken whole, from its first one, so no match\n // ever starts again inside it -- `(\\\\+?) ` did, which made a long run\n // quadratic.\n /(\\\\+)( ?)/g,\n (_, run, space) => space\n ? run.slice(0, run.length - run.length % 2) + SPACE\n : run,\n ESCAPE + SPACE\n ],\n\n // Escape metacharacters\n // which is written down by users but means special for regular expressions.\n\n // > There are 12 characters with special meanings:\n // > - the backslash \\,\n // > - the caret ^,\n // > - the dollar sign $,\n // > - the period or dot .,\n // > - the vertical bar or pipe symbol |,\n // > - the question mark ?,\n // > - the asterisk or star *,\n // > - the plus sign +,\n // > - the opening parenthesis (,\n // > - the closing parenthesis ),\n // > - and the opening square bracket [,\n // > - the opening curly brace {,\n // > These special characters are often called \"metacharacters\".\n [\n /[\\\\$.|*+(){^]/g,\n match => `\\\\${match}`\n ],\n\n [\n // > a question mark (?) matches a single character\n /(?!\\\\)\\?/g,\n () => '[^/]',\n '?'\n ],\n\n // leading slash\n [\n\n // > A leading slash matches the beginning of the pathname.\n // > For example, \"/*.c\" matches \"cat-file.c\" but not \"mozilla-sha1/sha1.c\".\n // A leading slash matches the beginning of the pathname\n /^\\//,\n () => '^',\n SLASH\n ],\n\n // replace special metacharacter slash after the leading slash\n [\n /\\//g,\n () => '\\\\/',\n SLASH\n ],\n\n [\n // > A leading \"**\" followed by a slash means match in all directories.\n // > For example, \"**/foo\" matches file or directory \"foo\" anywhere,\n // > the same as pattern \"foo\".\n // > \"**/foo/bar\" matches file or directory \"bar\" anywhere that is directly\n // > under directory \"foo\".\n // Notice that the '*'s have been replaced as '\\\\*'\n /^\\^*(?:\\\\\\*\\\\\\*\\\\\\/)+/,\n\n // '**/foo' <-> 'foo'\n () => '^(?:.*\\\\/)?',\n '*'\n ],\n\n // starting\n [\n // there will be no leading '/'\n // (which has been replaced by section \"leading slash\")\n // If starts with '**', adding a '^' to the regular expression also works\n DIRECT,\n (source, pattern) => {\n // Nothing to anchor to, or already anchored\n if (!source || source[0] === '^') {\n return source\n }\n\n // If has a slash `/` at the beginning or middle\n const anchor = !REGEX_INNER_SLASH.test(pattern)\n // > Prior to 2.22.1\n // > If the pattern does not contain a slash /,\n // > Git treats it as a shell glob pattern\n // Actually, if there is only a trailing slash,\n // git also treats it as a shell glob pattern\n\n // After 2.22.1 (compatible but clearer)\n // > If there is a separator at the beginning or middle (or both)\n // > of the pattern, then the pattern is relative to the directory\n // > level of the particular .gitignore file itself.\n // > Otherwise the pattern may also match at any level below\n // > the .gitignore level.\n ? '(?:^|\\\\/)'\n\n // > Otherwise, Git treats the pattern as a shell glob suitable for\n // > consumption by fnmatch(3)\n : '^'\n\n return anchor + source\n }\n ],\n\n // two globstars\n [\n // Use lookahead assertions so that we could match more than one `'/**'`\n /\\\\\\/\\\\\\*\\\\\\*(?=\\\\\\/|$)/g,\n\n // Zero, one or several directories\n // should not use '*', or it will be replaced by the next replacer\n\n // Check if it is not the last `'/**'`\n (_, index, str) => index + 6 < str.length\n\n // case: /**/ at the end of the pattern, i.e. a trailing `'/**/'`\n // > A trailing `\"/**/\"` (a trailing `\"/**\"` restricted to directories)\n // > matches everything inside, but it should not match the current\n // > folder itself, so it requires at least one directory segment.\n // 'a/**/' matches 'a/b/', 'a/x/y/' but not 'a/'\n ? str.slice(index + 6) === '\\\\/'\n ? '(?:\\\\/[^\\\\/]+)+'\n\n // case: /**/\n // > A slash followed by two consecutive asterisks then a slash matches\n // > zero or more directories.\n // > For example, \"a/**/b\" matches \"a/b\", \"a/x/b\", \"a/x/y/b\" and so on.\n // '/**/'\n : '(?:\\\\/[^\\\\/]+)*'\n\n // case: /**\n // > A trailing `\"/**\"` matches everything inside.\n\n // #21: everything inside but it should not include the current folder,\n // resolved by `_make`\n : `\\\\/${TRAILING_DOUBLESTAR}`,\n '*'\n ],\n\n // normal intermediate wildcards\n [\n // Never replace escaped '*'\n // ignore rule '\\*' will match the path '*'\n\n // 'abc.*/' -> go\n // 'abc.*' -> skip this rule,\n // coz trailing single wildcard will be handed by [trailing wildcard]\n /(^|[^\\\\]+)(\\\\\\*)+(?=.+)/g,\n\n // '*.js' matches '.js'\n // '*.js' doesn't match 'abc'\n (_, p1, p2) => {\n // 1.\n // > An asterisk \"*\" matches anything except a slash.\n // 2.\n // > Other consecutive asterisks are considered regular asterisks\n // > and will match according to the previous rules.\n const unescaped = p2.replace(/\\\\\\*/g, '[^\\\\/]*')\n return p1 + unescaped\n },\n '*'\n ],\n\n // trailing wildcard, held apart from a literal star\n [\n // The step above leaves a trailing `*` alone, so a single `\\*` is all that\n // can be left at the end here. Whether it is a wildcard or a literal\n // turns on the backslashes the user put in front of it: the escaper has\n // since doubled every one, so what stands here is those `2N` doubled\n // backslashes and then the star's own escape. An even number of the\n // original `N` leaves the star unescaped -- a wildcard -- and an odd\n // number escapes it -- a literal. This runs while the two are still\n // distinct, before the unescape steps below collapse the literal onto\n // the very `\\*` a wildcard leaves behind.\n /(^|[^\\\\])((?:\\\\\\\\)*)\\\\\\*$/,\n\n (match, p1, p2) =>\n // `p2` holds the doubled user backslashes; half of them is `N`.\n (p2.length / 2) % 2 === 0\n // A real wildcard: carry it to `_make` as the marker, so the unescape\n // steps and the trailing-wildcard rewrite can never mistake it for a\n // literal `\\*` (nor the reverse).\n ? p1 + p2 + TRAILING_WILDCARD\n // A literal star: leave it exactly as it stands for the unescape steps.\n : match,\n '*'\n ],\n\n [\n // unescape, revert step 3 except for back slash\n // For example, if a user escape a '\\\\*',\n // after step 3, the result will be '\\\\\\\\\\\\*'\n /\\\\\\\\\\\\(?=[$.|*+(){^])/g,\n () => ESCAPE,\n ESCAPE + ESCAPE\n ],\n\n [\n // '\\\\\\\\' -> '\\\\'\n /\\\\\\\\/g,\n () => ESCAPE,\n ESCAPE + ESCAPE\n ],\n\n [\n // Every real bracket expression -- POSIX classes included -- has already\n // been held aside by `extractBrackets`, so the only `[` left in the\n // pattern is an escaped, literal one.\n\n // `\\` is escaped by step 3\n /\\\\\\[([^\\]/]*?)(\\\\*)($|\\])/g,\n\n // '\\\\[bar]' -> '\\\\\\\\[bar\\\\]'\n (match, range, endEscape, close) =>\n `\\\\[${range}${cleanRangeBackSlash(endEscape)}${close}`,\n '['\n ],\n\n // ending\n [\n // 'js' will not match 'js.'\n // 'ab' will not match 'abc'\n DIRECT,\n\n // WTF!\n // https://git-scm.com/docs/gitignore\n // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1)\n // which re-fixes #24, #38\n\n // > If there is a separator at the end of the pattern then the pattern\n // > will only match directories, otherwise the pattern can match both\n // > files and directories.\n\n // 'js*' will not match 'a.js'\n // 'js/' will not match 'a.js'\n // 'js' will match 'a.js' and 'a.js/'\n source => {\n const last = source[source.length - 1]\n\n // The pattern is empty, or ends in a pending marker `_make` owns --\n // the trailing wildcard, or a trailing \"/**\". A trailing `*` that is\n // not the marker is a literal star, which anchors like any other\n // final character.\n if (!last || last === TRAILING_WILDCARD || last === TRAILING_DOUBLESTAR) {\n return source\n }\n\n return last === SLASH\n // foo/ will not match 'foo'\n ? `${source}$`\n // foo matches 'foo' and 'foo/'\n : `${source}(?=$|\\\\/$)`\n }\n ]\n]\n\nconst REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\\\\/)?\\uE000$/\nconst MODE_IGNORE = 'regex'\nconst MODE_CHECK_IGNORE = 'checkRegex'\nconst UNDERSCORE = '_'\n\nconst replaceTrailingWildcard = (_, p1) => {\n const prefix = p1\n // '\\^':\n // '/*' does not match EMPTY\n // '/*' does not match everything\n\n // '\\\\\\/':\n // 'abc/*' does not match 'abc/'\n ? `${p1}[^/]+`\n\n // 'a*' matches 'a'\n // 'a*' matches 'aa'\n : '[^/]*'\n\n return `${prefix}(?=$|\\\\/$)`\n}\n\nconst REGEX_REPLACE_TRAILING_DOUBLESTAR = /\\uE001$/\n\n// 'abc/**' matches everything under 'abc', but not 'abc/' itself (#21)\nconst replaceTrailingDoublestar = () => '.+(?=$|\\\\/$)'\n\nconst WILDCARD = '[^\\\\/]*'\n\n// Where a run of non-slash wildcards is split by single fixed characters --\n// `[^/]*x[^/]*y...` -- the engine has many equivalent ways to lay the\n// input across the wildcards, and on input that does not match it works\n// through all of them, so the time grows with the number of wildcards\n// rather than the length of the path. Every wildcard but the last in such a\n// run can be pinned to stop at the character that follows it, which leaves\n// one way to lay out the input and no rewinding, without changing which\n// paths match: the fixed characters still bound the count, and the last\n// wildcard still absorbs the rest.\n//\n// The source is read one token at a time -- a wildcard, a single-character\n// piece (a literal, an escape, a class), or a parenthesised group or anchor\n// that ends the run -- so only a genuine wildcard is touched.\n\n// The fixed pieces between the wildcard at `at` and the next wildcard of the\n// run, joined as they appear in the source. A run never holds two wildcards\n// in a row, so there is always at least one piece.\nconst separatorAfter = (run, at) => {\n let separator = EMPTY\n\n for (let index = at + 1; index < run.length && !run[index].wildcard; index ++) {\n separator += run[index].single\n }\n\n return separator\n}\n\nconst pinWildcards = source => {\n if (source.indexOf(WILDCARD) < 0) {\n return source\n }\n\n const tokens = []\n const {length} = source\n let index = 0\n\n while (index < length) {\n const char = source[index]\n\n if (source.startsWith(WILDCARD, index)) {\n tokens.push({wildcard: true})\n index += WILDCARD.length\n } else if (char === '[') {\n let end = index + 1\n\n if (source[end] === '^') {\n end ++\n }\n\n if (source[end] === ']') {\n end ++\n }\n\n while (end < length && source[end] !== ']') {\n end += source[end] === ESCAPE\n ? 2\n : 1\n }\n\n end ++\n tokens.push({single: source.slice(index, end)})\n index = end\n } else if (char === ESCAPE) {\n tokens.push({single: source.slice(index, index + 2)})\n index += 2\n } else if (char === '(') {\n let depth = 0\n let end = index\n\n do {\n if (source[end] === ESCAPE) {\n end ++\n } else if (source[end] === '(') {\n depth ++\n } else if (source[end] === ')') {\n depth --\n }\n\n end ++\n } while (end < length && depth > 0)\n\n if ('*+?'.indexOf(source[end]) >= 0) {\n end ++\n }\n\n tokens.push({boundary: source.slice(index, end)})\n index = end\n } else if (char === '^' || char === '$') {\n tokens.push({boundary: char})\n index ++\n } else {\n tokens.push({single: char})\n index ++\n }\n }\n\n let out = EMPTY\n let run = []\n\n const flush = () => {\n let lastWildcard\n\n run.forEach((token, at) => {\n if (token.wildcard) {\n lastWildcard = at\n }\n })\n\n run.forEach((token, at) => {\n if (!token.wildcard) {\n out += token.single\n return\n }\n\n // A wildcard that is not the last in the run is pinned to the whole\n // fixed piece that separates it from the next wildcard, not just to\n // that piece's first character: stopping at the first character alone\n // would let the wildcard hand over at a place where the rest of the\n // separator cannot follow, and the pin leaves no way back, so\n // `f*o/*/*` would miss `foo/b/c` -- the first `o` of `foo` is not the\n // one the `o/` after it needs. Every piece matches exactly one\n // character, so pinning to the first place the whole separator fits\n // is the leftmost place it can sit, and the wildcards that follow\n // take up whatever is left.\n out += at === lastWildcard\n ? WILDCARD\n : `(?:(?!${separatorAfter(run, at)})[^\\\\/])*`\n })\n\n run = []\n }\n\n tokens.forEach(token => {\n if (token.boundary === undefined) {\n run.push(token)\n return\n }\n\n flush()\n out += token.boundary\n })\n\n flush()\n\n return out\n}\n\n// @param {pattern}\nconst makeRegexPrefix = pattern => {\n const {source, sources} = extractBrackets(pattern)\n\n const replaced = REPLACERS.reduce(\n // A pass whose matcher finds nothing hands back the very string it was\n // given, so asking first costs a search and saves a rewrite. Ten of the\n // fifteen passes never fire for a typical .gitignore line, and between\n // them they were 45% of this chain.\n (prev, [matcher, replacer, required]) => {\n if (matcher === DIRECT) {\n return replacer(prev, pattern)\n }\n\n if (required !== UNDEFINED && prev.indexOf(required) < 0) {\n return prev\n }\n\n // A pass whose matcher finds nothing hands back the very string it was\n // given, so asking first costs a search and saves a rewrite.\n return matcher.test(prev)\n ? prev.replace(matcher, replacer.bind(pattern))\n : prev\n },\n source\n )\n\n // Most patterns hold no bracket expression at all, and then there is\n // nothing to put back.\n return sources.length\n ? replaced.replace(\n REGEX_RESTORE_PLACEHOLDER,\n (match, index) => sources[index]\n )\n : replaced\n}\n\n// `checkRegex` tests a path with a trailing slash, 'abc/', the way\n// `git check-ignore` matches that literal string once the directory itself\n// is known not to be excluded (see `checkIgnore`): a trailing '/' of the\n// pattern is dropped, a basename pattern sees an empty basename, and only a\n// pattern whose last segment is a pending wildcard can match what is left\n// after the final slash -- 'abc/*' and 'abc/**' match 'abc/', and 'abc/**'\n// also matches 'abc/d/' (#77, #169).\nconst checkSourceOf = (body, prefix) => {\n if (body[body.length - 1] === SLASH) {\n prefix = makeRegexPrefix(body.slice(0, - 1))\n }\n\n const head = prefix.slice(0, - 1)\n const last = prefix[prefix.length - 1]\n\n return last === TRAILING_WILDCARD\n ? `${head}$`\n : last === TRAILING_DOUBLESTAR\n ? `${head}.*$`\n : NEVER_MATCH\n}\n\n// A trailing slash does not stop a pattern being basename-only: it restricts\n// the match to a directory, it does not let the pattern reach across one.\n// Everything else a pattern can hold -- a wildcard, a character class, an\n// escape -- stays inside a single path segment too, so a pattern with no\n// separator in it can only ever describe the last one.\nconst matchesBasename = body => {\n const index = body.indexOf(SLASH)\n\n return index < 0 || index === body.length - 1\n}\n\n// The last segment of a path, keeping a trailing slash, because a pattern that\n// ends in one matches only a directory.\n// 'a/b/c.js' -> 'c.js'; 'a/b/' -> 'b/'; 'c.js' -> 'c.js' (no copy made)\nconst basenameOf = path => {\n const end = path.length - 1\n\n const index = path.lastIndexOf(\n SLASH,\n path[end] === SLASH\n ? end - 1\n : end\n )\n\n return index < 0\n ? path\n : path.slice(index + 1)\n}\n\n// The parent directory of a path, with its trailing separator, or EMPTY when\n// the path has none.\n// 'a/b/c' -> 'a/b/'; 'a/b/' -> 'a/'; 'a' -> EMPTY; 'a/' -> EMPTY\n//\n// A path holding an empty segment has to be taken apart, because its\n// ancestors are not prefixes of it: the parent of 'a//b' is 'a/', not\n// 'a//', and the parent of '/a/' is nothing at all. Both shapes reach here\n// -- 'a//b' is accepted outright, and `checkIgnore` does not put the path\n// it is given through the relative-path check.\n//\n// Every other path is a prefix of itself, and cutting one costs a fraction of\n// splitting it into an array and joining that back at every level: 73ns\n// against 207ns.\nconst parentOf = path => {\n if (\n path.charCodeAt(0) === SLASH_CODE\n || path.indexOf(DOUBLE_SLASH) >= 0\n ) {\n const slices = path.split(SLASH).filter(Boolean)\n\n slices.pop()\n\n return slices.length\n ? slices.join(SLASH) + SLASH\n : EMPTY\n }\n\n const end = path.length - 1\n\n // Look back from before a trailing separator, since that one belongs to the\n // path itself.\n //\n // A negative place to start would be a trap here: `lastIndexOf` clamps one\n // to 0 and searches there rather than reporting no match, so '/' would\n // come back as its own parent and the walk would never end. It cannot\n // happen -- that is the only path short enough to produce one, and the\n // test above has already sent it the other way.\n const cut = path.lastIndexOf(\n SLASH,\n path.charCodeAt(end) === SLASH_CODE\n ? end - 1\n : end\n )\n\n return cut < 0\n ? EMPTY\n : path.slice(0, cut + 1)\n}\n\nconst isString = subject => typeof subject === 'string'\n\n// Blank lines and comments are told by `createRule`, once the pattern is\n// normalized.\nconst checkPattern = pattern => pattern\n && isString(pattern)\n && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern)\n\nconst splitPattern = pattern => pattern\n.split(REGEX_SPLITALL_CRLF)\n.filter(Boolean)\n\nclass IgnoreRule {\n constructor (\n pattern,\n mark,\n body,\n ignoreCase,\n negative,\n prefix\n ) {\n this.pattern = pattern\n this.mark = mark\n this.negative = negative\n\n define(this, 'body', body)\n define(this, 'ignoreCase', ignoreCase)\n define(this, 'regexPrefix', prefix)\n }\n\n // Worked out on first use and kept behind an own property, the way `regex`\n // caches itself in `_regex`. Deciding it in the constructor instead would\n // add a fourth `defineProperty` to every rule ever built, which cost 4% of\n // every compile -- including the compiles of rules that are never matched\n // against anything.\n get _basenameOnly () {\n return define(this, '_basenameOnly', matchesBasename(this.body))\n }\n\n get regex () {\n const key = UNDERSCORE + MODE_IGNORE\n\n if (this[key]) {\n return this[key]\n }\n\n return this._make(MODE_IGNORE, key)\n }\n\n get checkRegex () {\n const key = UNDERSCORE + MODE_CHECK_IGNORE\n\n if (this[key]) {\n return this[key]\n }\n\n return this._make(MODE_CHECK_IGNORE, key)\n }\n\n _make (mode, key) {\n const str = pinWildcards(\n mode === MODE_IGNORE\n ? this.regexPrefix\n .replace(REGEX_REPLACE_TRAILING_WILDCARD, replaceTrailingWildcard)\n .replace(REGEX_REPLACE_TRAILING_DOUBLESTAR, replaceTrailingDoublestar)\n : checkSourceOf(this.body, this.regexPrefix)\n )\n\n const regex = this.ignoreCase\n ? new RegExp(str, 'i')\n : new RegExp(str)\n\n return define(this, key, regex)\n }\n}\n\nconst isLineEnd = char => char === '\\r' || char === '\\n'\n\n// Drop a line terminator, which can remain when patterns are passed as an\n// array, and then the trailing spaces. Git trims spaces only, and a space\n// escaped by an odd run of backslashes stays, unescaped, as the last one.\n// Scanned from the end: a regular expression can only try each start in\n// turn, which is quadratic over a long run of spaces or backslashes.\n// 'a ' -> 'a'; 'a\\ ' -> 'a '; 'a\\\\ ' -> 'a\\\\'; 'a\\t' -> 'a\\t'\nconst trimEnd = body => {\n let end = body.length\n\n while (end && isLineEnd(body[end - 1])) {\n end --\n }\n\n const lineEnd = end\n\n while (end && body[end - 1] === SPACE) {\n end --\n }\n\n if (end === lineEnd) {\n return body.slice(0, end)\n }\n\n let backslashes = 0\n\n while (backslashes < end && body[end - backslashes - 1] === ESCAPE) {\n backslashes ++\n }\n\n return backslashes % 2\n ? body.slice(0, end - 1) + SPACE\n : body.slice(0, end)\n}\n\nconst createRule = ({\n pattern,\n mark\n}, ignoreCase) => {\n // Git drops a BOM before it reads the first line, so before anything else.\n let body = pattern[0] === BOM\n ? pattern.slice(1)\n : pattern\n\n // > A line starting with # serves as a comment.\n if (body[0] === '#') {\n return\n }\n\n let negative = false\n\n // > An optional prefix \"!\" which negates the pattern;\n if (body[0] === '!') {\n negative = true\n body = body.slice(1)\n }\n\n // Normalize the body before checking which slashes anchor the pattern.\n // Otherwise `bar/ ` is mistaken for a pattern containing an inner slash.\n // Only a body ending in a space or a line terminator has anything to trim.\n const last = body[body.length - 1]\n\n if (last === SPACE || last === '\\r' || last === '\\n') {\n body = trimEnd(body)\n }\n\n // > A blank line matches no files, so it can serve as a separator for\n // > readability.\n // Nor does a lone \"!\", or anything else left empty here: an empty pattern\n // would compile to a regex matching every path.\n if (!body) {\n return\n }\n\n body = body\n // > Put a backslash (\"\\\") in front of the first \"!\" for patterns that\n // > begin with a literal \"!\", for example, `\"\\!important!.txt\"`.\n .replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, '!')\n // > Put a backslash (\"\\\") in front of the first hash for patterns that\n // > begin with a hash.\n .replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, '#')\n\n const regexPrefix = makeRegexPrefix(body)\n\n return new IgnoreRule(\n pattern,\n mark,\n body,\n ignoreCase,\n negative,\n regexPrefix\n )\n}\n\nclass RuleManager {\n constructor (ignoreCase) {\n this._ignoreCase = ignoreCase\n this._rules = []\n\n // How many of the rules git would tag `EXC_FLAG_NODIR`.\n //\n // The scan uses it to decide, once for the whole set, whether handing\n // those rules the basename is worth what it costs the others. The\n // shortcut saves a full-path scan on every rule it applies to and costs\n // a check on every rule it does not, so a set where almost nothing is\n // basename-only comes out behind -- a 955 pattern set with 44 of them\n // measured 19% slower with the shortcut always on.\n //\n // Deciding this by measurement rather than by meaning is safe: a\n // basename-only pattern gives the very same answer against the whole\n // path, it just takes longer to say so. The choice can only change how\n // fast the scan runs, never what it returns.\n this._basenameCount = 0\n }\n\n _add (pattern) {\n // #32\n if (pattern && pattern[KEY_IGNORE]) {\n this._rules = this._rules.concat(pattern._rules._rules)\n this._basenameCount += pattern._rules._basenameCount\n this._added = true\n return\n }\n\n if (isString(pattern)) {\n pattern = {\n pattern\n }\n }\n\n const rule = checkPattern(pattern.pattern)\n && createRule(pattern, this._ignoreCase)\n\n if (rule) {\n this._added = true\n this._rules.push(rule)\n\n // Deliberately not `rule._basenameOnly`: reading that would materialise\n // the rule's own copy, and the whole point of leaving it lazy is that\n // a rule which is compiled and never matched never pays for it.\n if (matchesBasename(rule.body)) {\n this._basenameCount ++\n }\n }\n }\n\n // @param {Array<string> | string | Ignore} pattern\n add (pattern) {\n this._added = false\n\n makeArray(\n isString(pattern)\n ? splitPattern(pattern)\n : pattern\n ).forEach(this._add, this)\n\n if (this._added) {\n this._literalRules = UNDEFINED\n }\n\n return this._added\n }\n\n // Match the literal 'abc/' for `checkIgnore`, last rule wins. Only a rule\n // ending in a wildcard can match it (see `checkSourceOf`), so the rest\n // are left out once, rather than tested or compiled for every path.\n testLiteral (path) {\n const rules = this._literalRules || (\n this._literalRules = this._rules.filter(\n ({body}) => body[body.length - (\n body[body.length - 1] === SLASH ? 2 : 1\n )] === '*'\n )\n )\n\n let ignored = false\n let unignored = false\n let matchedRule\n\n for (let index = rules.length - 1; index >= 0; index --) {\n const rule = rules[index]\n\n if (rule.checkRegex.test(path)) {\n ignored = !rule.negative\n unignored = rule.negative\n matchedRule = rule.negative\n ? UNDEFINED\n : rule\n break\n }\n }\n\n const ret = {\n ignored,\n unignored\n }\n\n if (matchedRule) {\n ret.rule = matchedRule\n }\n\n return ret\n }\n\n // Test one single path without recursively checking parent directories\n //\n // - checkUnignored `boolean` whether should check if the path is unignored,\n // setting `checkUnignored` to `false` could reduce additional\n // path matching.\n // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`\n\n // @returns {TestResult} true if a file is ignored\n test (path, checkUnignored, mode) {\n let ignored = false\n let unignored = false\n let matchedRule\n\n // Most of a .gitignore is patterns with no slash in them, and running\n // those against the whole path makes the regular expression engine walk\n // every directory name on the way to the only segment that could match.\n // Handing them the basename instead is what git does, and it is where\n // the time in a directory walk goes: the rule scan was two thirds of it.\n const rules = this._rules\n const {length} = rules\n\n const shortcut = this._basenameCount * 2 >= length\n\n const basename = shortcut\n ? basenameOf(path)\n : path\n\n // A plain loop rather than `forEach`, so that `path`, `basename` and\n // `shortcut` are locals. As a callback they became closure variables,\n // and reaching for one of those once per rule cost 10% of a scan over a\n // large rule set -- more than the shortcut they were there to serve.\n for (let index = 0; index < length; index ++) {\n const rule = rules[index]\n const {negative} = rule\n\n // | ignored : unignored\n // -------- | ---------------------------------------\n // negative | 0:0 | 0:1 | 1:0 | 1:1\n // -------- | ------- | ------- | ------- | --------\n // 0 | TEST | TEST | SKIP | X\n // 1 | TESTIF | SKIP | TEST | X\n\n // - SKIP: always skip\n // - TEST: always test\n // - TESTIF: only test if checkUnignored\n // - X: that never happen\n const skip = unignored === negative && ignored !== unignored\n || negative && !ignored && !unignored && !checkUnignored\n\n if (!skip && rule[mode].test(\n shortcut && rule._basenameOnly\n ? basename\n : path\n )) {\n ignored = !negative\n unignored = negative\n\n matchedRule = negative\n ? UNDEFINED\n : rule\n }\n }\n\n const ret = {\n ignored,\n unignored\n }\n\n if (matchedRule) {\n ret.rule = matchedRule\n }\n\n return ret\n }\n}\n\nconst throwError = (message, Ctor) => {\n throw new Ctor(message)\n}\n\nconst checkPath = (path, originalPath, doThrow) => {\n if (!isString(path)) {\n return doThrow(\n `path must be a string, but got \\`${originalPath}\\``,\n TypeError\n )\n }\n\n // We don't know if we should ignore EMPTY, so throw\n if (!path) {\n return doThrow(`path must not be empty`, TypeError)\n }\n\n // Check if it is a relative path\n if (checkPath.isNotRelative(path)) {\n const r = '`path.relative()`d'\n return doThrow(\n `path should be a ${r} string, but got \"${originalPath}\"`,\n RangeError\n )\n }\n\n return true\n}\n\n// > pathname should be a `path.relative()`d one\n//\n// The same thing `REGEX_TEST_INVALID_PATH` says, spelled out: a path is not\n// relative if it begins with a separator, or with `./` or `../`, or is\n// nothing but `.` or `..`. Every match is decided by the first three\n// characters, and this runs on every path handed to the library -- where it\n// was 55% of a cached lookup, more than the cache lookup itself.\nconst isNotRelative = path => {\n const first = path.charCodeAt(0)\n\n if (first === SLASH_CODE) {\n return true\n }\n\n if (first !== DOT_CODE) {\n return false\n }\n\n // '.'\n if (path.length === 1) {\n return true\n }\n\n const second = path.charCodeAt(1)\n\n // './'\n if (second === SLASH_CODE) {\n return true\n }\n\n if (second !== DOT_CODE) {\n return false\n }\n\n // '..' or '../'\n return path.length === 2 || path.charCodeAt(2) === SLASH_CODE\n}\n\ncheckPath.isNotRelative = isNotRelative\n\n// On windows, the following function will be replaced\n/* istanbul ignore next */\ncheckPath.convert = p => p\n\n\nclass Ignore {\n constructor ({\n ignorecase = true,\n ignoreCase = ignorecase,\n allowRelativePaths = false\n } = {}) {\n define(this, KEY_IGNORE, true)\n\n this._rules = new RuleManager(ignoreCase)\n this._strictPathCheck = !allowRelativePaths\n this._initCache()\n }\n\n _initCache () {\n // A cache for the result of `.ignores()`\n this._ignoreCache = Object.create(null)\n\n // A cache for the result of `.test()`\n this._testCache = Object.create(null)\n }\n\n add (pattern) {\n if (this._rules.add(pattern)) {\n // Some rules have just added to the ignore,\n // making the behavior changed,\n // so we need to re-initialize the result cache\n this._initCache()\n }\n\n return this\n }\n\n // legacy\n addPattern (pattern) {\n return this.add(pattern)\n }\n\n // @returns {TestResult}\n _test (originalPath, cache, checkUnignored) {\n const path = originalPath\n // Supports nullable path\n && checkPath.convert(originalPath)\n\n checkPath(\n path,\n originalPath,\n this._strictPathCheck\n ? throwError\n : RETURN_FALSE\n )\n\n return this._t(path, cache, checkUnignored)\n }\n\n checkIgnore (path) {\n // If the path doest not end with a slash, `.ignores()` is much equivalent\n // to `git check-ignore`\n if (path.charCodeAt(path.length - 1) !== SLASH_CODE) {\n return this.test(path)\n }\n\n // Like `git check-ignore`: a directory excluded by itself or by an\n // ancestor settles it, and only then is the literal 'abc/' matched\n // against the rules (see `checkSourceOf`).\n const dir = this._t(path, this._testCache, true)\n\n if (dir.ignored) {\n return dir\n }\n\n const literal = this._rules.testLiteral(path)\n\n return literal.ignored || literal.unignored\n ? literal\n : dir\n }\n\n _t (\n // The path to be tested\n path,\n\n // The cache for the result of a certain checking\n cache,\n\n // Whether should check if the path is unignored\n checkUnignored\n ) {\n if (path in cache) {\n return cache[path]\n }\n\n const parentPath = parentOf(path)\n\n // If the path contains a parent directory, check the parent first\n const parent = parentPath\n ? this._t(parentPath, cache, checkUnignored)\n : UNDEFINED\n\n return cache[path] = parent && parent.ignored\n // > It is not possible to re-include a file if a parent directory of\n // > that file is excluded.\n ? parent\n : this._rules.test(path, checkUnignored, MODE_IGNORE)\n }\n\n ignores (path) {\n return this._test(path, this._ignoreCache, false).ignored\n }\n\n createFilter () {\n return path => !this.ignores(path)\n }\n\n filter (paths) {\n return makeArray(paths).filter(this.createFilter())\n }\n\n // @returns {TestResult}\n test (path) {\n return this._test(path, this._testCache, true)\n }\n}\n\nconst factory = options => new Ignore(options)\n\nconst isPathValid = path =>\n checkPath(path && checkPath.convert(path), path, RETURN_FALSE)\n\n/* istanbul ignore next */\nconst setupWindows = () => {\n /* eslint no-control-regex: \"off\" */\n const makePosix = str => /^\\\\\\\\\\?\\\\/.test(str)\n || /[\"<>|\\u0000-\\u001F]+/u.test(str)\n ? str\n : str.replace(/\\\\/g, '/')\n\n checkPath.convert = makePosix\n\n // 'C:\\\\foo' <- 'C:\\\\foo' has been converted to 'C:/'\n // 'd:\\\\foo'\n const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\\//i\n checkPath.isNotRelative = path =>\n REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path)\n || isNotRelative(path)\n}\n\n\n// Windows\n// --------------------------------------------------------------\n/* istanbul ignore next */\nif (\n // Detect `process` so that it can run in browsers.\n typeof process !== 'undefined'\n && process.platform === 'win32'\n) {\n setupWindows()\n}\n\n// COMMONJS_EXPORTS ////////////////////////////////////////////////////////////\n\nmodule.exports = factory\n\n// Although it is an anti-pattern,\n// it is still widely misused by a lot of libraries in github\n// Ref: https://github.com/search?q=ignore.default%28%29&type=code\nfactory.default = factory\n\nmodule.exports.isPathValid = isPathValid\n\n// For testing purposes\ndefine(module.exports, Symbol.for('setupWindows'), setupWindows)\n","import type { CheckResult, PermissionHandler, Rule } from \"../types.ts\"\n\nimport { normPath, prettyPath } from \"@zaly/shared\"\nimport ignore from \"ignore\"\nimport { dirname, isAbsolute, relative } from \"pathe\"\n\n/**\n * Handler for `Rule<\"read\" | \"write\">` — one impl serves both scopes\n * because they share the same matcher and the same workspace state.\n *\n * Algorithm:\n * 1. Normalize candidate to an absolute path.\n * 2. Sensitive-file check (env, ssh keys, credentials) → deny.\n * 3. Find the longest-prefix workspace containing the path. None →\n * ask, with a `kind: \"workspace\"` suggestion for the containing\n * directory.\n * 4. Inside a workspace: resolve the first matching rule against the\n * workspace-relative path. Earlier rules win.\n * 5. Default when no rule matches but the path is inside a workspace:\n * `allow` for reads (workspace = trust signal), `ask` for writes\n * (mutating ops escalate by default; users add `Write(...)` rules\n * to whitelist).\n *\n * Pattern grammar (per Claude-Code conventions):\n * //abs/path absolute filesystem path\n * ~/path path under home directory\n * /path or path workspace-relative (passed through to gitignore)\n */\nexport const fileHandler: PermissionHandler<\"read\" | \"write\"> = {\n validate(input, ctx): CheckResult {\n const abs = normPath(ctx.cwd, input)\n\n if (isSensitiveFile(abs)) {\n return { reason: `${abs}: sensitive file`, verdict: \"deny\" }\n }\n\n const ws = longestWorkspace(abs, ctx.workspaces)\n const inWorkspace = ws !== undefined\n\n // Inside-workspace + path === root: the workspace itself (e.g. `find .`\n // resolved to cwd). Workspace containment alone is enough to allow.\n if (inWorkspace && abs === ws) return { verdict: \"allow\" }\n\n // Outside any workspace, fall through to rule matching with `/` as\n // synthetic root. Lets blanket rules like `read(*)` (yolo preset)\n // override the default \"ask outside workspace\" verdict, while\n // narrower presets keep the safer default.\n const base = ws ?? \"/\"\n const rel = relative(base, abs)\n\n const verb = ctx.scope === \"read\" ? \"reading\" : \"writing\"\n\n const rule = resolveRule(base, rel, ctx.rules)\n if (rule?.policy === \"deny\") return { reason: `${abs}: denied by rule`, verdict: \"deny\" }\n if (rule?.policy === \"ask\") {\n return {\n ask: `Allow ${verb} ${prettyPath(abs)}?`,\n reason: `${abs}: rule requires confirmation`,\n suggestions: [{ kind: \"rule\", pattern: `/${rel}`, scope: ctx.scope }],\n verdict: \"ask\",\n }\n }\n if (rule?.policy === \"allow\") return { verdict: \"allow\" }\n\n // No rule matched. Defaults differ by location:\n // - Inside a workspace: reads ride on workspace trust → allow;\n // writes escalate → ask.\n // - Outside any workspace: always ask, and surface a\n // workspace-add suggestion so the user can promote the parent\n // dir without composing a rule by hand.\n if (!inWorkspace) {\n const parent = dirname(abs)\n return {\n ask: `Allow ${verb} ${prettyPath(abs)}? (not in any workspace)`,\n reason: `${abs}: outside any workspace`,\n suggestions: [\n { description: `add ${parent} as workspace`, kind: \"workspace\", path: parent },\n ],\n verdict: \"ask\",\n }\n }\n if (ctx.scope === \"read\") return { verdict: \"allow\" }\n return {\n ask: `Allow ${verb} ${prettyPath(abs)}?`,\n reason: `${abs}: write requires confirmation`,\n suggestions: [{ kind: \"rule\", pattern: `/${rel}`, scope: \"write\" }],\n verdict: \"ask\",\n }\n },\n}\n\n/** True if `path` looks like a credentials/keys file we shouldn't\n * expose to the model by default. Conservative — false positives are\n * fine here, and users can override via `allow` rules. */\nexport function isSensitiveFile(path: string): boolean {\n return SENSITIVE_PATTERNS.some((re) => re.test(path))\n}\n\n/** Common locations for credentials, keys, and other sensitive data.\n * Matched against the path string directly — covers both absolute and\n * relative inputs. Add to this list as new gotchas surface. */\nconst SENSITIVE_PATTERNS: RegExp[] = [\n /(?:^|\\/)\\.env(?:\\.|$)/, // .env, .env.local, .env.production, ...\n /(?:^|\\/)\\.git\\/(?!hooks\\/)/, // .git/* but not hooks (commonly inspected)\n /(?:^|\\/)\\.ssh\\//, // ssh keys + config\n /(?:^|\\/)\\.aws\\/credentials/,\n /(?:^|\\/)\\.netrc$/,\n /(?:^|\\/)id_(?:rsa|dsa|ed25519|ecdsa)(?:\\.pub)?$/,\n /(?:^|\\/)\\.npmrc$/, // can hold auth tokens\n /(?:^|\\/)\\.pypirc$/,\n /(?:^|\\/)secrets?\\//,\n /(?:^|\\/)credentials?(?:\\.|\\/)/,\n]\n\n/** Longest-prefix match against the workspace list — returns the\n * containing workspace's absolute path, or undefined if none contain\n * the candidate. Workspaces arrive pre-normalized from the manager. */\nfunction longestWorkspace(abs: string, workspaces: readonly string[]): string | undefined {\n let best: string | undefined\n for (const root of workspaces) {\n if (abs === root || abs.startsWith(root.endsWith(\"/\") ? root : `${root}/`)) {\n if (best === undefined || root.length > best.length) best = root\n }\n }\n return best\n}\n\n/** Resolve the first matching rule.\n * Patterns get normalized (`//abs`, `~/`, `/rel`, `rel`) into\n * workspace-rooted gitignore syntax; patterns that resolve outside the\n * candidate's workspace are dropped (they don't apply here). */\nfunction resolveRule(\n workspace: string,\n rel: string,\n rules: readonly Rule<\"read\" | \"write\">[]\n): Rule<\"read\" | \"write\"> | undefined {\n if (rel === \"\") return undefined\n for (const rule of rules) {\n const norm = normalizePattern(rule.pattern, workspace)\n if (norm === undefined) continue\n try {\n if (ignore().add(norm).ignores(rel)) return rule\n } catch {\n // Skip invalid rels.\n }\n }\n}\n\nfunction normalizePattern(pattern: string, workspace: string): string | undefined {\n if (pattern.startsWith(\"//\")) {\n return relativizeToWorkspace(pattern.slice(1), workspace)\n }\n if (pattern.startsWith(\"~/\")) {\n return relativizeToWorkspace(normPath(pattern), workspace)\n }\n // /pattern or pattern → already workspace-relative gitignore syntax.\n return pattern\n}\n\nfunction relativizeToWorkspace(absPattern: string, workspace: string): string | undefined {\n const rel = relative(workspace, absPattern)\n if (rel === \"\" || rel.startsWith(\"..\") || isAbsolute(rel)) return undefined\n return `/${rel}`\n}\n","import type { CheckResult, PermissionHandler, Rule } from \"../types.ts\"\n\nimport ignore from \"ignore\"\n\n/**\n * Generic tool-scope handler.\n *\n * Used by `Tasks` to gate every tool dispatch on the tool name, and\n * available to tools themselves for richer checks (e.g. fetch passing\n * `fetch:example.com` so users can ask-for-domain).\n *\n * Pattern grammar is gitignore-style — reuses the `ignore` package so\n * the syntax matches `read` / `write` patterns. Common shapes:\n *\n * \"tool\" → bare scope; matches every input (default-allow)\n * \"tool(bash)\" → exact match for `\"bash\"`\n * \"tool(fetch:*)\" → glob match `\"fetch:...\"`\n * \"tool(task_*)\" → glob match `\"task_...\"`\n *\n * Resolution is first-match-wins. Default verdict when no rule matches is\n * `allow` — an unconfigured `tool` scope should not surprise users by\n * gating ordinary tool calls.\n */\nexport const toolHandler: PermissionHandler<\"tool\"> = {\n validate(input, ctx): CheckResult {\n const rule = resolveRule(input, ctx.rules)\n if (rule?.policy === \"deny\") {\n return { reason: `tool ${input}: denied by rule`, verdict: \"deny\" }\n }\n if (rule?.policy === \"ask\") {\n return {\n ask: `Allow tool ${input}?`,\n reason: `tool ${input}: rule requires confirmation`,\n suggestions: [{ kind: \"rule\", pattern: input, scope: \"tool\" }],\n verdict: \"ask\",\n }\n }\n return { verdict: \"allow\" }\n },\n}\n\nfunction resolveRule(input: string, rules: readonly Rule<\"tool\">[]): Rule<\"tool\"> | undefined {\n for (const rule of rules) {\n if (ignore().add(rule.pattern).ignores(input)) return rule\n }\n}\n","import type { PermissionHandler } from \"../types.ts\"\n\nimport { createRegistry } from \"@zaly/shared/registry\"\nimport { bashHandler } from \"./bash.ts\"\nimport { fileHandler } from \"./files.ts\"\nimport { toolHandler } from \"./tool.ts\"\n\nexport type HandlerLoader = () => PermissionHandler<string>\n\nconst builtin = {\n bash: () => bashHandler,\n read: () => fileHandler,\n tool: () => toolHandler,\n write: () => fileHandler,\n} as const satisfies Record<string, HandlerLoader>\n\n/** Open registry of permission scopes — declaration-merge to add your\n * own. The keys give `ctx.need(\"…\", input)` typed autocomplete and\n * catch typos at compile time. The value carries the *input* type for\n * that scope (almost always `string` — files take paths, bash takes\n * commands, the generic `tool` scope takes the tool name).\n *\n * Built-ins:\n * - `bash` → command string\n * - `read` → absolute path\n * - `write` → absolute path\n * - `tool` → tool name (or richer `name:arg` shape per tool)\n *\n * Add your own scope:\n *\n * ```ts\n * declare module \"@zaly/agent\" {\n * interface PermissionScopes {\n * fetch: string // expects a domain (or \"*\" for any)\n * }\n * }\n * ```\n *\n * Then `ctx.need(\"fetch\", domain)` is typed end-to-end. You still need\n * to register a runtime handler via `handlerRegistry.register(\"fetch\", …)`\n * — this interface is only the type-side declaration.\n */\nexport interface PermissionScopes {\n bash: string\n read: string\n tool: string\n write: string\n}\n\nexport type BuiltinScope = keyof typeof builtin\nexport type PermissionScope = keyof PermissionScopes\nexport type AnyScope = PermissionScope | (string & {})\n\nexport const handlerRegistry = createRegistry<HandlerLoader>(\"scope\").from(builtin)\n"],"ignoreList":[1],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,cAAyC,EACpD,SAAS,OAAO,KAAkB;CAChC,MAAM,SAAS,UAAU,KAAK;CAC9B,IAAI,CAAC,OAAO,IAAI;EACd,IAAI;GAAC;GAAQ;GAAQ;EAAO,CAAC,CAAC,OAAO,UAAU,IAAI,YAAY,KAAK,CAAC,GACnE,OAAO,EAAE,SAAS,QAAQ;EAE5B,OAAO;GACL,KAAK,6BAA6B,OAAO,OAAO;GAChD,QAAQ,OAAO;GACf,SAAS;EACX;CACF;CAEA,IAAI,UAAmB;CACvB,IAAI;CACJ,MAAM,cAA4B,CAAC;CAEnC,KAAK,MAAM,OAAO,OAAO,UAAU;EAIjC,MAAM,aAAa,aAAa,IAAI,OAAO,GAAG,KAAK;EACnD,IAAI,IAAI,mBAAmB,eAAe,SACxC,OAAO;GACL,KAAK,mCAAmC,IAAI,IAAI;GAChD,QAAQ;GACR,SAAS;EACX;EAEF,MAAM,OAAO,MAAM,IAAI;EACvB,IAAI,MAAM,SAAS,IAAI,IAAI,KAAK,eAAe,SAC7C,OAAO;GACL,KAAK,yBAAyB,IAAI,IAAI;GACtC,QAAQ,GAAG,IAAI,IAAI;GACnB,SAAS;EACX;EAGF,IAAI,aAAsB;EAC1B,IAAI,YACF,eAAe,UAAU,KAAA,IAAY,YAAY,IAAI,IAAI,MAAM;EACjE,IAAI,eAAe,OACjB,YAAY,KAAK;GACf,MAAM;GACN,SAAS,IAAI,KAAK,SAAS,IAAI,GAAG,IAAI,IAAI,MAAM,IAAI;GACpD,OAAO;EACT,CAAC;EAKH,MAAM,QAAQ,CAAC,GAAI,MAAM,QAAQ,IAAI,IAAI,KAAK,CAAC,GAAI,GAAG,IAAI,KAAK;EAC/D,MAAM,SAAS,CAAC,GAAI,MAAM,SAAS,IAAI,IAAI,KAAK,CAAC,GAAI,GAAG,IAAI,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC;EAErF,KAAK,MAAM,QAAQ,OAAO;GACxB,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,YAAY,MAAM,GAAG;IAClD,aAAa,QAAQ,YAAY,KAAK;IACtC,cAAc,GAAG,IAAI,IAAI,SAAS,KAAK;IACvC,YAAY,KAAK;KACf,MAAM;KACN,SAAS,GAAG,IAAI,IAAI;KACpB,OAAO;IACT,CAAC;IACD;GACF;GACA,MAAM,IAAI,IAAI,SAAS,QAAQ,IAAI;GACnC,aAAa,QAAQ,YAAY,EAAE,OAAO;GAC1C,IAAI,EAAE,YAAY,SAAS;IACzB,cAAc,GAAG,IAAI,IAAI,SAAS,KAAK,IAAI,EAAE;IAC7C,IAAI,EAAE,aAAa,YAAY,KAAK,GAAG,EAAE,WAAW;GACtD;GACA,IAAI,eAAe,QAAQ;EAC7B;EACA,IAAI,eAAe,QACjB,KAAK,MAAM,QAAQ,QAAQ;GACzB,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,YAAY,OAAO,GAAG;IACnD,aAAa,QAAQ,YAAY,KAAK;IACtC,cAAc,GAAG,IAAI,IAAI,UAAU,KAAK;IACxC,YAAY,KAAK;KACf,MAAM;KACN,SAAS,GAAG,IAAI,IAAI;KACpB,OAAO;IACT,CAAC;IACD;GACF;GACA,MAAM,IAAI,IAAI,SAAS,SAAS,IAAI;GACpC,aAAa,QAAQ,YAAY,EAAE,OAAO;GAC1C,IAAI,EAAE,YAAY,SAAS;IACzB,cAAc,GAAG,IAAI,IAAI,UAAU,KAAK,IAAI,EAAE;IAC9C,IAAI,EAAE,aAAa,YAAY,KAAK,GAAG,EAAE,WAAW;GACtD;GACA,IAAI,eAAe,QAAQ;EAC7B;EAGF,UAAU,QAAQ,SAAS,UAAU;EACrC,IAAI,YAAY,WAAW,WAAW,KAAA,GAAW,SAAS;EAC1D,IAAI,YAAY,QAAQ;CAC1B;CAEA,OAAO,YAAY,UACf,EAAE,SAAS,QAAQ,IACnB;EACE,KAAK;EACL,QAAQ,UAAU;EAClB,aAAa,YAAY,SAAS,IAAI,cAAc,KAAA;EACpD;CACF;AACN,EACF;;AA6BA,SAAS,QAAQ,GAAY,GAAqB;CAChD,IAAI,MAAM,UAAU,MAAM,QAAQ,OAAO;CACzC,IAAI,MAAM,SAAS,MAAM,OAAO,OAAO;CACvC,OAAO;AACT;AAEA,MAAM,+BAAe,IAAI,IAA2B;AAEpD,SAAS,aAAa,SAAgC;CACpD,MAAM,SAAS,aAAa,IAAI,OAAO;CACvC,IAAI,QAAQ,OAAO;CAEnB,IAAI,OAAO,QAAQ,KAAK;CACxB,IAAI,mBAAmB;CACvB,IAAI,KAAK,SAAS,IAAI,GAAG;EACvB,mBAAmB;EACnB,OAAO,KAAK,MAAM,GAAG,EAAE;CACzB;CAEA,MAAM,SAAS,KAAK,MAAM,KAAK,CAAC,CAAC,QAAQ,MAAM,EAAE,SAAS,CAAC;CAE3D,MAAM,SAAwB;EAC5B,KAFU,OAAO,MAAM;EAGvB,OAAO,OAAO,MAAM,CAAC;EACrB;CACF;CACA,aAAa,IAAI,SAAS,MAAM;CAChC,OAAO;AACT;AAEA,SAAS,UAAU,MAAc,OAAwB;CACvD,IAAI,KAAK,SAAS,GAAG,GACnB,OAAO,MAAM,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;CAE3C,OAAO,SAAS;AAClB;;AAGA,SAAgB,UAAU,MAAY,KAAuB;CAC3D,MAAM,IAAI,aAAa,KAAK,OAAO;CAEnC,IAAI,EAAE,QAAQ,OAAO,EAAE,MAAM,WAAW,GAAG,OAAO;CAClD,IAAI,EAAE,QAAQ,IAAI,KAAK,OAAO;CAC9B,IAAI,IAAI,KAAK,SAAS,EAAE,MAAM,QAAQ,OAAO;CAC7C,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,MAAM,QAAQ,KAClC,IAAI,CAAC,UAAU,EAAE,MAAM,IAAI,IAAI,KAAK,EAAE,GAAG,OAAO;CAElD,IAAI,EAAE,kBAAkB,OAAO;CAC/B,OAAO,IAAI,KAAK,WAAW,EAAE,MAAM;AACrC;;AAGA,SAAS,aAAa,OAAwB,KAAmC;CAC/E,KAAK,MAAM,KAAK,OACd,IAAI,UAAU,GAAG,GAAG,GAAG,OAAO,EAAE;AAGpC;;;;CC5NA,SAAS,UAAW,SAAS;EAC3B,OAAO,MAAM,QAAQ,OAAO,IACxB,UACA,CAAC,OAAO;CACd;CAEA,MAAM,YAAY,KAAA;CAClB,MAAM,QAAQ;CACd,MAAM,QAAQ;CACd,MAAM,SAAS;CAIf,MAAM,wBAAwB;CAC9B,MAAM,MAAM;CACZ,MAAM,mCAAmC;CACzC,MAAM,4CAA4C;CAClD,MAAM,qCAAqC;CAC3C,MAAM,sBAAsB;CAU5B,MAAM,eAAe;CACrB,MAAM,aAAa;CACnB,MAAM,WAAW;CAGjB,MAAM,QAAQ;CAGd,IAAI,iBAAiB;;CAErB,IAAI,OAAO,WAAW,aACpB,iBAAiB,OAAO,IAAI,aAAa;CAE3C,MAAM,aAAa;CAEnB,MAAM,UAAU,QAAQ,KAAK,UAAU;EACrC,OAAO,eAAe,QAAQ,KAAK,EAAC,MAAK,CAAC;EAC1C,OAAO;CACT;CAEA,MAAM,qBAAqB;CAG3B,MAAM,uBAAsB,YAAW;EACrC,MAAM,EAAC,WAAU;EACjB,OAAO,QAAQ,MAAM,GAAG,SAAS,SAAS,CAAC;CAC7C;CAwBA,MAAM,gBAAgB;EACpB,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EAGP,OAAO;EACP,OAAO;EACP,QAAQ;CACV;CAEA,MAAM,0BAA0B;CAEhC,MAAM,gBAAe,SAAQ,wBAAwB,QAAQ,IAAI,IAAI,IACjE,OACA,SAAS;CASb,MAAM,YAAY;CAElB,MAAM,eAAe,SAAS,SAAS;EACrC,IAAI,SACF,OAAO,QAAQ,KAAK;EAGtB,MAAM,SAAS,IAAI,KAAK;EAExB,OAAO,IAAI,OAAO,MAAM,CAAC,CAAC,KAAK,GAAG,IAC9B,YAAY,SACZ;CACN;CAMA,MAAM,eAAe,SAAS,UAAU;EACtC,MAAM,EAAC,WAAU;EACjB,IAAI,QAAQ,QAAQ;EACpB,IAAI,UAAU;EAEd,MAAM,OAAO,QAAQ;EACrB,IAAI,SAAS,OAAO,SAAS,KAAK;GAChC,UAAU;GACV;EACF;EAEA,IAAI,OAAO;EAKX,IAAI,OAAO;EAKX,SAAS;GACP,MAAM,OAAO,QAAQ;GAErB,IAAI,SAAS,WACX,OAAO;GAGT,IAAI,SAAS,QAAQ;IACnB,MAAM,UAAU,QAAQ,QAAQ;IAChC,IAAI,YAAY,WACd,OAAO;IAET,QAAQ,aAAa,OAAO;IAC5B,OAAO;IACP;GACF,OAAO,IACL,SAAS,OACN,QACA,QAAQ,IAAI,UACZ,QAAQ,QAAQ,OAAO,KAC1B;IACA;IACA,IAAI,KAAK,QAAQ;IACjB,IAAI,OAAO,QAGT,KAAK,QAAQ,SAAS;IAMxB,IAAI,QAAQ,IACV,QAAQ,IAAI,aAAa,EAAE;IAE7B,OAAO;GACT,OAAO,IAAI,SAAS,OAAO,QAAQ,QAAQ,OAAO,KAAK;IACrD,MAAM,YAAY,QAAQ;IAC1B,IAAI,MAAM;IACV,OAAO,MAAM,UAAU,QAAQ,SAAS,KACtC;IAGF,IAAI,QAAQ,QACV,OAAO;IAGT,IAAI,MAAM,aAAa,QAAQ,MAAM,OAAO,KAAK;KAC/C,MAAM,WAAW,cAAc,QAAQ,MAAM,WAAW,MAAM,CAAC;KAG/D,IAAI,aAAa,WACf,OAAO;KAGT,QAAQ;KACR,OAAO;KACP,QAAQ;IACV,OAAO;KAGL,QAAQ,aAAa,GAAG;KACxB,OAAO;KACP,QAAQ,YAAY;IACtB;GACF,OAAO;IACL,QAAQ,aAAa,IAAI;IACzB,OAAO;GACT;GAEA;GAEA,IAAI,QAAQ,WAAW,KACrB,OAAO;IACL,KAAK;IACL,QAAQ,YAAY,SAAS,IAAI;GACnC;EAEJ;CACF;CAIA,MAAM,cAAc;CAKpB,MAAM,cAAc;CACpB,MAAM,4BAA4B,IAAI,OACpC,GAAG,YAAY,QAAQ,eAAe,GACxC;CAWA,MAAM,oBAAoB;CAK1B,MAAM,sBAAsB;CAI5B,MAAM,mBAAkB,YAAW;EACjC,MAAM,UAAU,CAAC;EACjB,MAAM,QAAO,WACX,GAAG,cAAc,QAAQ,KAAK,MAAM,IAAI,IAAI;EAE9C,MAAM,EAAC,WAAU;EACjB,IAAI,MAAM;EACV,IAAI,QAAQ;EAEZ,OAAO,QAAQ,QAAQ;GACrB,MAAM,OAAO,QAAQ;GAErB,IAAI,SAAS,QAAQ;IAgBnB,MAAM,UAAU,QAAQ,QAAQ;IAEhC,IACE,YAAY,aACT,YAAY,SAAS,QAAQ,MAAM,QAItC,OAAO,KAAK,WAAW;SAClB,IACL,YAAY,OACT,YAAY,OACZ,YAAY,SACZ,YAAY,QAEf,OAAO,QAAQ,MAAM,OAAO,QAAQ,CAAC;SAErC,OAAO,KACL,sBAAsB,KAAK,OAAO,IAC9B,SAAS,UACT,OACN;IAGF,SAAS;GACX,OAAO,IAAI,SAAS,aAAa;IAG/B,OAAO,KAAK,IAAI,YAAY,EAAE;IAC9B;GACF,OAAO,IAAI,SAAS,KAAK;IACvB,MAAM,UAAU,YAAY,SAAS,KAAK;IAE1C,IAAI,YAAY,MAAM;KAGpB,OAAO,KAAK,WAAW;KACvB,QAAQ;IACV,OAAO;KACL,OAAO,KAAK,QAAQ,MAAM;KAC1B,QAAQ,QAAQ,MAAM;IACxB;GACF,OAAO;IACL,OAAO;IACP;GACF;EACF;EAEA,OAAO;GACL,QAAQ;GACR;EACF;CACF;CAWA,MAAM,SAAS;CAIf,MAAM,oBAAoB;CA4B1B,MAAM,YAAY;EAShB;GAIE;IACC,GAAG,KAAK,UAAU,QACf,IAAI,MAAM,GAAG,IAAI,SAAS,IAAI,SAAS,CAAC,IAAI,QAC5C;GACJ;EACF;EAmBA,CACE,mBACA,UAAS,KAAK,OAChB;EAEA;GAEE;SACM;GACN;EACF;EAGA;GAKE;SACM;GACN;EACF;EAGA;GACE;SACM;GACN;EACF;EAEA;GAOE;SAGM;GACN;EACF;EAGA,CAIE,SACC,QAAQ,YAAY;GAEnB,IAAI,CAAC,UAAU,OAAO,OAAO,KAC3B,OAAO;GAuBT,QAnBe,CAAC,kBAAkB,KAAK,OAAO,IAa1C,cAIA,OAEY;EAClB,CACF;EAGA;GAEE;IAMC,GAAG,OAAO,QAAQ,QAAQ,IAAI,IAAI,SAO/B,IAAI,MAAM,QAAQ,CAAC,MAAM,QACvB,oBAOA,oBAOF,MAAM;GACV;EACF;EAGA;GAOE;IAIC,GAAG,IAAI,OAAO;IAOb,OAAO,KADW,GAAG,QAAQ,SAAS,SAClB;GACtB;GACA;EACF;EAGA;GAUE;IAEC,OAAO,IAAI,OAET,GAAG,SAAS,IAAK,MAAM,IAIpB,KAAK,KAAK,oBAEV;GACN;EACF;EAEA;GAIE;SACM;GACN;EACF;EAEA;GAEE;SACM;GACN;EACF;EAEA;GAME;IAGC,OAAO,OAAO,WAAW,UACxB,MAAM,QAAQ,oBAAoB,SAAS,IAAI;GACjD;EACF;EAGA,CAGE,SAcA,WAAU;GACR,MAAM,OAAO,OAAO,OAAO,SAAS;GAMpC,IAAI,CAAC,QAAQ,SAAS,qBAAqB,SAAS,qBAClD,OAAO;GAGT,OAAO,SAAS,QAEZ,GAAG,OAAO,KAEV,GAAG,OAAO;EAChB,CACF;CACF;CAEA,MAAM,kCAAkC;CACxC,MAAM,cAAc;CACpB,MAAM,oBAAoB;CAG1B,MAAM,2BAA2B,GAAG,OAAO;EAczC,OAAO,GAbQ,KAOX,GAAG,GAAG,SAIN,QAEa;CACnB;CAEA,MAAM,oCAAoC;CAG1C,MAAM,kCAAkC;CAExC,MAAM,WAAW;CAmBjB,MAAM,kBAAkB,KAAK,OAAO;EAClC,IAAI,YAAY;EAEhB,KAAK,IAAI,QAAQ,KAAK,GAAG,QAAQ,IAAI,UAAU,CAAC,IAAI,MAAM,CAAC,UAAU,SACnE,aAAa,IAAI,MAAM,CAAC;EAG1B,OAAO;CACT;CAEA,MAAM,gBAAe,WAAU;EAC7B,IAAI,OAAO,QAAQ,QAAQ,IAAI,GAC7B,OAAO;EAGT,MAAM,SAAS,CAAC;EAChB,MAAM,EAAC,WAAU;EACjB,IAAI,QAAQ;EAEZ,OAAO,QAAQ,QAAQ;GACrB,MAAM,OAAO,OAAO;GAEpB,IAAI,OAAO,WAAW,UAAU,KAAK,GAAG;IACtC,OAAO,KAAK,EAAC,UAAU,KAAI,CAAC;IAC5B,SAAS;GACX,OAAO,IAAI,SAAS,KAAK;IACvB,IAAI,MAAM,QAAQ;IAElB,IAAI,OAAO,SAAS,KAClB;IAGF,IAAI,OAAO,SAAS,KAClB;IAGF,OAAO,MAAM,UAAU,OAAO,SAAS,KACrC,OAAO,OAAO,SAAS,SACnB,IACA;IAGN;IACA,OAAO,KAAK,EAAC,QAAQ,OAAO,MAAM,OAAO,GAAG,EAAC,CAAC;IAC9C,QAAQ;GACV,OAAO,IAAI,SAAS,QAAQ;IAC1B,OAAO,KAAK,EAAC,QAAQ,OAAO,MAAM,OAAO,QAAQ,CAAC,EAAC,CAAC;IACpD,SAAS;GACX,OAAO,IAAI,SAAS,KAAK;IACvB,IAAI,QAAQ;IACZ,IAAI,MAAM;IAEV,GAAG;KACD,IAAI,OAAO,SAAS,QAClB;UACK,IAAI,OAAO,SAAS,KACzB;UACK,IAAI,OAAO,SAAS,KACzB;KAGF;IACF,SAAS,MAAM,UAAU,QAAQ;IAEjC,IAAI,MAAM,QAAQ,OAAO,IAAI,KAAK,GAChC;IAGF,OAAO,KAAK,EAAC,UAAU,OAAO,MAAM,OAAO,GAAG,EAAC,CAAC;IAChD,QAAQ;GACV,OAAO,IAAI,SAAS,OAAO,SAAS,KAAK;IACvC,OAAO,KAAK,EAAC,UAAU,KAAI,CAAC;IAC5B;GACF,OAAO;IACL,OAAO,KAAK,EAAC,QAAQ,KAAI,CAAC;IAC1B;GACF;EACF;EAEA,IAAI,MAAM;EACV,IAAI,MAAM,CAAC;EAEX,MAAM,cAAc;GAClB,IAAI;GAEJ,IAAI,SAAS,OAAO,OAAO;IACzB,IAAI,MAAM,UACR,eAAe;GAEnB,CAAC;GAED,IAAI,SAAS,OAAO,OAAO;IACzB,IAAI,CAAC,MAAM,UAAU;KACnB,OAAO,MAAM;KACb;IACF;IAYA,OAAO,OAAO,eACV,WACA,SAAS,eAAe,KAAK,EAAE,EAAE;GACvC,CAAC;GAED,MAAM,CAAC;EACT;EAEA,OAAO,SAAQ,UAAS;GACtB,IAAI,MAAM,aAAa,KAAA,GAAW;IAChC,IAAI,KAAK,KAAK;IACd;GACF;GAEA,MAAM;GACN,OAAO,MAAM;EACf,CAAC;EAED,MAAM;EAEN,OAAO;CACT;CAGA,MAAM,mBAAkB,YAAW;EACjC,MAAM,EAAC,QAAQ,YAAW,gBAAgB,OAAO;EAEjD,MAAM,WAAW,UAAU,QAKxB,MAAM,CAAC,SAAS,UAAU,cAAc;GACvC,IAAI,YAAY,QACd,OAAO,SAAS,MAAM,OAAO;GAG/B,IAAI,aAAa,aAAa,KAAK,QAAQ,QAAQ,IAAI,GACrD,OAAO;GAKT,OAAO,QAAQ,KAAK,IAAI,IACpB,KAAK,QAAQ,SAAS,SAAS,KAAK,OAAO,CAAC,IAC5C;EACN,GACA,MACF;EAIA,OAAO,QAAQ,SACX,SAAS,QACT,4BACC,OAAO,UAAU,QAAQ,MAC5B,IACE;CACN;CASA,MAAM,iBAAiB,MAAM,WAAW;EACtC,IAAI,KAAK,KAAK,SAAS,OAAO,OAC5B,SAAS,gBAAgB,KAAK,MAAM,GAAG,EAAG,CAAC;EAG7C,MAAM,OAAO,OAAO,MAAM,GAAG,EAAG;EAChC,MAAM,OAAO,OAAO,OAAO,SAAS;EAEpC,OAAO,SAAS,oBACZ,GAAG,KAAK,KACR,SAAS,sBACP,GAAG,KAAK,OACR;CACR;CAOA,MAAM,mBAAkB,SAAQ;EAC9B,MAAM,QAAQ,KAAK,QAAQ,KAAK;EAEhC,OAAO,QAAQ,KAAK,UAAU,KAAK,SAAS;CAC9C;CAKA,MAAM,cAAa,SAAQ;EACzB,MAAM,MAAM,KAAK,SAAS;EAE1B,MAAM,QAAQ,KAAK,YACjB,OACA,KAAK,SAAS,QACV,MAAM,IACN,GACN;EAEA,OAAO,QAAQ,IACX,OACA,KAAK,MAAM,QAAQ,CAAC;CAC1B;CAeA,MAAM,YAAW,SAAQ;EACvB,IACE,KAAK,WAAW,CAAC,MAAM,cACpB,KAAK,QAAQ,YAAY,KAAK,GACjC;GACA,MAAM,SAAS,KAAK,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO;GAE/C,OAAO,IAAI;GAEX,OAAO,OAAO,SACV,OAAO,KAAK,KAAK,IAAI,QACrB;EACN;EAEA,MAAM,MAAM,KAAK,SAAS;EAU1B,MAAM,MAAM,KAAK,YACf,OACA,KAAK,WAAW,GAAG,MAAM,aACrB,MAAM,IACN,GACN;EAEA,OAAO,MAAM,IACT,QACA,KAAK,MAAM,GAAG,MAAM,CAAC;CAC3B;CAEA,MAAM,YAAW,YAAW,OAAO,YAAY;CAI/C,MAAM,gBAAe,YAAW,WAC3B,SAAS,OAAO,KAChB,CAAC,iCAAiC,KAAK,OAAO;CAEnD,MAAM,gBAAe,YAAW,QAC/B,MAAM,mBAAmB,CAAC,CAC1B,OAAO,OAAO;CAEf,IAAM,aAAN,MAAiB;EACf,YACE,SACA,MACA,MACA,YACA,UACA,QACA;GACA,KAAK,UAAU;GACf,KAAK,OAAO;GACZ,KAAK,WAAW;GAEhB,OAAO,MAAM,QAAQ,IAAI;GACzB,OAAO,MAAM,cAAc,UAAU;GACrC,OAAO,MAAM,eAAe,MAAM;EACpC;EAOA,IAAI,gBAAiB;GACnB,OAAO,OAAO,MAAM,iBAAiB,gBAAgB,KAAK,IAAI,CAAC;EACjE;EAEA,IAAI,QAAS;GACX,MAAM,MAAM;GAEZ,IAAI,KAAK,MACP,OAAO,KAAK;GAGd,OAAO,KAAK,MAAM,aAAa,GAAG;EACpC;EAEA,IAAI,aAAc;GAChB,MAAM,MAAM;GAEZ,IAAI,KAAK,MACP,OAAO,KAAK;GAGd,OAAO,KAAK,MAAM,mBAAmB,GAAG;EAC1C;EAEA,MAAO,MAAM,KAAK;GAChB,MAAM,MAAM,aACV,SAAS,cACL,KAAK,YACN,QAAQ,iCAAiC,uBAAuB,CAAC,CACjE,QAAQ,mCAAmC,yBAAyB,IACnE,cAAc,KAAK,MAAM,KAAK,WAAW,CAC/C;GAEA,MAAM,QAAQ,KAAK,aACf,IAAI,OAAO,KAAK,GAAG,IACnB,IAAI,OAAO,GAAG;GAElB,OAAO,OAAO,MAAM,KAAK,KAAK;EAChC;CACF;CAEA,MAAM,aAAY,SAAQ,SAAS,QAAQ,SAAS;CAQpD,MAAM,WAAU,SAAQ;EACtB,IAAI,MAAM,KAAK;EAEf,OAAO,OAAO,UAAU,KAAK,MAAM,EAAE,GACnC;EAGF,MAAM,UAAU;EAEhB,OAAO,OAAO,KAAK,MAAM,OAAO,OAC9B;EAGF,IAAI,QAAQ,SACV,OAAO,KAAK,MAAM,GAAG,GAAG;EAG1B,IAAI,cAAc;EAElB,OAAO,cAAc,OAAO,KAAK,MAAM,cAAc,OAAO,QAC1D;EAGF,OAAO,cAAc,IACjB,KAAK,MAAM,GAAG,MAAM,CAAC,IAAI,QACzB,KAAK,MAAM,GAAG,GAAG;CACvB;CAEA,MAAM,cAAc,EAClB,SACA,QACC,eAAe;EAEhB,IAAI,OAAO,QAAQ,OAAO,MACtB,QAAQ,MAAM,CAAC,IACf;EAGJ,IAAI,KAAK,OAAO,KACd;EAGF,IAAI,WAAW;EAGf,IAAI,KAAK,OAAO,KAAK;GACnB,WAAW;GACX,OAAO,KAAK,MAAM,CAAC;EACrB;EAKA,MAAM,OAAO,KAAK,KAAK,SAAS;EAEhC,IAAI,SAAS,SAAS,SAAS,QAAQ,SAAS,MAC9C,OAAO,QAAQ,IAAI;EAOrB,IAAI,CAAC,MACH;EAGF,OAAO,KAGN,QAAQ,2CAA2C,GAAG,CAAC,CAGvD,QAAQ,oCAAoC,GAAG;EAEhD,MAAM,cAAc,gBAAgB,IAAI;EAExC,OAAO,IAAI,WACT,SACA,MACA,MACA,YACA,UACA,WACF;CACF;CAEA,IAAM,cAAN,MAAkB;EAChB,YAAa,YAAY;GACvB,KAAK,cAAc;GACnB,KAAK,SAAS,CAAC;GAef,KAAK,iBAAiB;EACxB;EAEA,KAAM,SAAS;GAEb,IAAI,WAAW,QAAQ,aAAa;IAClC,KAAK,SAAS,KAAK,OAAO,OAAO,QAAQ,OAAO,MAAM;IACtD,KAAK,kBAAkB,QAAQ,OAAO;IACtC,KAAK,SAAS;IACd;GACF;GAEA,IAAI,SAAS,OAAO,GAClB,UAAU,EACR,QACF;GAGF,MAAM,OAAO,aAAa,QAAQ,OAAO,KACpC,WAAW,SAAS,KAAK,WAAW;GAEzC,IAAI,MAAM;IACR,KAAK,SAAS;IACd,KAAK,OAAO,KAAK,IAAI;IAKrB,IAAI,gBAAgB,KAAK,IAAI,GAC3B,KAAK;GAET;EACF;EAGA,IAAK,SAAS;GACZ,KAAK,SAAS;GAEd,UACE,SAAS,OAAO,IACZ,aAAa,OAAO,IACpB,OACN,CAAC,CAAC,QAAQ,KAAK,MAAM,IAAI;GAEzB,IAAI,KAAK,QACP,KAAK,gBAAgB;GAGvB,OAAO,KAAK;EACd;EAKA,YAAa,MAAM;GACjB,MAAM,QAAQ,KAAK,kBACjB,KAAK,gBAAgB,KAAK,OAAO,QAC9B,EAAC,WAAU,KAAK,KAAK,UACpB,KAAK,KAAK,SAAS,OAAO,QAAQ,IAAI,QACjC,GACT;GAGF,IAAI,UAAU;GACd,IAAI,YAAY;GAChB,IAAI;GAEJ,KAAK,IAAI,QAAQ,MAAM,SAAS,GAAG,SAAS,GAAG,SAAU;IACvD,MAAM,OAAO,MAAM;IAEnB,IAAI,KAAK,WAAW,KAAK,IAAI,GAAG;KAC9B,UAAU,CAAC,KAAK;KAChB,YAAY,KAAK;KACjB,cAAc,KAAK,WACf,YACA;KACJ;IACF;GACF;GAEA,MAAM,MAAM;IACV;IACA;GACF;GAEA,IAAI,aACF,IAAI,OAAO;GAGb,OAAO;EACT;EAUA,KAAM,MAAM,gBAAgB,MAAM;GAChC,IAAI,UAAU;GACd,IAAI,YAAY;GAChB,IAAI;GAOJ,MAAM,QAAQ,KAAK;GACnB,MAAM,EAAC,WAAU;GAEjB,MAAM,WAAW,KAAK,iBAAiB,KAAK;GAE5C,MAAM,WAAW,WACb,WAAW,IAAI,IACf;GAMJ,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,SAAU;IAC5C,MAAM,OAAO,MAAM;IACnB,MAAM,EAAC,aAAY;IAgBnB,IAAI,EAHS,cAAc,YAAY,YAAY,aAC9C,YAAY,CAAC,WAAW,CAAC,aAAa,CAAC,mBAE/B,KAAK,KAAK,CAAC,KACtB,YAAY,KAAK,gBACb,WACA,IACN,GAAG;KACD,UAAU,CAAC;KACX,YAAY;KAEZ,cAAc,WACV,YACA;IACN;GACF;GAEA,MAAM,MAAM;IACV;IACA;GACF;GAEA,IAAI,aACF,IAAI,OAAO;GAGb,OAAO;EACT;CACF;CAEA,MAAM,cAAc,SAAS,SAAS;EACpC,MAAM,IAAI,KAAK,OAAO;CACxB;CAEA,MAAM,aAAa,MAAM,cAAc,YAAY;EACjD,IAAI,CAAC,SAAS,IAAI,GAChB,OAAO,QACL,oCAAoC,aAAa,KACjD,SACF;EAIF,IAAI,CAAC,MACH,OAAO,QAAQ,0BAA0B,SAAS;EAIpD,IAAI,UAAU,cAAc,IAAI,GAE9B,OAAO,QACL,0DAA0C,aAAa,IACvD,UACF;EAGF,OAAO;CACT;CASA,MAAM,iBAAgB,SAAQ;EAC5B,MAAM,QAAQ,KAAK,WAAW,CAAC;EAE/B,IAAI,UAAU,YACZ,OAAO;EAGT,IAAI,UAAU,UACZ,OAAO;EAIT,IAAI,KAAK,WAAW,GAClB,OAAO;EAGT,MAAM,SAAS,KAAK,WAAW,CAAC;EAGhC,IAAI,WAAW,YACb,OAAO;EAGT,IAAI,WAAW,UACb,OAAO;EAIT,OAAO,KAAK,WAAW,KAAK,KAAK,WAAW,CAAC,MAAM;CACrD;CAEA,UAAU,gBAAgB;;CAI1B,UAAU,WAAU,MAAK;CAGzB,IAAM,SAAN,MAAa;EACX,YAAa,EACX,aAAa,MACb,aAAa,YACb,qBAAqB,UACnB,CAAC,GAAG;GACN,OAAO,MAAM,YAAY,IAAI;GAE7B,KAAK,SAAS,IAAI,YAAY,UAAU;GACxC,KAAK,mBAAmB,CAAC;GACzB,KAAK,WAAW;EAClB;EAEA,aAAc;GAEZ,KAAK,eAAe,OAAO,OAAO,IAAI;GAGtC,KAAK,aAAa,OAAO,OAAO,IAAI;EACtC;EAEA,IAAK,SAAS;GACZ,IAAI,KAAK,OAAO,IAAI,OAAO,GAIzB,KAAK,WAAW;GAGlB,OAAO;EACT;EAGA,WAAY,SAAS;GACnB,OAAO,KAAK,IAAI,OAAO;EACzB;EAGA,MAAO,cAAc,OAAO,gBAAgB;GAC1C,MAAM,OAAO,gBAER,UAAU,QAAQ,YAAY;GAEnC,UACE,MACA,cACA,KAAK,mBACD,aACA,YACN;GAEA,OAAO,KAAK,GAAG,MAAM,OAAO,cAAc;EAC5C;EAEA,YAAa,MAAM;GAGjB,IAAI,KAAK,WAAW,KAAK,SAAS,CAAC,MAAM,YACvC,OAAO,KAAK,KAAK,IAAI;GAMvB,MAAM,MAAM,KAAK,GAAG,MAAM,KAAK,YAAY,IAAI;GAE/C,IAAI,IAAI,SACN,OAAO;GAGT,MAAM,UAAU,KAAK,OAAO,YAAY,IAAI;GAE5C,OAAO,QAAQ,WAAW,QAAQ,YAC9B,UACA;EACN;EAEA,GAEE,MAGA,OAGA,gBACA;GACA,IAAI,QAAQ,OACV,OAAO,MAAM;GAGf,MAAM,aAAa,SAAS,IAAI;GAGhC,MAAM,SAAS,aACX,KAAK,GAAG,YAAY,OAAO,cAAc,IACzC;GAEJ,OAAO,MAAM,QAAQ,UAAU,OAAO,UAGlC,SACA,KAAK,OAAO,KAAK,MAAM,gBAAgB,WAAW;EACxD;EAEA,QAAS,MAAM;GACb,OAAO,KAAK,MAAM,MAAM,KAAK,cAAc,KAAK,CAAC,CAAC;EACpD;EAEA,eAAgB;GACd,QAAO,SAAQ,CAAC,KAAK,QAAQ,IAAI;EACnC;EAEA,OAAQ,OAAO;GACb,OAAO,UAAU,KAAK,CAAC,CAAC,OAAO,KAAK,aAAa,CAAC;EACpD;EAGA,KAAM,MAAM;GACV,OAAO,KAAK,MAAM,MAAM,KAAK,YAAY,IAAI;EAC/C;CACF;CAEA,MAAM,WAAU,YAAW,IAAI,OAAO,OAAO;CAE7C,MAAM,eAAc,SAClB,UAAU,QAAQ,UAAU,QAAQ,IAAI,GAAG,MAAM,YAAY;;CAG/D,MAAM,qBAAqB;EAEzB,MAAM,aAAY,QAAO,YAAY,KAAK,GAAG,KAC1C,wBAAwB,KAAK,GAAG,IAC/B,MACA,IAAI,QAAQ,OAAO,GAAG;EAE1B,UAAU,UAAU;EAIpB,MAAM,mCAAmC;EACzC,UAAU,iBAAgB,SACxB,iCAAiC,KAAK,IAAI,KACvC,cAAc,IAAI;CACzB;;CAMA,IAEE,OAAO,YAAY,eAChB,QAAQ,aAAa,SAExB,aAAa;CAKf,OAAO,UAAU;CAKjB,QAAQ,UAAU;CAElB,OAAO,QAAQ,cAAc;CAG7B,OAAO,OAAO,SAAS,OAAO,IAAI,cAAc,GAAG,YAAY;;;;;;;;;;;;;;;;;;;;;;;;ACjgD/D,MAAa,cAAmD,EAC9D,SAAS,OAAO,KAAkB;CAChC,MAAM,MAAM,SAAS,IAAI,KAAK,KAAK;CAEnC,IAAI,gBAAgB,GAAG,GACrB,OAAO;EAAE,QAAQ,GAAG,IAAI;EAAmB,SAAS;CAAO;CAG7D,MAAM,KAAK,iBAAiB,KAAK,IAAI,UAAU;CAC/C,MAAM,cAAc,OAAO,KAAA;CAI3B,IAAI,eAAe,QAAQ,IAAI,OAAO,EAAE,SAAS,QAAQ;CAMzD,MAAM,OAAO,MAAM;CACnB,MAAM,MAAM,SAAS,MAAM,GAAG;CAE9B,MAAM,OAAO,IAAI,UAAU,SAAS,YAAY;CAEhD,MAAM,OAAOA,cAAY,MAAM,KAAK,IAAI,KAAK;CAC7C,IAAI,MAAM,WAAW,QAAQ,OAAO;EAAE,QAAQ,GAAG,IAAI;EAAmB,SAAS;CAAO;CACxF,IAAI,MAAM,WAAW,OACnB,OAAO;EACL,KAAK,SAAS,KAAK,GAAG,WAAW,GAAG,EAAE;EACtC,QAAQ,GAAG,IAAI;EACf,aAAa,CAAC;GAAE,MAAM;GAAQ,SAAS,IAAI;GAAO,OAAO,IAAI;EAAM,CAAC;EACpE,SAAS;CACX;CAEF,IAAI,MAAM,WAAW,SAAS,OAAO,EAAE,SAAS,QAAQ;CAQxD,IAAI,CAAC,aAAa;EAChB,MAAM,SAAS,QAAQ,GAAG;EAC1B,OAAO;GACL,KAAK,SAAS,KAAK,GAAG,WAAW,GAAG,EAAE;GACtC,QAAQ,GAAG,IAAI;GACf,aAAa,CACX;IAAE,aAAa,OAAO,OAAO;IAAgB,MAAM;IAAa,MAAM;GAAO,CAC/E;GACA,SAAS;EACX;CACF;CACA,IAAI,IAAI,UAAU,QAAQ,OAAO,EAAE,SAAS,QAAQ;CACpD,OAAO;EACL,KAAK,SAAS,KAAK,GAAG,WAAW,GAAG,EAAE;EACtC,QAAQ,GAAG,IAAI;EACf,aAAa,CAAC;GAAE,MAAM;GAAQ,SAAS,IAAI;GAAO,OAAO;EAAQ,CAAC;EAClE,SAAS;CACX;AACF,EACF;;;;AAKA,SAAgB,gBAAgB,MAAuB;CACrD,OAAO,mBAAmB,MAAM,OAAO,GAAG,KAAK,IAAI,CAAC;AACtD;;;;AAKA,MAAM,qBAA+B;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;AAKA,SAAS,iBAAiB,KAAa,YAAmD;CACxF,IAAI;CACJ,KAAK,MAAM,QAAQ,YACjB,IAAI,QAAQ,QAAQ,IAAI,WAAW,KAAK,SAAS,GAAG,IAAI,OAAO,GAAG,KAAK,EAAE,GACnE;MAAA,SAAS,KAAA,KAAa,KAAK,SAAS,KAAK,QAAQ,OAAO;CAAA;CAGhE,OAAO;AACT;;;;;AAMA,SAASA,cACP,WACA,KACA,OACoC;CACpC,IAAI,QAAQ,IAAI,OAAO,KAAA;CACvB,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,iBAAiB,KAAK,SAAS,SAAS;EACrD,IAAI,SAAS,KAAA,GAAW;EACxB,IAAI;GACF,KAAA,GAAIC,cAAAA,QAAAA,CAAO,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,QAAQ,GAAG,GAAG,OAAO;EAC9C,QAAQ,CAER;CACF;AACF;AAEA,SAAS,iBAAiB,SAAiB,WAAuC;CAChF,IAAI,QAAQ,WAAW,IAAI,GACzB,OAAO,sBAAsB,QAAQ,MAAM,CAAC,GAAG,SAAS;CAE1D,IAAI,QAAQ,WAAW,IAAI,GACzB,OAAO,sBAAsB,SAAS,OAAO,GAAG,SAAS;CAG3D,OAAO;AACT;AAEA,SAAS,sBAAsB,YAAoB,WAAuC;CACxF,MAAM,MAAM,SAAS,WAAW,UAAU;CAC1C,IAAI,QAAQ,MAAM,IAAI,WAAW,IAAI,KAAK,WAAW,GAAG,GAAG,OAAO,KAAA;CAClE,OAAO,IAAI;AACb;;;;;;;;;;;;;;;;;;;;;;AC5IA,MAAa,cAAyC,EACpD,SAAS,OAAO,KAAkB;CAChC,MAAM,OAAO,YAAY,OAAO,IAAI,KAAK;CACzC,IAAI,MAAM,WAAW,QACnB,OAAO;EAAE,QAAQ,QAAQ,MAAM;EAAmB,SAAS;CAAO;CAEpE,IAAI,MAAM,WAAW,OACnB,OAAO;EACL,KAAK,cAAc,MAAM;EACzB,QAAQ,QAAQ,MAAM;EACtB,aAAa,CAAC;GAAE,MAAM;GAAQ,SAAS;GAAO,OAAO;EAAO,CAAC;EAC7D,SAAS;CACX;CAEF,OAAO,EAAE,SAAS,QAAQ;AAC5B,EACF;AAEA,SAAS,YAAY,OAAe,OAA0D;CAC5F,KAAK,MAAM,QAAQ,OACjB,KAAA,GAAIC,cAAAA,QAAAA,CAAO,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,QAAQ,KAAK,GAAG,OAAO;AAE1D;ACQA,MAAa,kBAAkB,eAA8B,OAAO,CAAC,CAAC,KAAK;CA3CzE,YAAY;CACZ,YAAY;CACZ,YAAY;CACZ,aAAa;AAwC4D,CAAO"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"search-CRw247ES.mjs","names":[],"sources":["../src/tools/search.ts"],"sourcesContent":["import type { MetaPart, TextPart } from \"@zaly/ai\"\n\nimport { defineTool, AiError } from \"@zaly/ai\"\nimport { Type } from \"typebox\"\n\n/**\n * Web search via Brave's LLM Context API.\n *\n * Returns extracted snippets pre-chunked from each source page rather\n * than just titles + URLs, so the model gets grounding content in one\n * call without a follow-up `fetch`. Each result lands as one TextPart\n * (snippets joined per-source) preceded by a `<source>` MetaPart with\n * URL / title / age metadata.\n *\n * Authentication: reads `BRAVE_API_KEY` from the environment. Tools\n * don't get config-injected today, so the env var is the simplest\n * surface — same convention `fetch`-style tools use elsewhere.\n *\n * Endpoint: `https://api.search.brave.com/res/v1/llm/context`. Docs:\n * https://api.search.brave.com/app/documentation/llm-context/get-started\n */\n\nconst ENDPOINT = \"https://api.search.brave.com/res/v1/llm/context\"\n\ninterface BraveSourceMeta {\n title?: string\n hostname?: string\n age?: string[] | null\n}\n\ninterface BraveResult {\n url: string\n title?: string\n snippets?: string[]\n}\n\ninterface BraveResponse {\n grounding?: {\n generic?: BraveResult[]\n }\n sources?: Record<string, BraveSourceMeta>\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const searchTool = defineTool({\n name: \"search\",\n desc:\n \"Web search optimised for agents. Returns extracted snippets from each \" +\n \"source page (not just titles + URLs), so you get grounding content in \" +\n \"one call. Use for fact-finding, current events, documentation lookups, \" +\n \"and anything beyond your knowledge cutoff.\",\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n query: Type.String({\n description: \"The search query. 1–400 characters, max 50 words.\",\n maxLength: 400,\n minLength: 1,\n }),\n count: Type.Optional(\n Type.Integer({\n default: 10,\n description: \"Maximum number of source URLs in the response.\",\n maximum: 50,\n minimum: 1,\n })\n ),\n freshness: Type.Optional(\n Type.Union([Type.Literal(\"pd\"), Type.Literal(\"pw\"), Type.Literal(\"pm\"), Type.Literal(\"py\")], {\n description:\n \"Restrict to recent pages. `pd` = past 24h, `pw` = past week, \" +\n \"`pm` = past month, `py` = past year. Omit for no filter.\",\n })\n ),\n country: Type.Optional(\n Type.String({\n default: \"us\",\n description: \"Two-letter country code for region-specific results.\",\n })\n ),\n }),\n\n async call(args): Promise<(MetaPart | TextPart)[]> {\n const apiKey = process.env.BRAVE_API_KEY\n if (!apiKey || apiKey === \"\") {\n throw new AiError({\n code: \"MISSING_API_KEY\",\n message:\n \"search requires BRAVE_API_KEY in the environment. Get a key at \" +\n \"https://api.search.brave.com/.\",\n })\n }\n\n const url = new URL(ENDPOINT)\n url.searchParams.set(\"q\", args.query)\n url.searchParams.set(\"count\", String(args.count ?? 10))\n url.searchParams.set(\"country\", args.country ?? \"us\")\n if (args.freshness !== undefined) url.searchParams.set(\"freshness\", args.freshness)\n\n const t0 = Date.now()\n const res = await fetch(url, {\n headers: {\n accept: \"application/json\",\n \"x-subscription-token\": apiKey,\n },\n })\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\")\n throw new AiError({\n code: \"SEARCH_FAILED\",\n data: { status: res.status, statusText: res.statusText },\n message: `Brave search failed (${res.status} ${res.statusText}): ${body.slice(0, 200)}`,\n retryable: res.status >= 500 || res.status === 429,\n })\n }\n\n const json = (await res.json()) as BraveResponse\n // console.log(json)\n const results = json.grounding?.generic ?? []\n const sources = json.sources ?? {}\n const durationMs = Date.now() - t0\n\n const parts: (MetaPart | TextPart)[] = [\n {\n data: { count: results.length, durationMs, query: args.query },\n tag: \"search\",\n type: \"meta\",\n },\n ]\n\n if (results.length === 0) {\n parts.push({ text: \"No results found.\", type: \"text\" })\n return parts\n }\n\n for (const r of results) {\n const meta = sources[r.url] ?? {}\n const header: Record<string, unknown> = { url: r.url }\n // `age` arrives as a 3-tuple [pretty, ISO, relative]; keep the\n // ISO form (index 1) when available — most useful for the model.\n if (meta.age && meta.age.length > 0) header.age = meta.age[1] ?? meta.age[0]\n\n parts.push({ data: header, tag: \"source\", type: \"meta\" })\n // Title as a markdown H1 right after the `<source>` tag — the\n // pair reads like a document section. Snippets flow underneath.\n const title = r.title ?? meta.title\n const body: string[] = []\n if (title) body.push(`# ${title}`)\n const snippets = r.snippets ?? []\n if (snippets.length > 0) body.push(snippets.join(\"\\n\\n\"))\n if (body.length > 0) parts.push({ text: body.join(\"\\n\\n\"), type: \"text\" })\n }\n\n return parts\n },\n})\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"session-Ds5nJz5A.d.mts","names":[],"sources":["../src/session/types.ts","../src/session/store.ts","../src/session/jsonl.ts","../src/session/memory.ts","../src/session/session.ts"],"mappings":";;;;;;;;;;KAYK;EACH;;EAEA;;EAEA;;EAEI;;EACA;;EACA;EAA0B,UAAU;;EACpC;EAAiB,SAAS;;EAC1B;EAAyB;EAAmB;;EAE5C;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA,SAAS;;KAGV,kBAAkB;KAEX;EAAmB;EAAmB;;KAEtC,iBAAiB;EAAY;EAAY;;KAEzC,YAAY,UAAU,kBAAkB,mBAAmB,QACrE;EACE,MAAM;;;;;KAME,kBAAkB;EAAgB,UAAU;;KAE5C;EACV,UAAU;EACV,OAAO,YAAY;EACnB,UAAU;EACV,iBAAiB;;KAGP;EACV;EACA;EACA;EACA;EACA;EACA,YAAY;;KAGF,gBAAgB,KAAK;KAE5B,iBAAiB,GAAG,UAAU,eAAe,oBAAoB,KAAK,GAAG;KAElE,cAAc,iBAAiB;;;KAM/B;EACV;IAAQ,MAAM;;EACd;IAAY;IAA0B,mBAAmB;;EACzD;IAAW,MAAM;;EAEjB;IAAO;IAAa;;EACpB;IAAS;IAAe;;EACxB;IAAa,QAAQ;IAAiB,OAAO;;EAE7C;EACA;;KAKU,eAAe,UAAU,eAAe;EAClD,QAAQ;;;;;EAKR;;;;;EAKA;;;EAGA,WAAW,KAAK;;KAGN,YAAY,UAAU,eAAe,gBAAgB;;;EAG/D,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;UC7FQ;;;WAGN,MAAM;;EAGf,MAAM,eAAe,QAAQ;;;;EAK7B,QAAQ,MAAM,gBAAgB;;EAG9B,cAAc;;;;EAKd,YAAY,SAAS,eAAe,cAAc;;;;;;;;;;;;;;;;cCvBvC,sBAAsB;;WACxB;EAKT,YAAY;;;;SAQC,KAAK,eAAe,QAAQ;MAmBrC,QAAQ;EAIN,IAAI,aAAa,QAAQ;EAIzB,MAAM,MAAM,cAAc;EAS1B,SAAS;EAIR,OAAO,cAAc;;cAMjB,YAAY;;WAMF;EAArB,YAAqB;UAsBN;EAgCT,QAAQ,QAAQ;EAMhB,SAAK;;;;;;;;;cC3IA,uBAAuB;;EAIlC,YAAY,QAAQ,SAAS;MAUzB,QAAQ;EAIN,IAAI,aAAa,QAAQ;EAIzB,MAAM,MAAM,cAAc;EAK/B,OAAO,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCoBN,QAAQ,UAAU,eAAe,sBAAsB,QAAQ;;YAYjE,YAAa,MAAM,YAAY;;;;;;;SAe3B,KAAK,UAAU,eAAe,cACzC,MAAM,eAAe;IAAO,OAAO;MAClC,QAAQ,QAAQ;SACN,KAAK,MAAM;IAAmB;MAAiB,QAAQ,QAAQ;SAC/D,KAAK,OAAO,sBAAsB,QAAQ,QAAQ;iBAO1C;;;SAUR,YAAY,OAAM,iBAAsB,QAAQ;;;;MA4BzD,qBAAqB;MAIrB,kBAAkB;MAIlB;MAIA;MAIA;MAIA;;MAKA,YAAY;EAIV,OAAO,UAAU,eAAe;IAAS;MAAoB;;;;;EAe7D,MAAM,WAAW,gBAAgB;;;;;EAcjC,KAAK,cAAc,UAAU,SAAS;IAAoB;;;;MAS5D;;;EAMJ,SAAS,SAAS,eAAe,cAAc;MAI3C,QAAQ;;;;;;;;;;EAeN,IAAI,SAAS,UAAU;;;;;;;EAavB,QAAQ;IACZ;IACA;IACA;IACA;IACA,SAAS;MACP;EAmBE,kBAAkB,MAAM,iBAAiB;EAKzC,SAAS,eAAe;;;;;;;;EAaxB,QAAQ,iBAAiB,iBAAiB;;EAM1C,SAAS"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"write-DwGiDNeG.mjs","names":[],"sources":["../src/tools/write.ts"],"sourcesContent":["import type { ToolContext } from \"@zaly/ai\"\nimport type { FileMeta } from \"../utils/file.ts\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath, safeReadFile } from \"@zaly/shared\"\nimport { detectEol, normalizeEol } from \"@zaly/shared/text\"\nimport { mkdir, stat, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"pathe\"\nimport { Type } from \"typebox\"\n\nexport type WriteTool = typeof writeTool\n\nexport type WriteToolMeta = FileMeta & {\n /** File content before the write. Undefined for new files. The\n * post-write content is recoverable from the original call's\n * `params.content`, so we don't redundantly stash it on meta. */\n original?: string\n}\n\n/**\n * Write a file to disk. Overwrites if the file exists; creates parent\n * directories as needed.\n *\n * Content is written *verbatim* in everything but line endings. The\n * model always emits LF; this tool detects the existing file's\n * line-ending style (or the platform default for new files via\n * `detectEol`) and re-applies it on write so a CRLF file stays CRLF\n * after an LF-from-the-model overwrite. No trailing-newline injection,\n * no whitespace stripping.\n *\n * Returns a small acknowledgement (`{ ok, bytes, lines }`) rather than\n * echoing the content back — the model already has it. `bytes` reflects\n * the on-disk byte count, including any extra bytes from CRLF expansion.\n */\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const writeTool = defineTool({\n name: \"write\",\n desc:\n \"Write content to a file. Overwrites existing files; \" +\n \"creates parent directories as needed. Returns the byte count and line \" +\n \"count of the written file.\",\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n path: Type.String({ description: \"Path to the file. Absolute or cwd-relative.\" }),\n content: Type.String({ description: \"File contents to write, verbatim.\" }),\n }),\n\n async preflight(args, ctx: ToolContext<WriteToolMeta>) {\n const path = normPath(ctx.cwd, args.path)\n await ctx.need?.(\"write\", path)\n },\n\n async call(\n args,\n ctx: ToolContext<WriteToolMeta>\n ): Promise<{ ok: true; path: string; bytes: number; lines: number }> {\n const path = normPath(ctx.cwd, args.path)\n\n let text: string\n let original: string | undefined\n try {\n original = await safeReadFile(path)\n const eol = detectEol({ path, text: original })\n text = normalizeEol(args.content, { eol })\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, text, \"utf8\")\n } catch (error) {\n throw new AiError({\n cause: error,\n code: \"WRITE_FAILED\",\n message: `failed to write ${path}: ${error instanceof Error ? error.message : String(error)}`,\n })\n }\n\n // Record the new mtime for unchanged-read detection and mark the\n // full-file replacement for context masking.\n const fstat = await stat(path)\n ctx.meta = { full: true, kind: \"write\", mtime: fstat.mtimeMs, original, path }\n\n const bytes = Buffer.byteLength(text, \"utf8\")\n const lines = countLines(text)\n return { bytes, lines, ok: true, path }\n },\n})\n\n/** Count lines the way an editor would — non-empty trailing line counts;\n * trailing newline doesn't add a phantom blank line. */\nfunction countLines(content: string): number {\n if (content === \"\") return 0\n let n = 1\n for (const c of content) if (c === \"\\n\") n++\n // A trailing newline means the count is one too high (no real line follows).\n if (content.endsWith(\"\\n\")) n--\n return n\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,YAAY,WAAW;CAClC,MAAM;CACN,MACE;CAIF,QAAQ,KAAK,OAAO;EAClB,MAAM,KAAK,OAAO,EAAE,aAAa,8CAA8C,CAAC;EAChF,SAAS,KAAK,OAAO,EAAE,aAAa,oCAAoC,CAAC;CAC3E,CAAC;CAED,MAAM,UAAU,MAAM,KAAiC;EACrD,MAAM,OAAO,SAAS,IAAI,KAAK,KAAK,IAAI;EACxC,MAAM,IAAI,OAAO,SAAS,IAAI;CAChC;CAEA,MAAM,KACJ,MACA,KACmE;EACnE,MAAM,OAAO,SAAS,IAAI,KAAK,KAAK,IAAI;EAExC,IAAI;EACJ,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,aAAa,IAAI;GAClC,MAAM,MAAM,UAAU;IAAE;IAAM,MAAM;GAAS,CAAC;GAC9C,OAAO,aAAa,KAAK,SAAS,EAAE,IAAI,CAAC;GACzC,MAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;GAC9C,MAAM,UAAU,MAAM,MAAM,MAAM;EACpC,SAAS,OAAO;GACd,MAAM,IAAI,QAAQ;IAChB,OAAO;IACP,MAAM;IACN,SAAS,mBAAmB,KAAK,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC5F,CAAC;EACH;EAKA,IAAI,OAAO;GAAE,MAAM;GAAM,MAAM;GAAS,QAAO,MAD3B,KAAK,IAAI,EAAA,CACwB;GAAS;GAAU;EAAK;EAI7E,OAAO;GAAE,OAFK,OAAO,WAAW,MAAM,MAEzB;GAAG,OADF,WAAW,IACL;GAAG,IAAI;GAAM;EAAK;CACxC;AACF,CAAC;;;AAID,SAAS,WAAW,SAAyB;CAC3C,IAAI,YAAY,IAAI,OAAO;CAC3B,IAAI,IAAI;CACR,KAAK,MAAM,KAAK,SAAS,IAAI,MAAM,MAAM;CAEzC,IAAI,QAAQ,SAAS,IAAI,GAAG;CAC5B,OAAO;AACT"}