@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 +63 -0
- package/dist/cli.d.ts +53 -0
- package/dist/cli.js +77 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/pack.d.ts +84 -0
- package/dist/pack.js +101 -0
- package/package.json +44 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
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
|
+
}
|