@warlock.js/ai 4.5.0 → 4.6.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 (159) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/cjs/index.cjs +20 -1
  3. package/cjs/{src-DFibP2FQ.cjs → src-Bmajk4Qg.cjs} +1 -1
  4. package/cjs/{src-C02yzsLs.cjs → src-OZyDYHxm.cjs} +2789 -691
  5. package/cjs/src-OZyDYHxm.cjs.map +1 -0
  6. package/esm/agent/agent-config.type.d.mts +29 -0
  7. package/esm/agent/agent-config.type.d.mts.map +1 -1
  8. package/esm/agent/agent.d.mts.map +1 -1
  9. package/esm/agent/agent.mjs +126 -7
  10. package/esm/agent/agent.mjs.map +1 -1
  11. package/esm/agent/signature.mjs +57 -0
  12. package/esm/agent/signature.mjs.map +1 -0
  13. package/esm/agent/snapshot.mjs +101 -0
  14. package/esm/agent/snapshot.mjs.map +1 -0
  15. package/esm/ai-openai/src/image.mjs +5 -0
  16. package/esm/ai-openai/src/index.mjs +3 -0
  17. package/esm/ai-openai/src/sdk.mjs +3 -0
  18. package/esm/ai-openai/src/speech.mjs +5 -0
  19. package/esm/ai-openai/src/transcription.mjs +6 -0
  20. package/esm/ai-openai/src/utils/index.mjs +1 -0
  21. package/esm/ai-openai/src/utils/to-openai-messages.mjs +3 -0
  22. package/esm/ai.d.mts +45 -0
  23. package/esm/ai.d.mts.map +1 -1
  24. package/esm/ai.mjs +37 -1
  25. package/esm/ai.mjs.map +1 -1
  26. package/esm/contracts/agent/agent-options.type.d.mts +22 -2
  27. package/esm/contracts/agent/agent-options.type.d.mts.map +1 -1
  28. package/esm/contracts/agent/agent-snapshot.type.d.mts +90 -0
  29. package/esm/contracts/agent/agent-snapshot.type.d.mts.map +1 -0
  30. package/esm/contracts/agent/agent.contract.d.mts +29 -1
  31. package/esm/contracts/agent/agent.contract.d.mts.map +1 -1
  32. package/esm/contracts/agent/index.d.mts +2 -1
  33. package/esm/contracts/image-model.contract.d.mts +156 -0
  34. package/esm/contracts/image-model.contract.d.mts.map +1 -0
  35. package/esm/contracts/index.d.mts +8 -3
  36. package/esm/contracts/planner/index.d.mts +3 -2
  37. package/esm/contracts/planner/planner-config.type.d.mts +30 -0
  38. package/esm/contracts/planner/planner-config.type.d.mts.map +1 -1
  39. package/esm/contracts/planner/planner-execute-options.type.d.mts +13 -1
  40. package/esm/contracts/planner/planner-execute-options.type.d.mts.map +1 -1
  41. package/esm/contracts/planner/planner-snapshot.type.d.mts +77 -0
  42. package/esm/contracts/planner/planner-snapshot.type.d.mts.map +1 -0
  43. package/esm/contracts/planner/planner.contract.d.mts +21 -1
  44. package/esm/contracts/planner/planner.contract.d.mts.map +1 -1
  45. package/esm/contracts/result/base-report.type.d.mts +1 -1
  46. package/esm/contracts/result/base-report.type.d.mts.map +1 -1
  47. package/esm/contracts/result/base-report.type.mjs.map +1 -1
  48. package/esm/contracts/sdk-adapter.contract.d.mts +37 -0
  49. package/esm/contracts/sdk-adapter.contract.d.mts.map +1 -1
  50. package/esm/contracts/speech-model.contract.d.mts +97 -0
  51. package/esm/contracts/speech-model.contract.d.mts.map +1 -0
  52. package/esm/contracts/transcription-model.contract.d.mts +101 -0
  53. package/esm/contracts/transcription-model.contract.d.mts.map +1 -0
  54. package/esm/errors/agent-drift-error.d.mts +32 -0
  55. package/esm/errors/agent-drift-error.d.mts.map +1 -0
  56. package/esm/errors/agent-drift-error.mjs +31 -0
  57. package/esm/errors/agent-drift-error.mjs.map +1 -0
  58. package/esm/errors/error-code.type.d.mts +1 -1
  59. package/esm/errors/index.d.mts +2 -0
  60. package/esm/errors/index.mjs +2 -0
  61. package/esm/errors/planner-drift-error.d.mts +34 -0
  62. package/esm/errors/planner-drift-error.d.mts.map +1 -0
  63. package/esm/errors/planner-drift-error.mjs +33 -0
  64. package/esm/errors/planner-drift-error.mjs.map +1 -0
  65. package/esm/image/image-cost.d.mts +32 -0
  66. package/esm/image/image-cost.d.mts.map +1 -0
  67. package/esm/image/image-cost.mjs +55 -0
  68. package/esm/image/image-cost.mjs.map +1 -0
  69. package/esm/image/image.d.mts +92 -0
  70. package/esm/image/image.d.mts.map +1 -0
  71. package/esm/image/image.mjs +113 -0
  72. package/esm/image/image.mjs.map +1 -0
  73. package/esm/image/index.mjs +4 -0
  74. package/esm/index.d.mts +26 -4
  75. package/esm/index.mjs +20 -1
  76. package/esm/mock/index.d.mts +3 -0
  77. package/esm/mock/index.mjs +3 -0
  78. package/esm/mock/mock-config.type.d.mts +22 -0
  79. package/esm/mock/mock-config.type.d.mts.map +1 -1
  80. package/esm/mock/mock-image-model.d.mts +41 -0
  81. package/esm/mock/mock-image-model.d.mts.map +1 -0
  82. package/esm/mock/mock-image-model.mjs +52 -0
  83. package/esm/mock/mock-image-model.mjs.map +1 -0
  84. package/esm/mock/mock-sdk.d.mts +7 -1
  85. package/esm/mock/mock-sdk.d.mts.map +1 -1
  86. package/esm/mock/mock-sdk.mjs +27 -0
  87. package/esm/mock/mock-sdk.mjs.map +1 -1
  88. package/esm/mock/mock-speech-model.d.mts +31 -0
  89. package/esm/mock/mock-speech-model.d.mts.map +1 -0
  90. package/esm/mock/mock-speech-model.mjs +39 -0
  91. package/esm/mock/mock-speech-model.mjs.map +1 -0
  92. package/esm/mock/mock-transcription-model.d.mts +32 -0
  93. package/esm/mock/mock-transcription-model.d.mts.map +1 -0
  94. package/esm/mock/mock-transcription-model.mjs +36 -0
  95. package/esm/mock/mock-transcription-model.mjs.map +1 -0
  96. package/esm/planner/planner-run.d.mts +8 -0
  97. package/esm/planner/planner-run.d.mts.map +1 -1
  98. package/esm/planner/planner-run.mjs +161 -6
  99. package/esm/planner/planner-run.mjs.map +1 -1
  100. package/esm/planner/planner.d.mts.map +1 -1
  101. package/esm/planner/planner.mjs +25 -1
  102. package/esm/planner/planner.mjs.map +1 -1
  103. package/esm/planner/snapshot.mjs +95 -0
  104. package/esm/planner/snapshot.mjs.map +1 -0
  105. package/esm/rag/index.d.mts +7 -0
  106. package/esm/rag/index.mjs +7 -0
  107. package/esm/rag/loaders/errors.d.mts +19 -0
  108. package/esm/rag/loaders/errors.d.mts.map +1 -0
  109. package/esm/rag/loaders/errors.mjs +25 -0
  110. package/esm/rag/loaders/errors.mjs.map +1 -0
  111. package/esm/rag/loaders/index.mjs +7 -0
  112. package/esm/rag/loaders/load-html.d.mts +26 -0
  113. package/esm/rag/loaders/load-html.d.mts.map +1 -0
  114. package/esm/rag/loaders/load-html.mjs +138 -0
  115. package/esm/rag/loaders/load-html.mjs.map +1 -0
  116. package/esm/rag/loaders/load-pdf.d.mts +38 -0
  117. package/esm/rag/loaders/load-pdf.d.mts.map +1 -0
  118. package/esm/rag/loaders/load-pdf.mjs +150 -0
  119. package/esm/rag/loaders/load-pdf.mjs.map +1 -0
  120. package/esm/rag/loaders/load-text.d.mts +47 -0
  121. package/esm/rag/loaders/load-text.d.mts.map +1 -0
  122. package/esm/rag/loaders/load-text.mjs +60 -0
  123. package/esm/rag/loaders/load-text.mjs.map +1 -0
  124. package/esm/rag/loaders/load-web.d.mts +42 -0
  125. package/esm/rag/loaders/load-web.d.mts.map +1 -0
  126. package/esm/rag/loaders/load-web.mjs +89 -0
  127. package/esm/rag/loaders/load-web.mjs.map +1 -0
  128. package/esm/rag/loaders/loader.type.d.mts +89 -0
  129. package/esm/rag/loaders/loader.type.d.mts.map +1 -0
  130. package/esm/rag/store/pg-vector-store.d.mts +139 -0
  131. package/esm/rag/store/pg-vector-store.d.mts.map +1 -0
  132. package/esm/rag/store/pg-vector-store.mjs +328 -0
  133. package/esm/rag/store/pg-vector-store.mjs.map +1 -0
  134. package/esm/speech/index.mjs +3 -0
  135. package/esm/speech/speech.d.mts +65 -0
  136. package/esm/speech/speech.d.mts.map +1 -0
  137. package/esm/speech/speech.mjs +123 -0
  138. package/esm/speech/speech.mjs.map +1 -0
  139. package/esm/supervisor/entries.mjs +2 -2
  140. package/esm/supervisor/entries.mjs.map +1 -1
  141. package/esm/transcribe/audio-input.d.mts +47 -0
  142. package/esm/transcribe/audio-input.d.mts.map +1 -0
  143. package/esm/transcribe/audio-input.mjs +84 -0
  144. package/esm/transcribe/audio-input.mjs.map +1 -0
  145. package/esm/transcribe/index.mjs +4 -0
  146. package/esm/transcribe/transcribe.d.mts +64 -0
  147. package/esm/transcribe/transcribe.d.mts.map +1 -0
  148. package/esm/transcribe/transcribe.mjs +128 -0
  149. package/esm/transcribe/transcribe.mjs.map +1 -0
  150. package/llms-full.txt +753 -0
  151. package/llms.txt +5 -0
  152. package/package.json +3 -3
  153. package/skills/README.md +4 -0
  154. package/skills/durable-agent-runs/SKILL.md +135 -0
  155. package/skills/generate-images/SKILL.md +138 -0
  156. package/skills/generate-speech/SKILL.md +139 -0
  157. package/skills/rag-loaders-and-stores/SKILL.md +164 -0
  158. package/skills/transcribe-audio/SKILL.md +157 -0
  159. package/cjs/src-C02yzsLs.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"planner.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai/src/planner/planner.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { agent } from \"../agent/agent\";\nimport type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { PlannerCapability } from \"../contracts/planner/planner-capability.type\";\nimport type { PlannerConfig } from \"../contracts/planner/planner-config.type\";\nimport type { PlannerExecuteOptions } from \"../contracts/planner/planner-execute-options.type\";\nimport type { PlannerResult } from \"../contracts/planner/planner-result.type\";\nimport type { PlannerContract } from \"../contracts/planner/planner.contract\";\nimport { PlannerFailedError } from \"../errors\";\nimport { buildPlanSystemPrompt } from \"./plan-prompt\";\nimport { PlannerRun } from \"./planner-run\";\nimport { computeSignature } from \"./signature\";\n\nconst LOG_MODULE = \"ai.planner\";\n\n/**\n * `ai.planner(config)` — construct a {@link PlannerContract}.\n *\n * Validates the config at author time (throws {@link PlannerFailedError}\n * on a bad shape), builds (or adopts) the plan-generation agent, computes\n * a stable structural signature, and returns an instance satisfying\n * `ExecutableContract` so the planner composes into supervisors,\n * orchestrators, and outer agents through the same uniform surface.\n *\n * At `execute(goal)` the planner asks its LLM for an ordered plan over\n * the registered `capabilities`, then executes that plan step-by-step\n * through each capability's own `execute()` — reusing the existing\n * executable machinery rather than forking it — and returns the unified\n * `{ data, report, usage, error }` envelope with `report.type ===\n * \"planner\"`.\n *\n * @example\n * const research = ai.planner({\n * name: \"research-assistant\",\n * model: ai.openai.model({ name: \"gpt-4o\" }),\n * capabilities: [\n * { name: \"search\", description: \"Search the web\", executable: searchAgent },\n * { name: \"write\", description: \"Draft a summary\", executable: writerAgent },\n * ],\n * maxSteps: 6,\n * });\n *\n * const { data, report } = await research.execute(\"Compare React vs Vue in 2026\");\n */\nexport function planner<TOutput = unknown>(\n config: PlannerConfig<TOutput>,\n): PlannerContract<TOutput> {\n validateConfig(config);\n\n const maxSteps = config.maxSteps ?? 10;\n const capabilities = new Map<string, PlannerCapability>();\n\n for (const capability of config.capabilities) {\n capabilities.set(capability.name, capability);\n }\n\n const signature = computeSignature(config.name, config.capabilities);\n const planningAgent = resolvePlanningAgent(config, maxSteps);\n\n async function execute(\n goal: string,\n options?: PlannerExecuteOptions<TOutput>,\n ): Promise<PlannerResult<TOutput>> {\n log.debug(LOG_MODULE, \"execute\", \"Planner run starting\", {\n name: config.name,\n capabilities: capabilities.size,\n });\n\n return new PlannerRun<TOutput>({\n config,\n capabilities,\n maxSteps,\n signature,\n planningAgent,\n goal,\n options,\n }).run();\n }\n\n return {\n name: config.name,\n signature,\n execute,\n };\n}\n\n/**\n * Resolve the plan-generation agent: either adopt the dev's `planner`\n * agent, or build an internal one from `model` with the generated\n * plan-system-prompt baked on. The plan output schema is supplied\n * per-call in {@link PlannerRun}, so it isn't baked here.\n *\n * **`maxSteps` and BYO planners.** In `model` mode the cap is woven\n * into the generated plan-system-prompt *and* the per-call plan schema\n * (`steps.maxItems`). In `planner` (BYO) mode the dev owns the prompt,\n * so the cap is communicated only through that same per-call schema —\n * and, regardless of mode, {@link PlannerRun} truncates any over-long\n * plan to `skipped` at execution time, so the cap is always enforced.\n */\nfunction resolvePlanningAgent<TOutput>(\n config: PlannerConfig<TOutput>,\n maxSteps: number,\n): AgentContract<unknown> {\n if (config.planner) {\n return config.planner;\n }\n\n const systemPrompt = buildPlanSystemPrompt(\n config.capabilities,\n maxSteps,\n config.systemPrompt,\n config.dag === true,\n );\n\n return agent({\n name: `${config.name}-planner`,\n description: \"Generates an ordered execution plan over the planner's capabilities.\",\n model: config.model!,\n systemPrompt,\n maxTrips: 1,\n });\n}\n\n/**\n * Factory-time validation. Surfaces every violation as a typed\n * {@link PlannerFailedError} tagged `authoring: true`, mirroring the\n * supervisor/orchestrator authoring-error convention.\n */\nfunction validateConfig<TOutput>(config: PlannerConfig<TOutput>): void {\n if (!config.name || typeof config.name !== \"string\") {\n throw new PlannerFailedError(\"ai.planner: `name` is required and must be a string\", {\n context: { authoring: true },\n });\n }\n\n const hasModel = config.model !== undefined;\n const hasPlanner = config.planner !== undefined;\n\n if (!hasModel && !hasPlanner) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): one of \\`model\\` or \\`planner\\` is required`,\n { context: { authoring: true } },\n );\n }\n\n if (hasModel && hasPlanner) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): \\`model\\` and \\`planner\\` are mutually exclusive — configure exactly one`,\n { context: { authoring: true } },\n );\n }\n\n if (!Array.isArray(config.capabilities) || config.capabilities.length === 0) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): at least one capability is required`,\n { context: { authoring: true } },\n );\n }\n\n const seen = new Set<string>();\n\n for (const capability of config.capabilities) {\n if (!capability || typeof capability.name !== \"string\" || capability.name.length === 0) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): every capability needs a non-empty \\`name\\``,\n { context: { authoring: true } },\n );\n }\n\n if (typeof capability.description !== \"string\" || capability.description.length === 0) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): capability \"${capability.name}\" needs a \\`description\\``,\n { context: { authoring: true } },\n );\n }\n\n if (!capability.executable || typeof capability.executable.execute !== \"function\") {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): capability \"${capability.name}\" needs an \\`executable\\` with an execute() method`,\n { context: { authoring: true } },\n );\n }\n\n if (seen.has(capability.name)) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): duplicate capability name \"${capability.name}\"`,\n { context: { authoring: true } },\n );\n }\n\n seen.add(capability.name);\n }\n\n if (config.maxSteps !== undefined && config.maxSteps < 1) {\n throw new PlannerFailedError(`ai.planner(\"${config.name}\"): \\`maxSteps\\` must be >= 1`, {\n context: { authoring: true, maxSteps: config.maxSteps },\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"planner.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai/src/planner/planner.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { agent } from \"../agent/agent\";\nimport type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { PlannerCapability } from \"../contracts/planner/planner-capability.type\";\nimport type { PlannerConfig } from \"../contracts/planner/planner-config.type\";\nimport type {\n PlannerExecuteOptions,\n PlannerResumeOptions,\n} from \"../contracts/planner/planner-execute-options.type\";\nimport type { PlannerResult } from \"../contracts/planner/planner-result.type\";\nimport type { PlannerContract } from \"../contracts/planner/planner.contract\";\nimport { PlannerFailedError } from \"../errors\";\nimport { buildPlanSystemPrompt } from \"./plan-prompt\";\nimport { PlannerRun } from \"./planner-run\";\nimport { computeSignature } from \"./signature\";\nimport { loadPlannerSnapshotForResume } from \"./snapshot\";\n\nconst LOG_MODULE = \"ai.planner\";\n\n/**\n * `ai.planner(config)` — construct a {@link PlannerContract}.\n *\n * Validates the config at author time (throws {@link PlannerFailedError}\n * on a bad shape), builds (or adopts) the plan-generation agent, computes\n * a stable structural signature, and returns an instance satisfying\n * `ExecutableContract` so the planner composes into supervisors,\n * orchestrators, and outer agents through the same uniform surface.\n *\n * At `execute(goal)` the planner asks its LLM for an ordered plan over\n * the registered `capabilities`, then executes that plan step-by-step\n * through each capability's own `execute()` — reusing the existing\n * executable machinery rather than forking it — and returns the unified\n * `{ data, report, usage, error }` envelope with `report.type ===\n * \"planner\"`.\n *\n * @example\n * const research = ai.planner({\n * name: \"research-assistant\",\n * model: ai.openai.model({ name: \"gpt-4o\" }),\n * capabilities: [\n * { name: \"search\", description: \"Search the web\", executable: searchAgent },\n * { name: \"write\", description: \"Draft a summary\", executable: writerAgent },\n * ],\n * maxSteps: 6,\n * });\n *\n * const { data, report } = await research.execute(\"Compare React vs Vue in 2026\");\n */\nexport function planner<TOutput = unknown>(\n config: PlannerConfig<TOutput>,\n): PlannerContract<TOutput> {\n validateConfig(config);\n\n const maxSteps = config.maxSteps ?? 10;\n const capabilities = new Map<string, PlannerCapability>();\n\n for (const capability of config.capabilities) {\n capabilities.set(capability.name, capability);\n }\n\n const signature = computeSignature(config.name, config.capabilities);\n const planningAgent = resolvePlanningAgent(config, maxSteps);\n\n async function execute(\n goal: string,\n options?: PlannerExecuteOptions<TOutput>,\n ): Promise<PlannerResult<TOutput>> {\n log.debug(LOG_MODULE, \"execute\", \"Planner run starting\", {\n name: config.name,\n capabilities: capabilities.size,\n });\n\n return new PlannerRun<TOutput>({\n config,\n capabilities,\n maxSteps,\n signature,\n planningAgent,\n goal,\n options,\n }).run();\n }\n\n async function resume(\n runId: string,\n options?: PlannerResumeOptions<TOutput>,\n ): Promise<PlannerResult<TOutput>> {\n // Load the persisted snapshot and run the drift check (throws\n // PlannerDriftError on a structural mismatch unless `{ force: true }`).\n const snapshot = await loadPlannerSnapshotForResume({\n durable: config.durable,\n plannerName: config.name,\n signature,\n runId,\n options: options as PlannerResumeOptions<unknown> | undefined,\n });\n\n return new PlannerRun<TOutput>({\n config,\n capabilities,\n maxSteps,\n signature,\n planningAgent,\n goal: snapshot.goal,\n options: { ...options, runId } as PlannerExecuteOptions<TOutput>,\n resumeFrom: snapshot,\n }).run();\n }\n\n return {\n name: config.name,\n signature,\n execute,\n resume,\n };\n}\n\n/**\n * Resolve the plan-generation agent: either adopt the dev's `planner`\n * agent, or build an internal one from `model` with the generated\n * plan-system-prompt baked on. The plan output schema is supplied\n * per-call in {@link PlannerRun}, so it isn't baked here.\n *\n * **`maxSteps` and BYO planners.** In `model` mode the cap is woven\n * into the generated plan-system-prompt *and* the per-call plan schema\n * (`steps.maxItems`). In `planner` (BYO) mode the dev owns the prompt,\n * so the cap is communicated only through that same per-call schema —\n * and, regardless of mode, {@link PlannerRun} truncates any over-long\n * plan to `skipped` at execution time, so the cap is always enforced.\n */\nfunction resolvePlanningAgent<TOutput>(\n config: PlannerConfig<TOutput>,\n maxSteps: number,\n): AgentContract<unknown> {\n if (config.planner) {\n return config.planner;\n }\n\n const systemPrompt = buildPlanSystemPrompt(\n config.capabilities,\n maxSteps,\n config.systemPrompt,\n config.dag === true,\n );\n\n return agent({\n name: `${config.name}-planner`,\n description: \"Generates an ordered execution plan over the planner's capabilities.\",\n model: config.model!,\n systemPrompt,\n maxTrips: 1,\n });\n}\n\n/**\n * Factory-time validation. Surfaces every violation as a typed\n * {@link PlannerFailedError} tagged `authoring: true`, mirroring the\n * supervisor/orchestrator authoring-error convention.\n */\nfunction validateConfig<TOutput>(config: PlannerConfig<TOutput>): void {\n if (!config.name || typeof config.name !== \"string\") {\n throw new PlannerFailedError(\"ai.planner: `name` is required and must be a string\", {\n context: { authoring: true },\n });\n }\n\n const hasModel = config.model !== undefined;\n const hasPlanner = config.planner !== undefined;\n\n if (!hasModel && !hasPlanner) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): one of \\`model\\` or \\`planner\\` is required`,\n { context: { authoring: true } },\n );\n }\n\n if (hasModel && hasPlanner) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): \\`model\\` and \\`planner\\` are mutually exclusive — configure exactly one`,\n { context: { authoring: true } },\n );\n }\n\n if (!Array.isArray(config.capabilities) || config.capabilities.length === 0) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): at least one capability is required`,\n { context: { authoring: true } },\n );\n }\n\n const seen = new Set<string>();\n\n for (const capability of config.capabilities) {\n if (!capability || typeof capability.name !== \"string\" || capability.name.length === 0) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): every capability needs a non-empty \\`name\\``,\n { context: { authoring: true } },\n );\n }\n\n if (typeof capability.description !== \"string\" || capability.description.length === 0) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): capability \"${capability.name}\" needs a \\`description\\``,\n { context: { authoring: true } },\n );\n }\n\n if (!capability.executable || typeof capability.executable.execute !== \"function\") {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): capability \"${capability.name}\" needs an \\`executable\\` with an execute() method`,\n { context: { authoring: true } },\n );\n }\n\n if (seen.has(capability.name)) {\n throw new PlannerFailedError(\n `ai.planner(\"${config.name}\"): duplicate capability name \"${capability.name}\"`,\n { context: { authoring: true } },\n );\n }\n\n seen.add(capability.name);\n }\n\n if (config.maxSteps !== undefined && config.maxSteps < 1) {\n throw new PlannerFailedError(`ai.planner(\"${config.name}\"): \\`maxSteps\\` must be >= 1`, {\n context: { authoring: true, maxSteps: config.maxSteps },\n });\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import { PlannerFailedError } from "../errors/planner-failed-error.mjs";
2
+ import { PlannerDriftError } from "../errors/planner-drift-error.mjs";
3
+ import "../errors/index.mjs";
4
+ import { resolveDefaultSnapshotStore } from "../config.mjs";
5
+
6
+ //#region ../@warlock.js/ai/src/planner/snapshot.ts
7
+ /**
8
+ * Resolve the effective {@link SnapshotStore}: the planner's own
9
+ * `durable.store` wins; absent that, fall back to the global default set
10
+ * via `ai.config({ defaultSnapshotStore })`.
11
+ *
12
+ * The global default is typed for the supervisor snapshot shape, but
13
+ * every store impl keys purely by `runId` and round-trips whatever
14
+ * envelope it is handed — so it serves a `PlannerSnapshot` just as well.
15
+ * The cast re-tags the shape at this single boundary (Option B); the
16
+ * planner only ever hands it a `PlannerSnapshot`.
17
+ */
18
+ function resolveSnapshotStore(durable) {
19
+ return durable?.store ?? resolveDefaultSnapshotStore();
20
+ }
21
+ /**
22
+ * Write the current run state to the resolved snapshot store. No-op
23
+ * (returns `{ ok: true }`) when neither `durable.store` nor the global
24
+ * `defaultSnapshotStore` is configured — the common non-durable path.
25
+ * Failures are returned as `{ ok: false }` rather than thrown so the
26
+ * engine can surface them via logs without aborting the run.
27
+ */
28
+ async function persistPlannerSnapshot(params) {
29
+ const store = resolveSnapshotStore(params.durable);
30
+ if (!store) return { ok: true };
31
+ const snapshot = {
32
+ runId: params.runId,
33
+ plannerName: params.plannerName,
34
+ signature: params.signature,
35
+ version: params.version,
36
+ goal: params.goal,
37
+ plan: params.plan,
38
+ executedSteps: params.executedSteps,
39
+ usage: params.usage,
40
+ children: params.children,
41
+ replanCount: params.replanCount,
42
+ status: params.status,
43
+ startedAt: params.startedAt,
44
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
45
+ };
46
+ try {
47
+ await store.save(snapshot);
48
+ return { ok: true };
49
+ } catch (error) {
50
+ return {
51
+ ok: false,
52
+ error
53
+ };
54
+ }
55
+ }
56
+ /**
57
+ * Delete a persisted snapshot — used after a successful run when
58
+ * `durable.deleteOnComplete` is set. Never throws. No-op (ok) when no
59
+ * store is configured.
60
+ */
61
+ async function deletePlannerSnapshot(params) {
62
+ const store = resolveSnapshotStore(params.durable);
63
+ if (!store) return { ok: true };
64
+ try {
65
+ await store.delete(params.runId);
66
+ return { ok: true };
67
+ } catch (error) {
68
+ return {
69
+ ok: false,
70
+ error
71
+ };
72
+ }
73
+ }
74
+ /**
75
+ * Load a persisted snapshot for `resume()` and run the drift check.
76
+ * Throws `PlannerFailedError` when no store is configured or when the run
77
+ * is missing; throws `PlannerDriftError` when the stored signature
78
+ * doesn't match the current definition (unless `force` is set).
79
+ */
80
+ async function loadPlannerSnapshotForResume(params) {
81
+ const store = resolveSnapshotStore(params.durable);
82
+ if (!store) throw new PlannerFailedError(`ai.planner("${params.plannerName}"): no durable store configured — set \`durable: { store }\` on the config or call \`ai.config({ defaultSnapshotStore })\` at boot before calling resume()`, { context: { runId: params.runId } });
83
+ const snapshot = await store.load(params.runId) ?? null;
84
+ if (!snapshot) throw new PlannerFailedError(`ai.planner("${params.plannerName}"): no snapshot for runId "${params.runId}"`, { context: { runId: params.runId } });
85
+ if (!params.options?.force && snapshot.signature !== params.signature) throw new PlannerDriftError(`ai.planner("${params.plannerName}") signature drift on resume`, {
86
+ savedSignature: snapshot.signature,
87
+ currentSignature: params.signature,
88
+ runId: params.runId
89
+ });
90
+ return snapshot;
91
+ }
92
+
93
+ //#endregion
94
+ export { deletePlannerSnapshot, loadPlannerSnapshotForResume, persistPlannerSnapshot };
95
+ //# sourceMappingURL=snapshot.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai/src/planner/snapshot.ts"],"sourcesContent":["import { resolveDefaultSnapshotStore } from \"../config\";\nimport type { SnapshotStore } from \"../contracts/orchestrator/snapshot-store.contract\";\nimport type { PlannerResumeOptions } from \"../contracts/planner/planner-execute-options.type\";\nimport type { PlannerPlan } from \"../contracts/planner/planner-plan.type\";\nimport type { PlannerStepSnapshot } from \"../contracts/planner/planner-result.type\";\nimport type {\n PlannerSnapshot,\n PlannerSnapshotStatus,\n} from \"../contracts/planner/planner-snapshot.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { PlannerDriftError, PlannerFailedError } from \"../errors\";\n\n/**\n * The planner's `durable` config, narrowed to the fields the snapshot\n * helpers read.\n */\nexport type PlannerDurableConfig = {\n store?: SnapshotStore<PlannerSnapshot>;\n deleteOnComplete?: boolean;\n};\n\n/**\n * Resolve the effective {@link SnapshotStore}: the planner's own\n * `durable.store` wins; absent that, fall back to the global default set\n * via `ai.config({ defaultSnapshotStore })`.\n *\n * The global default is typed for the supervisor snapshot shape, but\n * every store impl keys purely by `runId` and round-trips whatever\n * envelope it is handed — so it serves a `PlannerSnapshot` just as well.\n * The cast re-tags the shape at this single boundary (Option B); the\n * planner only ever hands it a `PlannerSnapshot`.\n */\nfunction resolveSnapshotStore(\n durable: PlannerDurableConfig | undefined,\n): SnapshotStore<PlannerSnapshot> | undefined {\n return (\n durable?.store ??\n (resolveDefaultSnapshotStore() as SnapshotStore<PlannerSnapshot> | undefined)\n );\n}\n\nexport type PersistPlannerParams = {\n durable: PlannerDurableConfig | undefined;\n runId: string;\n plannerName: string;\n signature: string;\n version?: string;\n goal: string;\n plan: PlannerPlan;\n executedSteps: PlannerStepSnapshot[];\n usage: Usage;\n children: BaseReport[];\n replanCount: number;\n status: PlannerSnapshotStatus;\n startedAt: string;\n};\n\nexport type PersistOutcome = { ok: true } | { ok: false; error: unknown };\n\n/**\n * Write the current run state to the resolved snapshot store. No-op\n * (returns `{ ok: true }`) when neither `durable.store` nor the global\n * `defaultSnapshotStore` is configured — the common non-durable path.\n * Failures are returned as `{ ok: false }` rather than thrown so the\n * engine can surface them via logs without aborting the run.\n */\nexport async function persistPlannerSnapshot(\n params: PersistPlannerParams,\n): Promise<PersistOutcome> {\n const store = resolveSnapshotStore(params.durable);\n\n if (!store) {\n return { ok: true };\n }\n\n const snapshot: PlannerSnapshot = {\n runId: params.runId,\n plannerName: params.plannerName,\n signature: params.signature,\n version: params.version,\n goal: params.goal,\n plan: params.plan,\n executedSteps: params.executedSteps,\n usage: params.usage,\n children: params.children,\n replanCount: params.replanCount,\n status: params.status,\n startedAt: params.startedAt,\n savedAt: new Date().toISOString(),\n };\n\n try {\n await store.save(snapshot);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, error };\n }\n}\n\n/**\n * Delete a persisted snapshot — used after a successful run when\n * `durable.deleteOnComplete` is set. Never throws. No-op (ok) when no\n * store is configured.\n */\nexport async function deletePlannerSnapshot(params: {\n durable: PlannerDurableConfig | undefined;\n runId: string;\n}): Promise<PersistOutcome> {\n const store = resolveSnapshotStore(params.durable);\n\n if (!store) {\n return { ok: true };\n }\n\n try {\n await store.delete(params.runId);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, error };\n }\n}\n\n/**\n * Load a persisted snapshot for `resume()` and run the drift check.\n * Throws `PlannerFailedError` when no store is configured or when the run\n * is missing; throws `PlannerDriftError` when the stored signature\n * doesn't match the current definition (unless `force` is set).\n */\nexport async function loadPlannerSnapshotForResume(params: {\n durable: PlannerDurableConfig | undefined;\n plannerName: string;\n signature: string;\n runId: string;\n options?: PlannerResumeOptions<unknown>;\n}): Promise<PlannerSnapshot> {\n const store = resolveSnapshotStore(params.durable);\n\n if (!store) {\n throw new PlannerFailedError(\n `ai.planner(\"${params.plannerName}\"): no durable store configured — set \\`durable: { store }\\` on the config or call \\`ai.config({ defaultSnapshotStore })\\` at boot before calling resume()`,\n { context: { runId: params.runId } },\n );\n }\n\n const snapshot = (await store.load(params.runId)) ?? null;\n\n if (!snapshot) {\n throw new PlannerFailedError(\n `ai.planner(\"${params.plannerName}\"): no snapshot for runId \"${params.runId}\"`,\n { context: { runId: params.runId } },\n );\n }\n\n if (!params.options?.force && snapshot.signature !== params.signature) {\n throw new PlannerDriftError(\n `ai.planner(\"${params.plannerName}\") signature drift on resume`,\n {\n savedSignature: snapshot.signature,\n currentSignature: params.signature,\n runId: params.runId,\n },\n );\n }\n\n return snapshot;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiCA,SAAS,qBACP,SAC4C;CAC5C,OACE,SAAS,SACR,4BAA4B;AAEjC;;;;;;;;AA2BA,eAAsB,uBACpB,QACyB;CACzB,MAAM,QAAQ,qBAAqB,OAAO,OAAO;CAEjD,IAAI,CAAC,OACH,OAAO,EAAE,IAAI,KAAK;CAGpB,MAAM,WAA4B;EAChC,OAAO,OAAO;EACd,aAAa,OAAO;EACpB,WAAW,OAAO;EAClB,SAAS,OAAO;EAChB,MAAM,OAAO;EACb,MAAM,OAAO;EACb,eAAe,OAAO;EACtB,OAAO,OAAO;EACd,UAAU,OAAO;EACjB,aAAa,OAAO;EACpB,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,0BAAS,IAAI,KAAK,EAAC,CAAC,YAAY;CAClC;CAEA,IAAI;EACF,MAAM,MAAM,KAAK,QAAQ;EAEzB,OAAO,EAAE,IAAI,KAAK;CACpB,SAAS,OAAO;EACd,OAAO;GAAE,IAAI;GAAO;EAAM;CAC5B;AACF;;;;;;AAOA,eAAsB,sBAAsB,QAGhB;CAC1B,MAAM,QAAQ,qBAAqB,OAAO,OAAO;CAEjD,IAAI,CAAC,OACH,OAAO,EAAE,IAAI,KAAK;CAGpB,IAAI;EACF,MAAM,MAAM,OAAO,OAAO,KAAK;EAE/B,OAAO,EAAE,IAAI,KAAK;CACpB,SAAS,OAAO;EACd,OAAO;GAAE,IAAI;GAAO;EAAM;CAC5B;AACF;;;;;;;AAQA,eAAsB,6BAA6B,QAMtB;CAC3B,MAAM,QAAQ,qBAAqB,OAAO,OAAO;CAEjD,IAAI,CAAC,OACH,MAAM,IAAI,mBACR,eAAe,OAAO,YAAY,6JAClC,EAAE,SAAS,EAAE,OAAO,OAAO,MAAM,EAAE,CACrC;CAGF,MAAM,WAAY,MAAM,MAAM,KAAK,OAAO,KAAK,KAAM;CAErD,IAAI,CAAC,UACH,MAAM,IAAI,mBACR,eAAe,OAAO,YAAY,6BAA6B,OAAO,MAAM,IAC5E,EAAE,SAAS,EAAE,OAAO,OAAO,MAAM,EAAE,CACrC;CAGF,IAAI,CAAC,OAAO,SAAS,SAAS,SAAS,cAAc,OAAO,WAC1D,MAAM,IAAI,kBACR,eAAe,OAAO,YAAY,+BAClC;EACE,gBAAgB,SAAS;EACzB,kBAAkB,OAAO;EACzB,OAAO,OAAO;CAChB,CACF;CAGF,OAAO;AACT"}
@@ -7,6 +7,13 @@ import { rag } from "./rag.mjs";
7
7
  import { chunk } from "./chunk/chunk.mjs";
8
8
  import { VectorStore } from "./store/vector-store.contract.mjs";
9
9
  import { cacheVectorStore } from "./store/cache-vector-store.mjs";
10
+ import { PgClientLike, PgVectorStoreInstance, PgVectorStoreOptions, pgVectorStore, vectorLiteral } from "./store/pg-vector-store.mjs";
11
+ import { LoadHtmlOptions, LoadPdfOptions, LoadTextOptions, LoadWebOptions, RagLoaderMetadata, RagLoaderOptions, RagLoaderResult, RagLoaderType } from "./loaders/loader.type.mjs";
12
+ import { TextInput, loadText } from "./loaders/load-text.mjs";
13
+ import { loadHtml } from "./loaders/load-html.mjs";
14
+ import { loadWeb } from "./loaders/load-web.mjs";
15
+ import { loadPdf } from "./loaders/load-pdf.mjs";
16
+ import { PDF_PARSE_INSTALL_INSTRUCTIONS } from "./loaders/errors.mjs";
10
17
  import { KeywordRerankerOptions, keywordReranker } from "./rerank/keyword-reranker.mjs";
11
18
  import { LlmRerankerOptions, llmReranker } from "./rerank/llm-reranker.mjs";
12
19
  import { RankedItem, reciprocalRankFusion } from "./hybrid/rrf.mjs";
package/esm/rag/index.mjs CHANGED
@@ -1,6 +1,13 @@
1
1
  import { chunk } from "./chunk/chunk.mjs";
2
2
  import { cacheVectorStore } from "./store/cache-vector-store.mjs";
3
3
  import { rag } from "./rag.mjs";
4
+ import { pgVectorStore, vectorLiteral } from "./store/pg-vector-store.mjs";
5
+ import { loadText } from "./loaders/load-text.mjs";
6
+ import { loadHtml } from "./loaders/load-html.mjs";
7
+ import { loadWeb } from "./loaders/load-web.mjs";
8
+ import { PDF_PARSE_INSTALL_INSTRUCTIONS } from "./loaders/errors.mjs";
9
+ import { loadPdf } from "./loaders/load-pdf.mjs";
10
+ import "./loaders/index.mjs";
4
11
  import { keywordReranker } from "./rerank/keyword-reranker.mjs";
5
12
  import { llmReranker } from "./rerank/llm-reranker.mjs";
6
13
  import { reciprocalRankFusion } from "./hybrid/rrf.mjs";
@@ -0,0 +1,19 @@
1
+ //#region ../@warlock.js/ai/src/rag/loaders/errors.d.ts
2
+ /**
3
+ * Loader error surface. The only loader-specific failure is a missing
4
+ * OPTIONAL peer (`pdf-parse`), which — like the moderation detector's
5
+ * missing `openai` peer — is an *infrastructure* fault, not a content
6
+ * problem, so {@link loadPdf} throws a plain `Error` carrying the curated
7
+ * {@link PDF_PARSE_INSTALL_INSTRUCTIONS} rather than an `AIError`. Mirrors
8
+ * the guard's `OPENAI_INSTALL_INSTRUCTIONS` / ai-panoptic's
9
+ * `LANGFUSE_INSTALL_INSTRUCTIONS`.
10
+ */
11
+ /**
12
+ * Curated install string thrown by {@link loadPdf} on first call when the
13
+ * `pdf-parse` peer is absent. Surfaced instead of a raw
14
+ * module-resolution stack trace.
15
+ */
16
+ declare const PDF_PARSE_INSTALL_INSTRUCTIONS: string;
17
+ //#endregion
18
+ export { PDF_PARSE_INSTALL_INSTRUCTIONS };
19
+ //# sourceMappingURL=errors.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/ai/src/rag/loaders/errors.ts"],"mappings":";;AAeA;;;;AAKQ;;;;;;;;;cALK,8BAAA"}
@@ -0,0 +1,25 @@
1
+ //#region ../@warlock.js/ai/src/rag/loaders/errors.ts
2
+ /**
3
+ * Loader error surface. The only loader-specific failure is a missing
4
+ * OPTIONAL peer (`pdf-parse`), which — like the moderation detector's
5
+ * missing `openai` peer — is an *infrastructure* fault, not a content
6
+ * problem, so {@link loadPdf} throws a plain `Error` carrying the curated
7
+ * {@link PDF_PARSE_INSTALL_INSTRUCTIONS} rather than an `AIError`. Mirrors
8
+ * the guard's `OPENAI_INSTALL_INSTRUCTIONS` / ai-panoptic's
9
+ * `LANGFUSE_INSTALL_INSTRUCTIONS`.
10
+ */
11
+ /**
12
+ * Curated install string thrown by {@link loadPdf} on first call when the
13
+ * `pdf-parse` peer is absent. Surfaced instead of a raw
14
+ * module-resolution stack trace.
15
+ */
16
+ const PDF_PARSE_INSTALL_INSTRUCTIONS = `
17
+ The @warlock.js/ai PDF loader requires the optional "pdf-parse" peer.
18
+ Install it with:
19
+
20
+ npm install pdf-parse
21
+ `.trim();
22
+
23
+ //#endregion
24
+ export { PDF_PARSE_INSTALL_INSTRUCTIONS };
25
+ //# sourceMappingURL=errors.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai/src/rag/loaders/errors.ts"],"sourcesContent":["/**\n * Loader error surface. The only loader-specific failure is a missing\n * OPTIONAL peer (`pdf-parse`), which — like the moderation detector's\n * missing `openai` peer — is an *infrastructure* fault, not a content\n * problem, so {@link loadPdf} throws a plain `Error` carrying the curated\n * {@link PDF_PARSE_INSTALL_INSTRUCTIONS} rather than an `AIError`. Mirrors\n * the guard's `OPENAI_INSTALL_INSTRUCTIONS` / ai-panoptic's\n * `LANGFUSE_INSTALL_INSTRUCTIONS`.\n */\n\n/**\n * Curated install string thrown by {@link loadPdf} on first call when the\n * `pdf-parse` peer is absent. Surfaced instead of a raw\n * module-resolution stack trace.\n */\nexport const PDF_PARSE_INSTALL_INSTRUCTIONS = `\nThe @warlock.js/ai PDF loader requires the optional \"pdf-parse\" peer.\nInstall it with:\n\n npm install pdf-parse\n`.trim();\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,iCAAiC;;;;;EAK5C,KAAK"}
@@ -0,0 +1,7 @@
1
+ import { loadText } from "./load-text.mjs";
2
+ import { loadHtml } from "./load-html.mjs";
3
+ import { loadWeb } from "./load-web.mjs";
4
+ import { PDF_PARSE_INSTALL_INSTRUCTIONS } from "./errors.mjs";
5
+ import { loadPdf } from "./load-pdf.mjs";
6
+
7
+ export { };
@@ -0,0 +1,26 @@
1
+ import { LoadHtmlOptions, RagLoaderResult } from "./loader.type.mjs";
2
+
3
+ //#region ../@warlock.js/ai/src/rag/loaders/load-html.d.ts
4
+ /**
5
+ * Load an HTML string into a single {@link RagDocument} of readable text.
6
+ * Scripts, styles, and other non-prose elements are dropped content-and-all,
7
+ * block tags become line breaks (so paragraph structure survives for the
8
+ * splitter), remaining tags are stripped, and HTML entities are decoded — a
9
+ * lightweight regex pass, no heavy DOM dependency.
10
+ *
11
+ * The document's `metadata.title` is set from the page's `<title>` (unless
12
+ * the caller overrode it), and `metadata.loader` is `"html"`. The output is
13
+ * the exact shape `index()` consumes.
14
+ *
15
+ * @example
16
+ * const kb = ai.rag({ embedder, store });
17
+ * await kb.index(loadHtml(rawHtmlString, { id: "landing-page" }));
18
+ *
19
+ * @param html - The raw HTML markup.
20
+ * @param options - Shared `id` / `metadata` / `tags` ({@link LoadHtmlOptions}).
21
+ * @returns A {@link RagLoaderResult} (one document) ready for `rag.index()`.
22
+ */
23
+ declare function loadHtml(html: string, options?: LoadHtmlOptions): RagLoaderResult;
24
+ //#endregion
25
+ export { loadHtml };
26
+ //# sourceMappingURL=load-html.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-html.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/ai/src/rag/loaders/load-html.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;iBAiKgB,QAAA,CACd,IAAA,UACA,OAAA,GAAS,eAAA,GACR,eAAe"}
@@ -0,0 +1,138 @@
1
+ //#region ../@warlock.js/ai/src/rag/loaders/load-html.ts
2
+ /** Default `id` when the caller supplies none. */
3
+ const DEFAULT_ID = "document";
4
+ /**
5
+ * Elements whose *content* is not human-readable text and must be removed
6
+ * wholesale (open tag → close tag → everything in between) before tags are
7
+ * stripped. `script` / `style` would otherwise leak code into the chunked
8
+ * text; `noscript` / `template` / `head` / `svg` are non-prose noise.
9
+ */
10
+ const STRIPPED_ELEMENTS = [
11
+ "script",
12
+ "style",
13
+ "noscript",
14
+ "template",
15
+ "head",
16
+ "svg"
17
+ ];
18
+ /**
19
+ * Block-level tags that imply a line break in the readable text. Replacing
20
+ * them with `\n` BEFORE the generic tag strip keeps paragraph / list / table
21
+ * structure (so the recursive splitter still sees `\n\n` boundaries) instead
22
+ * of collapsing the whole page onto one line.
23
+ */
24
+ const BLOCK_TAGS = /<\/?(?:p|div|section|article|header|footer|main|aside|nav|h[1-6]|ul|ol|li|table|tr|td|th|thead|tbody|blockquote|pre|hr|br)\b[^>]*>/gi;
25
+ /** Named HTML entities common in prose. Numeric entities are decoded generically. */
26
+ const NAMED_ENTITIES = {
27
+ amp: "&",
28
+ lt: "<",
29
+ gt: ">",
30
+ quot: "\"",
31
+ apos: "'",
32
+ nbsp: " ",
33
+ copy: "©",
34
+ reg: "®",
35
+ trade: "™",
36
+ hellip: "…",
37
+ mdash: "—",
38
+ ndash: "–",
39
+ lsquo: "‘",
40
+ rsquo: "’",
41
+ ldquo: "“",
42
+ rdquo: "”",
43
+ laquo: "«",
44
+ raquo: "»",
45
+ middot: "·",
46
+ bull: "•"
47
+ };
48
+ /**
49
+ * Decode the HTML entities that survive tag stripping: named (`&amp;`),
50
+ * decimal (`&#169;`), and hex (`&#xA9;`). Unknown named entities are left
51
+ * verbatim rather than dropped, so unusual markup never silently loses text.
52
+ */
53
+ function decodeEntities(text) {
54
+ return text.replace(/&(#x?[0-9a-f]+|[a-z][a-z0-9]*);/gi, (match, body) => {
55
+ if (body[0] === "#") {
56
+ const codePoint = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
57
+ if (Number.isNaN(codePoint) || codePoint < 0 || codePoint > 1114111) return match;
58
+ try {
59
+ return String.fromCodePoint(codePoint);
60
+ } catch {
61
+ return match;
62
+ }
63
+ }
64
+ return NAMED_ENTITIES[body.toLowerCase()] ?? match;
65
+ });
66
+ }
67
+ /**
68
+ * Pull the `<title>` text out of the document, decoded and trimmed, or
69
+ * `undefined` when there is none. Read BEFORE `<head>` is stripped.
70
+ */
71
+ function extractTitle(html) {
72
+ const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
73
+ if (!match) return;
74
+ const title = decodeEntities(match[1]).replace(/\s+/g, " ").trim();
75
+ return title.length > 0 ? title : void 0;
76
+ }
77
+ /**
78
+ * Strip HTML markup down to readable plain text — a lightweight,
79
+ * dependency-free pass (no DOM parser): drop comments and non-prose elements
80
+ * (`script` / `style` / `head` / `svg` / …) content-and-all, convert block
81
+ * tags to line breaks to preserve paragraph structure, remove every
82
+ * remaining tag, decode entities, then collapse runs of whitespace while
83
+ * keeping blank-line paragraph separators.
84
+ */
85
+ function htmlToText(html) {
86
+ let text = html;
87
+ text = text.replace(/<!--[\s\S]*?-->/g, " ");
88
+ for (const tag of STRIPPED_ELEMENTS) {
89
+ const element = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, "gi");
90
+ text = text.replace(element, " ");
91
+ text = text.replace(new RegExp(`<\\/?${tag}\\b[^>]*>`, "gi"), " ");
92
+ }
93
+ text = text.replace(BLOCK_TAGS, "\n");
94
+ text = text.replace(/<[^>]+>/g, "");
95
+ text = decodeEntities(text);
96
+ text = text.replace(/[^\S\n]+/g, " ").replace(/[ \t]*\n[ \t]*/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
97
+ return text;
98
+ }
99
+ /**
100
+ * Load an HTML string into a single {@link RagDocument} of readable text.
101
+ * Scripts, styles, and other non-prose elements are dropped content-and-all,
102
+ * block tags become line breaks (so paragraph structure survives for the
103
+ * splitter), remaining tags are stripped, and HTML entities are decoded — a
104
+ * lightweight regex pass, no heavy DOM dependency.
105
+ *
106
+ * The document's `metadata.title` is set from the page's `<title>` (unless
107
+ * the caller overrode it), and `metadata.loader` is `"html"`. The output is
108
+ * the exact shape `index()` consumes.
109
+ *
110
+ * @example
111
+ * const kb = ai.rag({ embedder, store });
112
+ * await kb.index(loadHtml(rawHtmlString, { id: "landing-page" }));
113
+ *
114
+ * @param html - The raw HTML markup.
115
+ * @param options - Shared `id` / `metadata` / `tags` ({@link LoadHtmlOptions}).
116
+ * @returns A {@link RagLoaderResult} (one document) ready for `rag.index()`.
117
+ */
118
+ function loadHtml(html, options = {}) {
119
+ const id = options.id ?? DEFAULT_ID;
120
+ const title = extractTitle(html);
121
+ const text = htmlToText(html);
122
+ if (text.length === 0) return [];
123
+ return [{
124
+ id,
125
+ text,
126
+ metadata: {
127
+ source: id,
128
+ loader: "html",
129
+ ...title !== void 0 ? { title } : {},
130
+ ...options.metadata
131
+ },
132
+ tags: options.tags
133
+ }];
134
+ }
135
+
136
+ //#endregion
137
+ export { extractTitle, htmlToText, loadHtml };
138
+ //# sourceMappingURL=load-html.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-html.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai/src/rag/loaders/load-html.ts"],"sourcesContent":["import type { RagDocument } from \"../contracts/rag-document.type\";\nimport type { LoadHtmlOptions, RagLoaderResult } from \"./loader.type\";\n\n/** Default `id` when the caller supplies none. */\nconst DEFAULT_ID = \"document\";\n\n/**\n * Elements whose *content* is not human-readable text and must be removed\n * wholesale (open tag → close tag → everything in between) before tags are\n * stripped. `script` / `style` would otherwise leak code into the chunked\n * text; `noscript` / `template` / `head` / `svg` are non-prose noise.\n */\nconst STRIPPED_ELEMENTS = [\n \"script\",\n \"style\",\n \"noscript\",\n \"template\",\n \"head\",\n \"svg\",\n];\n\n/**\n * Block-level tags that imply a line break in the readable text. Replacing\n * them with `\\n` BEFORE the generic tag strip keeps paragraph / list / table\n * structure (so the recursive splitter still sees `\\n\\n` boundaries) instead\n * of collapsing the whole page onto one line.\n */\nconst BLOCK_TAGS =\n /<\\/?(?:p|div|section|article|header|footer|main|aside|nav|h[1-6]|ul|ol|li|table|tr|td|th|thead|tbody|blockquote|pre|hr|br)\\b[^>]*>/gi;\n\n/** Named HTML entities common in prose. Numeric entities are decoded generically. */\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n copy: \"©\",\n reg: \"®\",\n trade: \"™\",\n hellip: \"…\",\n mdash: \"—\",\n ndash: \"–\",\n lsquo: \"‘\",\n rsquo: \"’\",\n ldquo: \"“\",\n rdquo: \"”\",\n laquo: \"«\",\n raquo: \"»\",\n middot: \"·\",\n bull: \"•\",\n};\n\n/**\n * Decode the HTML entities that survive tag stripping: named (`&amp;`),\n * decimal (`&#169;`), and hex (`&#xA9;`). Unknown named entities are left\n * verbatim rather than dropped, so unusual markup never silently loses text.\n */\nfunction decodeEntities(text: string): string {\n return text.replace(/&(#x?[0-9a-f]+|[a-z][a-z0-9]*);/gi, (match, body: string) => {\n if (body[0] === \"#\") {\n const codePoint =\n body[1] === \"x\" || body[1] === \"X\"\n ? Number.parseInt(body.slice(2), 16)\n : Number.parseInt(body.slice(1), 10);\n\n if (Number.isNaN(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {\n return match;\n }\n\n try {\n return String.fromCodePoint(codePoint);\n } catch {\n return match;\n }\n }\n\n const named = NAMED_ENTITIES[body.toLowerCase()];\n\n return named ?? match;\n });\n}\n\n/**\n * Pull the `<title>` text out of the document, decoded and trimmed, or\n * `undefined` when there is none. Read BEFORE `<head>` is stripped.\n */\nfunction extractTitle(html: string): string | undefined {\n const match = /<title[^>]*>([\\s\\S]*?)<\\/title>/i.exec(html);\n\n if (!match) {\n return undefined;\n }\n\n const title = decodeEntities(match[1]).replace(/\\s+/g, \" \").trim();\n\n return title.length > 0 ? title : undefined;\n}\n\n/**\n * Strip HTML markup down to readable plain text — a lightweight,\n * dependency-free pass (no DOM parser): drop comments and non-prose elements\n * (`script` / `style` / `head` / `svg` / …) content-and-all, convert block\n * tags to line breaks to preserve paragraph structure, remove every\n * remaining tag, decode entities, then collapse runs of whitespace while\n * keeping blank-line paragraph separators.\n */\nfunction htmlToText(html: string): string {\n let text = html;\n\n // 1. Comments first — a commented-out `<script>` must not survive.\n text = text.replace(/<!--[\\s\\S]*?-->/g, \" \");\n\n // 2. Non-prose elements, content and all.\n for (const tag of STRIPPED_ELEMENTS) {\n const element = new RegExp(`<${tag}\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/${tag}>`, \"gi\");\n text = text.replace(element, \" \");\n // Defensively drop a self-closing / unterminated open tag too.\n text = text.replace(new RegExp(`<\\\\/?${tag}\\\\b[^>]*>`, \"gi\"), \" \");\n }\n\n // 3. Block tags → newlines, so paragraph / list structure survives.\n text = text.replace(BLOCK_TAGS, \"\\n\");\n\n // 4. Every remaining tag → gone.\n text = text.replace(/<[^>]+>/g, \"\");\n\n // 5. Entities → characters.\n text = decodeEntities(text);\n\n // 6. Normalize whitespace: trim each line, drop blank runs to a single\n // blank line (a paragraph separator the recursive splitter honors).\n text = text\n .replace(/[^\\S\\n]+/g, \" \")\n .replace(/[ \\t]*\\n[ \\t]*/g, \"\\n\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n\n return text;\n}\n\n/**\n * Load an HTML string into a single {@link RagDocument} of readable text.\n * Scripts, styles, and other non-prose elements are dropped content-and-all,\n * block tags become line breaks (so paragraph structure survives for the\n * splitter), remaining tags are stripped, and HTML entities are decoded — a\n * lightweight regex pass, no heavy DOM dependency.\n *\n * The document's `metadata.title` is set from the page's `<title>` (unless\n * the caller overrode it), and `metadata.loader` is `\"html\"`. The output is\n * the exact shape `index()` consumes.\n *\n * @example\n * const kb = ai.rag({ embedder, store });\n * await kb.index(loadHtml(rawHtmlString, { id: \"landing-page\" }));\n *\n * @param html - The raw HTML markup.\n * @param options - Shared `id` / `metadata` / `tags` ({@link LoadHtmlOptions}).\n * @returns A {@link RagLoaderResult} (one document) ready for `rag.index()`.\n */\nexport function loadHtml(\n html: string,\n options: LoadHtmlOptions = {},\n): RagLoaderResult {\n const id = options.id ?? DEFAULT_ID;\n const title = extractTitle(html);\n const text = htmlToText(html);\n\n // An all-markup / empty page strips to nothing; emit no document so\n // index() never receives a no-op record (matches loadText's behavior).\n if (text.length === 0) {\n return [];\n }\n\n // Derived keys (source, loader, title) sit UNDER the caller's metadata so\n // an explicit override always wins.\n const doc: RagDocument = {\n id,\n text,\n metadata: {\n source: id,\n loader: \"html\",\n ...(title !== undefined ? { title } : {}),\n ...options.metadata,\n },\n tags: options.tags,\n };\n\n return [doc];\n}\n\n/** Internal — exported for the web loader so it shares the exact strip pass. */\nexport { htmlToText, extractTitle };\n"],"mappings":";;AAIA,MAAM,aAAa;;;;;;;AAQnB,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;AAQA,MAAM,aACJ;;AAGF,MAAM,iBAAyC;CAC7C,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,MAAM;CACN,MAAM;CACN,MAAM;CACN,KAAK;CACL,OAAO;CACP,QAAQ;CACR,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,QAAQ;CACR,MAAM;AACR;;;;;;AAOA,SAAS,eAAe,MAAsB;CAC5C,OAAO,KAAK,QAAQ,sCAAsC,OAAO,SAAiB;EAChF,IAAI,KAAK,OAAO,KAAK;GACnB,MAAM,YACJ,KAAK,OAAO,OAAO,KAAK,OAAO,MAC3B,OAAO,SAAS,KAAK,MAAM,CAAC,GAAG,EAAE,IACjC,OAAO,SAAS,KAAK,MAAM,CAAC,GAAG,EAAE;GAEvC,IAAI,OAAO,MAAM,SAAS,KAAK,YAAY,KAAK,YAAY,SAC1D,OAAO;GAGT,IAAI;IACF,OAAO,OAAO,cAAc,SAAS;GACvC,QAAQ;IACN,OAAO;GACT;EACF;EAIA,OAFc,eAAe,KAAK,YAAY,MAE9B;CAClB,CAAC;AACH;;;;;AAMA,SAAS,aAAa,MAAkC;CACtD,MAAM,QAAQ,mCAAmC,KAAK,IAAI;CAE1D,IAAI,CAAC,OACH;CAGF,MAAM,QAAQ,eAAe,MAAM,EAAE,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;CAEjE,OAAO,MAAM,SAAS,IAAI,QAAQ;AACpC;;;;;;;;;AAUA,SAAS,WAAW,MAAsB;CACxC,IAAI,OAAO;CAGX,OAAO,KAAK,QAAQ,oBAAoB,GAAG;CAG3C,KAAK,MAAM,OAAO,mBAAmB;EACnC,MAAM,UAAU,IAAI,OAAO,IAAI,IAAI,yBAAyB,IAAI,IAAI,IAAI;EACxE,OAAO,KAAK,QAAQ,SAAS,GAAG;EAEhC,OAAO,KAAK,QAAQ,IAAI,OAAO,QAAQ,IAAI,YAAY,IAAI,GAAG,GAAG;CACnE;CAGA,OAAO,KAAK,QAAQ,YAAY,IAAI;CAGpC,OAAO,KAAK,QAAQ,YAAY,EAAE;CAGlC,OAAO,eAAe,IAAI;CAI1B,OAAO,KACJ,QAAQ,aAAa,GAAG,CAAC,CACzB,QAAQ,mBAAmB,IAAI,CAAC,CAChC,QAAQ,WAAW,MAAM,CAAC,CAC1B,KAAK;CAER,OAAO;AACT;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,SACd,MACA,UAA2B,CAAC,GACX;CACjB,MAAM,KAAK,QAAQ,MAAM;CACzB,MAAM,QAAQ,aAAa,IAAI;CAC/B,MAAM,OAAO,WAAW,IAAI;CAI5B,IAAI,KAAK,WAAW,GAClB,OAAO,CAAC;CAiBV,OAAO,CAAC;EAXN;EACA;EACA,UAAU;GACR,QAAQ;GACR,QAAQ;GACR,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;GACvC,GAAG,QAAQ;EACb;EACA,MAAM,QAAQ;CAGN,CAAC;AACb"}
@@ -0,0 +1,38 @@
1
+ import { LoadPdfOptions, RagLoaderResult } from "./loader.type.mjs";
2
+
3
+ //#region ../@warlock.js/ai/src/rag/loaders/load-pdf.d.ts
4
+ /**
5
+ * Load a PDF's bytes into {@link RagDocument}(s) via the OPTIONAL `pdf-parse`
6
+ * peer. The peer is resolved lazily on the FIRST call (not at import) so
7
+ * importing `@warlock.js/ai` never forces it to be installed; when it is
8
+ * absent the curated {@link PDF_PARSE_INSTALL_INSTRUCTIONS} is thrown as a
9
+ * plain `Error` (a missing optional peer is an infrastructure fault, not a
10
+ * content problem).
11
+ *
12
+ * By default the whole PDF becomes a single document carrying
13
+ * `metadata.pageCount`. With `perPage: true`, each page becomes its own
14
+ * document (`id` suffixed `#p<n>`, `metadata.page` set) so citations stay
15
+ * page-precise. Document `metadata.title` comes from the PDF info
16
+ * dictionary's `Title` (unless overridden), and `metadata.loader` is
17
+ * `"pdf"`. The output is the exact shape `index()` consumes.
18
+ *
19
+ * @example
20
+ * import { readFile } from "node:fs/promises";
21
+ * const kb = ai.rag({ embedder, store });
22
+ * await kb.index(await loadPdf(await readFile("guide.pdf"), { id: "guide" }));
23
+ *
24
+ * @example
25
+ * // One document per page for page-precise citations:
26
+ * await kb.index(await loadPdf(bytes, { id: "manual", perPage: true }));
27
+ *
28
+ * @param input - The PDF bytes (`Buffer`, `ArrayBuffer`, or `Uint8Array`).
29
+ * @param options - `perPage` plus shared `id` / `metadata` / `tags`
30
+ * ({@link LoadPdfOptions}).
31
+ * @returns A {@link RagLoaderResult} ready for `rag.index()`.
32
+ * @throws {Error} carrying {@link PDF_PARSE_INSTALL_INSTRUCTIONS} when the
33
+ * `pdf-parse` peer is not installed.
34
+ */
35
+ declare function loadPdf(input: Buffer | ArrayBuffer | Uint8Array, options?: LoadPdfOptions): Promise<RagLoaderResult>;
36
+ //#endregion
37
+ export { loadPdf };
38
+ //# sourceMappingURL=load-pdf.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-pdf.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/ai/src/rag/loaders/load-pdf.ts"],"mappings":";;;;;AA8HA;;;;;;;;;;;;;;;;;;;;AAG0B;;;;;;;;;iBAHJ,OAAA,CACpB,KAAA,EAAO,MAAA,GAAS,WAAA,GAAc,UAAA,EAC9B,OAAA,GAAS,cAAA,GACR,OAAA,CAAQ,eAAA"}
@@ -0,0 +1,150 @@
1
+ import { PDF_PARSE_INSTALL_INSTRUCTIONS } from "./errors.mjs";
2
+
3
+ //#region ../@warlock.js/ai/src/rag/loaders/load-pdf.ts
4
+ /** Default `id` when the caller supplies none. */
5
+ const DEFAULT_ID = "document";
6
+ let pdfParse;
7
+ let isModuleExists;
8
+ let loadingPromise;
9
+ /**
10
+ * Settle the lazy import of `pdf-parse` once, concurrency-safe. A bare
11
+ * `catch` flips the flag to `false`; the curated
12
+ * {@link PDF_PARSE_INSTALL_INSTRUCTIONS} surfaces at first
13
+ * {@link loadPdf} call, never a raw module-resolution stack trace. Mirrors
14
+ * the guard moderation detector's `loadOpenAi`.
15
+ */
16
+ function loadPdfParse() {
17
+ if (isModuleExists !== void 0) return Promise.resolve();
18
+ if (loadingPromise) return loadingPromise;
19
+ loadingPromise = (async () => {
20
+ try {
21
+ const mod = await import("pdf-parse");
22
+ pdfParse = mod.default ?? mod;
23
+ isModuleExists = typeof pdfParse === "function";
24
+ } catch {
25
+ isModuleExists = false;
26
+ }
27
+ })();
28
+ return loadingPromise;
29
+ }
30
+ /**
31
+ * Coerce a {@link RagDocument}-compatible binary input into a `Buffer` for
32
+ * `pdf-parse`. Accepts a Node `Buffer`, an `ArrayBuffer`, or a typed array
33
+ * (`Uint8Array`) — the shapes a file read / fetch body hands back.
34
+ */
35
+ function toBuffer(input) {
36
+ if (Buffer.isBuffer(input)) return input;
37
+ if (input instanceof ArrayBuffer) return Buffer.from(input);
38
+ return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
39
+ }
40
+ /**
41
+ * Load a PDF's bytes into {@link RagDocument}(s) via the OPTIONAL `pdf-parse`
42
+ * peer. The peer is resolved lazily on the FIRST call (not at import) so
43
+ * importing `@warlock.js/ai` never forces it to be installed; when it is
44
+ * absent the curated {@link PDF_PARSE_INSTALL_INSTRUCTIONS} is thrown as a
45
+ * plain `Error` (a missing optional peer is an infrastructure fault, not a
46
+ * content problem).
47
+ *
48
+ * By default the whole PDF becomes a single document carrying
49
+ * `metadata.pageCount`. With `perPage: true`, each page becomes its own
50
+ * document (`id` suffixed `#p<n>`, `metadata.page` set) so citations stay
51
+ * page-precise. Document `metadata.title` comes from the PDF info
52
+ * dictionary's `Title` (unless overridden), and `metadata.loader` is
53
+ * `"pdf"`. The output is the exact shape `index()` consumes.
54
+ *
55
+ * @example
56
+ * import { readFile } from "node:fs/promises";
57
+ * const kb = ai.rag({ embedder, store });
58
+ * await kb.index(await loadPdf(await readFile("guide.pdf"), { id: "guide" }));
59
+ *
60
+ * @example
61
+ * // One document per page for page-precise citations:
62
+ * await kb.index(await loadPdf(bytes, { id: "manual", perPage: true }));
63
+ *
64
+ * @param input - The PDF bytes (`Buffer`, `ArrayBuffer`, or `Uint8Array`).
65
+ * @param options - `perPage` plus shared `id` / `metadata` / `tags`
66
+ * ({@link LoadPdfOptions}).
67
+ * @returns A {@link RagLoaderResult} ready for `rag.index()`.
68
+ * @throws {Error} carrying {@link PDF_PARSE_INSTALL_INSTRUCTIONS} when the
69
+ * `pdf-parse` peer is not installed.
70
+ */
71
+ async function loadPdf(input, options = {}) {
72
+ await loadPdfParse();
73
+ if (!isModuleExists || !pdfParse) throw new Error(PDF_PARSE_INSTALL_INSTRUCTIONS);
74
+ const id = options.id ?? DEFAULT_ID;
75
+ if (options.perPage ?? false) return loadPerPage(input, id, options);
76
+ const parsed = await pdfParse(toBuffer(input));
77
+ const text = parsed.text.trim();
78
+ const title = parsed.info?.Title?.trim();
79
+ if (text.length === 0) return [];
80
+ return [{
81
+ id,
82
+ text,
83
+ metadata: {
84
+ source: id,
85
+ loader: "pdf",
86
+ pageCount: parsed.numpages,
87
+ ...title ? { title } : {},
88
+ ...options.metadata
89
+ },
90
+ tags: options.tags
91
+ }];
92
+ }
93
+ /**
94
+ * Per-page variant: render each page separately via `pdf-parse`'s
95
+ * `pagerender` hook, accumulating one document per non-empty page. Each
96
+ * carries `metadata.page` (1-based) and `metadata.pageCount`, and its id is
97
+ * the base id suffixed `#p<n>` so every page-document is distinctly
98
+ * identified for citation.
99
+ *
100
+ * `pdf-parse` calls `pagerender` once per page in document order and
101
+ * `await`s the returned string, so capturing each page's joined text content
102
+ * here gives reliable page boundaries the concatenated `text` lacks.
103
+ */
104
+ async function loadPerPage(input, id, options) {
105
+ const pages = [];
106
+ const parsed = await pdfParse(toBuffer(input), { pagerender: async (page) => {
107
+ const rendered = await renderPage(page);
108
+ pages.push(rendered);
109
+ return rendered;
110
+ } });
111
+ const title = parsed.info?.Title?.trim();
112
+ const docs = [];
113
+ pages.forEach((pageText, index) => {
114
+ const text = pageText.trim();
115
+ if (text.length === 0) return;
116
+ const pageNumber = index + 1;
117
+ docs.push({
118
+ id: `${id}#p${pageNumber}`,
119
+ text,
120
+ metadata: {
121
+ source: id,
122
+ loader: "pdf",
123
+ page: pageNumber,
124
+ pageCount: parsed.numpages,
125
+ ...title ? { title } : {},
126
+ ...options.metadata
127
+ },
128
+ tags: options.tags
129
+ });
130
+ });
131
+ return docs;
132
+ }
133
+ /**
134
+ * Join a single page's text-layer items in reading order, inserting a space
135
+ * between items so adjacent words do not run together. Mirrors the essence
136
+ * of `pdf-parse`'s default renderer without depending on its internals, so
137
+ * the per-page hook stays stable across `pdf-parse` versions. A page with no
138
+ * text layer (scanned image) renders to an empty string and is dropped.
139
+ */
140
+ async function renderPage(page) {
141
+ if (typeof page?.getTextContent !== "function") return "";
142
+ return (await page.getTextContent({
143
+ normalizeWhitespace: true,
144
+ disableCombineTextItems: false
145
+ })).items.map((item) => item.str).join(" ").replace(/\s+/g, " ").trim();
146
+ }
147
+
148
+ //#endregion
149
+ export { loadPdf };
150
+ //# sourceMappingURL=load-pdf.mjs.map