@raintree-technology/docpull-sdk 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @raintree-technology/docpull-sdk
2
+
3
+ TypeScript SDK for [docpull](https://github.com/raintree-technology/docpull).
4
+ It reads local context packs (the v3 pack contract) and runs the `docpull`
5
+ CLI from Node or Bun. Reading a pack never touches the network; fetching
6
+ requires the `docpull` CLI on PATH.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ bun add @raintree-technology/docpull-sdk # or: npm install @raintree-technology/docpull-sdk
12
+ ```
13
+
14
+ ## Read a pack
15
+
16
+ ```ts
17
+ import { readCorpusManifest, readDocuments, readPack } from "@raintree-technology/docpull-sdk";
18
+
19
+ const manifest = await readCorpusManifest("./packs/example");
20
+ console.log(manifest.record_count, manifest.records[0]?.url);
21
+
22
+ // Stream records one line at a time.
23
+ for await (const record of readDocuments("./packs/example")) {
24
+ console.log(record.document_id, record.token_count);
25
+ }
26
+
27
+ // Or load everything at once.
28
+ const pack = await readPack("./packs/example");
29
+ console.log(pack.documents.length);
30
+ ```
31
+
32
+ `readDocuments` prefers `documents.ndjson` and falls back to
33
+ `documents.jsonl`. Types (`CorpusManifest`, `ManifestRecord`,
34
+ `DocumentRecord`) mirror the field names written by the Python side and keep
35
+ unknown fields as forward-compatible metadata.
36
+
37
+ ## Run the CLI
38
+
39
+ ```ts
40
+ import { fetchToPack, runDocpull } from "@raintree-technology/docpull-sdk";
41
+
42
+ // docpull https://example.com/docs -o ./packs/example --budget 0
43
+ await fetchToPack("https://example.com/docs", "./packs/example", { budget: 0 });
44
+
45
+ // Any other subcommand.
46
+ const result = await runDocpull(["pack", "validate", "./packs/example"]);
47
+ console.log(result.stdout);
48
+ ```
49
+
50
+ Commands are spawned with an argument array and `shell: false`. A non-zero
51
+ exit rejects with a `DocpullCliError` that carries `exitCode` and `stderr`.
52
+
53
+ ## Development
54
+
55
+ ```bash
56
+ bun install # from the repo root
57
+ bun test # from sdk/js
58
+ bun run typecheck # tsc --noEmit
59
+ ```
60
+
61
+ ## License
62
+
63
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Helpers for running the docpull CLI from JavaScript.
3
+ *
4
+ * Commands are spawned with an argument array and `shell: false`, so no
5
+ * shell interpolation happens. A non-zero exit rejects with the captured
6
+ * stderr in the error.
7
+ */
8
+ export interface SpawnedProcessStream {
9
+ on(event: "data", listener: (chunk: unknown) => void): void;
10
+ }
11
+ /** Minimal child process surface used by `runDocpull`; injectable in tests. */
12
+ export interface SpawnedProcess {
13
+ stdout: SpawnedProcessStream | null;
14
+ stderr: SpawnedProcessStream | null;
15
+ on(event: "error", listener: (error: Error) => void): void;
16
+ on(event: "close", listener: (code: number | null) => void): void;
17
+ }
18
+ export interface SpawnCallOptions {
19
+ cwd?: string;
20
+ env?: Record<string, string | undefined>;
21
+ shell: false;
22
+ }
23
+ export type SpawnFunction = (command: string, args: readonly string[], options: SpawnCallOptions) => SpawnedProcess;
24
+ export interface RunDocpullOptions {
25
+ /** Executable to run. Defaults to `docpull` resolved from PATH. */
26
+ bin?: string;
27
+ cwd?: string;
28
+ env?: Record<string, string | undefined>;
29
+ /** Spawn implementation override, used by tests to avoid real processes. */
30
+ spawnFn?: SpawnFunction;
31
+ }
32
+ export interface RunDocpullResult {
33
+ command: string;
34
+ args: string[];
35
+ stdout: string;
36
+ stderr: string;
37
+ exitCode: number;
38
+ }
39
+ export declare class DocpullCliError extends Error {
40
+ readonly exitCode: number | null;
41
+ readonly stderr: string;
42
+ constructor(message: string, exitCode: number | null, stderr: string);
43
+ }
44
+ /** Run the docpull CLI with the given arguments and capture its output. */
45
+ export declare function runDocpull(args: readonly string[], options?: RunDocpullOptions): Promise<RunDocpullResult>;
46
+ export interface FetchToPackOptions extends RunDocpullOptions {
47
+ /** Value for `--budget`; pass 0 to disable the network budget. */
48
+ budget?: number;
49
+ /** Extra CLI arguments appended after the generated ones. */
50
+ extraArgs?: readonly string[];
51
+ }
52
+ /** Fetch a URL into a pack directory: `docpull <url> -o <outputDir>`. */
53
+ export declare function fetchToPack(url: string, outputDir: string, options?: FetchToPackOptions): Promise<RunDocpullResult>;
package/dist/cli.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Helpers for running the docpull CLI from JavaScript.
3
+ *
4
+ * Commands are spawned with an argument array and `shell: false`, so no
5
+ * shell interpolation happens. A non-zero exit rejects with the captured
6
+ * stderr in the error.
7
+ */
8
+ import { spawn } from "node:child_process";
9
+ export class DocpullCliError extends Error {
10
+ exitCode;
11
+ stderr;
12
+ constructor(message, exitCode, stderr) {
13
+ super(message);
14
+ this.name = "DocpullCliError";
15
+ this.exitCode = exitCode;
16
+ this.stderr = stderr;
17
+ }
18
+ }
19
+ const defaultSpawn = (command, args, options) => {
20
+ const child = spawn(command, [...args], {
21
+ cwd: options.cwd,
22
+ env: options.env,
23
+ shell: false,
24
+ });
25
+ return {
26
+ stdout: child.stdout,
27
+ stderr: child.stderr,
28
+ on(event, listener) {
29
+ child.on(event, listener);
30
+ },
31
+ };
32
+ };
33
+ /** Run the docpull CLI with the given arguments and capture its output. */
34
+ export function runDocpull(args, options = {}) {
35
+ const command = options.bin ?? "docpull";
36
+ const spawnFn = options.spawnFn ?? defaultSpawn;
37
+ const argv = [...args];
38
+ return new Promise((resolve, reject) => {
39
+ const child = spawnFn(command, argv, { cwd: options.cwd, env: options.env, shell: false });
40
+ let stdout = "";
41
+ let stderr = "";
42
+ child.stdout?.on("data", (chunk) => {
43
+ stdout += String(chunk);
44
+ });
45
+ child.stderr?.on("data", (chunk) => {
46
+ stderr += String(chunk);
47
+ });
48
+ child.on("error", (error) => {
49
+ reject(new DocpullCliError(`Failed to run ${command}: ${error.message}`, null, stderr));
50
+ });
51
+ child.on("close", (code) => {
52
+ if (code === 0) {
53
+ resolve({ command, args: argv, stdout, stderr, exitCode: 0 });
54
+ return;
55
+ }
56
+ const detail = stderr.trim();
57
+ reject(new DocpullCliError(`${command} exited with code ${String(code)}${detail ? `: ${detail}` : ""}`, code, stderr));
58
+ });
59
+ });
60
+ }
61
+ /** Fetch a URL into a pack directory: `docpull <url> -o <outputDir>`. */
62
+ export function fetchToPack(url, outputDir, options = {}) {
63
+ if (url.trim().length === 0) {
64
+ throw new Error("fetchToPack requires a non-empty url");
65
+ }
66
+ if (outputDir.trim().length === 0) {
67
+ throw new Error("fetchToPack requires a non-empty outputDir");
68
+ }
69
+ const args = [url, "-o", outputDir];
70
+ if (options.budget !== undefined) {
71
+ args.push("--budget", String(options.budget));
72
+ }
73
+ if (options.extraArgs !== undefined) {
74
+ args.push(...options.extraArgs);
75
+ }
76
+ return runDocpull(args, options);
77
+ }
@@ -0,0 +1,4 @@
1
+ export { readCorpusManifest, readDocuments, readPack } from "./pack.js";
2
+ export type { CorpusManifest, DocumentRecord, ManifestRecord, Pack } from "./pack.js";
3
+ export { DocpullCliError, fetchToPack, runDocpull } from "./cli.js";
4
+ export type { FetchToPackOptions, RunDocpullOptions, RunDocpullResult, SpawnCallOptions, SpawnedProcess, SpawnedProcessStream, SpawnFunction, } from "./cli.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { readCorpusManifest, readDocuments, readPack } from "./pack.js";
2
+ export { DocpullCliError, fetchToPack, runDocpull } from "./cli.js";
package/dist/pack.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Typed readers for the docpull context pack contract (v3).
3
+ *
4
+ * A pack directory contains `corpus.manifest.json` (the stable source map)
5
+ * and `documents.ndjson` or `documents.jsonl` (one document record per
6
+ * line). Field names mirror the Python `DocumentRecord` model and the
7
+ * manifest writer; both drop `null` fields on write, so most fields are
8
+ * optional here. Unknown fields are forward-compatible metadata.
9
+ */
10
+ /** One entry of `corpus.manifest.json` `records[]`. */
11
+ export interface ManifestRecord {
12
+ schema_version?: number;
13
+ document_id: string;
14
+ url: string;
15
+ title?: string | null;
16
+ content_hash: string;
17
+ fetched_at?: string;
18
+ rendered_at?: string | null;
19
+ content_type?: string;
20
+ mime_type?: string;
21
+ token_count?: number | null;
22
+ route?: Record<string, unknown>;
23
+ rights?: Record<string, unknown>;
24
+ source_type?: string;
25
+ source_citation_id?: string;
26
+ record_citation_id?: string;
27
+ chunk_index?: number;
28
+ chunk_id?: string;
29
+ chunk_heading?: string;
30
+ output_path?: string;
31
+ [key: string]: unknown;
32
+ }
33
+ /** Shape of `corpus.manifest.json`. */
34
+ export interface CorpusManifest {
35
+ schema_version: number;
36
+ output_contract_version?: number;
37
+ generated_at?: string;
38
+ output_format?: string;
39
+ run?: Record<string, unknown> | null;
40
+ document_count: number;
41
+ record_count: number;
42
+ chunk_count?: number;
43
+ archive?: Record<string, unknown>;
44
+ records: ManifestRecord[];
45
+ [key: string]: unknown;
46
+ }
47
+ /** One line of `documents.ndjson` / `documents.jsonl`. */
48
+ export interface DocumentRecord {
49
+ schema_version?: number;
50
+ document_id: string;
51
+ url: string;
52
+ title?: string | null;
53
+ content: string;
54
+ metadata?: Record<string, unknown>;
55
+ extraction?: Record<string, unknown>;
56
+ source_type?: string | null;
57
+ fetched_at?: string;
58
+ rendered_at?: string | null;
59
+ content_type?: string;
60
+ mime_type?: string;
61
+ content_hash: string;
62
+ run?: Record<string, unknown> | null;
63
+ route?: Record<string, unknown>;
64
+ rights?: Record<string, unknown>;
65
+ source_citation_id?: string | null;
66
+ record_citation_id?: string | null;
67
+ chunk_index?: number | null;
68
+ chunk_id?: string | null;
69
+ chunk_heading?: string | null;
70
+ token_count?: number | null;
71
+ [key: string]: unknown;
72
+ }
73
+ /** Manifest plus materialized document records for one pack directory. */
74
+ export interface Pack {
75
+ packDir: string;
76
+ manifest: CorpusManifest;
77
+ documents: DocumentRecord[];
78
+ }
79
+ /** Read and parse `corpus.manifest.json` from a pack directory. */
80
+ export declare function readCorpusManifest(packDir: string): Promise<CorpusManifest>;
81
+ /** Iterate document records from `documents.ndjson` or `documents.jsonl`. */
82
+ export declare function readDocuments(packDir: string): AsyncGenerator<DocumentRecord, void, undefined>;
83
+ /** Read a whole pack: manifest plus every document record. */
84
+ export declare function readPack(packDir: string): Promise<Pack>;
package/dist/pack.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Typed readers for the docpull context pack contract (v3).
3
+ *
4
+ * A pack directory contains `corpus.manifest.json` (the stable source map)
5
+ * and `documents.ndjson` or `documents.jsonl` (one document record per
6
+ * line). Field names mirror the Python `DocumentRecord` model and the
7
+ * manifest writer; both drop `null` fields on write, so most fields are
8
+ * optional here. Unknown fields are forward-compatible metadata.
9
+ */
10
+ import { createReadStream } from "node:fs";
11
+ import { access, readFile } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ const DOCUMENT_FILES = ["documents.ndjson", "documents.jsonl"];
14
+ /** Read and parse `corpus.manifest.json` from a pack directory. */
15
+ export async function readCorpusManifest(packDir) {
16
+ const manifestPath = join(packDir, "corpus.manifest.json");
17
+ let raw;
18
+ try {
19
+ raw = await readFile(manifestPath, "utf8");
20
+ }
21
+ catch (error) {
22
+ throw new Error(`Cannot read corpus manifest at ${manifestPath}: ${errorMessage(error)}`);
23
+ }
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(raw);
27
+ }
28
+ catch (error) {
29
+ throw new Error(`Invalid JSON in ${manifestPath}: ${errorMessage(error)}`);
30
+ }
31
+ if (!isRecord(parsed) || !Array.isArray(parsed["records"])) {
32
+ throw new Error(`Invalid corpus manifest at ${manifestPath}: expected an object with a records array`);
33
+ }
34
+ return parsed;
35
+ }
36
+ /** Iterate document records from `documents.ndjson` or `documents.jsonl`. */
37
+ export async function* readDocuments(packDir) {
38
+ const documentsPath = await resolveDocumentsPath(packDir);
39
+ let lineNumber = 0;
40
+ for await (const line of readLines(documentsPath)) {
41
+ lineNumber += 1;
42
+ if (line.trim().length === 0) {
43
+ continue;
44
+ }
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(line);
48
+ }
49
+ catch (error) {
50
+ throw new Error(`Invalid NDJSON in ${documentsPath} line ${lineNumber}: ${errorMessage(error)}`);
51
+ }
52
+ if (!isRecord(parsed)) {
53
+ throw new Error(`Invalid NDJSON in ${documentsPath} line ${lineNumber}: expected an object`);
54
+ }
55
+ yield parsed;
56
+ }
57
+ }
58
+ /** Read a whole pack: manifest plus every document record. */
59
+ export async function readPack(packDir) {
60
+ const manifest = await readCorpusManifest(packDir);
61
+ const documents = [];
62
+ for await (const record of readDocuments(packDir)) {
63
+ documents.push(record);
64
+ }
65
+ return { packDir, manifest, documents };
66
+ }
67
+ async function resolveDocumentsPath(packDir) {
68
+ for (const name of DOCUMENT_FILES) {
69
+ const candidate = join(packDir, name);
70
+ try {
71
+ await access(candidate);
72
+ return candidate;
73
+ }
74
+ catch {
75
+ // Try the next candidate file name.
76
+ }
77
+ }
78
+ throw new Error(`No documents file found in ${packDir}; expected one of: ${DOCUMENT_FILES.join(", ")}`);
79
+ }
80
+ async function* readLines(path) {
81
+ const stream = createReadStream(path, { encoding: "utf8" });
82
+ let buffered = "";
83
+ for await (const chunk of stream) {
84
+ buffered += String(chunk);
85
+ let newlineIndex = buffered.indexOf("\n");
86
+ while (newlineIndex !== -1) {
87
+ yield buffered.slice(0, newlineIndex).replace(/\r$/, "");
88
+ buffered = buffered.slice(newlineIndex + 1);
89
+ newlineIndex = buffered.indexOf("\n");
90
+ }
91
+ }
92
+ if (buffered.length > 0) {
93
+ yield buffered.replace(/\r$/, "");
94
+ }
95
+ }
96
+ function isRecord(value) {
97
+ return typeof value === "object" && value !== null && !Array.isArray(value);
98
+ }
99
+ function errorMessage(error) {
100
+ return error instanceof Error ? error.message : String(error);
101
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@raintree-technology/docpull-sdk",
3
+ "version": "6.5.0",
4
+ "description": "TypeScript SDK for reading docpull context packs and running the docpull CLI",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "test": "bun test",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/raintree-technology/docpull.git",
27
+ "directory": "sdk/js"
28
+ },
29
+ "author": "Raintree Technology",
30
+ "keywords": [
31
+ "docpull",
32
+ "context-pack",
33
+ "rag",
34
+ "sdk"
35
+ ],
36
+ "devDependencies": {
37
+ "@types/node": "^25.9.1",
38
+ "typescript": "^6.0.3"
39
+ },
40
+ "packageManager": "bun@1.3.11",
41
+ "engines": {
42
+ "node": "24.x"
43
+ }
44
+ }