@xberg-io/opencode-xberg 1.3.6 → 1.3.8

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 (40) hide show
  1. package/.opencode/ai-rulez-bundle.json +51 -0
  2. package/.opencode/ai-rulez-content.js +167 -0
  3. package/.opencode/commands/xberg-extract.md +14 -0
  4. package/.opencode/commands/xberg-ocr.md +14 -0
  5. package/.opencode/plugins/xberg.js +75 -62
  6. package/.opencode/skills/batch-extraction/SKILL.md +168 -0
  7. package/.opencode/skills/chunking/SKILL.md +163 -0
  8. package/.opencode/skills/extracting-keywords/SKILL.md +171 -0
  9. package/.opencode/skills/extracting-tables/SKILL.md +156 -0
  10. package/.opencode/skills/extracting-with-ocr/SKILL.md +130 -0
  11. package/.opencode/skills/picking-a-format/SKILL.md +104 -0
  12. package/.opencode/skills/xberg/SKILL.md +427 -0
  13. package/.opencode/skills/xberg/references/advanced-features.md +628 -0
  14. package/.opencode/skills/xberg/references/cli-reference.md +462 -0
  15. package/.opencode/skills/xberg/references/configuration.md +587 -0
  16. package/.opencode/skills/xberg/references/nodejs-api.md +485 -0
  17. package/.opencode/skills/xberg/references/other-bindings.md +345 -0
  18. package/.opencode/skills/xberg/references/python-api.md +657 -0
  19. package/.opencode/skills/xberg/references/rust-api.md +412 -0
  20. package/.opencode/skills/xberg/references/supported-formats.md +252 -0
  21. package/.pi/prompts/xberg-extract.md +14 -0
  22. package/.pi/prompts/xberg-ocr.md +14 -0
  23. package/.pi/skills/batch-extraction/SKILL.md +168 -0
  24. package/.pi/skills/chunking/SKILL.md +163 -0
  25. package/.pi/skills/extracting-keywords/SKILL.md +171 -0
  26. package/.pi/skills/extracting-tables/SKILL.md +156 -0
  27. package/.pi/skills/extracting-with-ocr/SKILL.md +130 -0
  28. package/.pi/skills/picking-a-format/SKILL.md +104 -0
  29. package/.pi/skills/xberg/SKILL.md +427 -0
  30. package/.pi/skills/xberg/references/advanced-features.md +628 -0
  31. package/.pi/skills/xberg/references/cli-reference.md +462 -0
  32. package/.pi/skills/xberg/references/configuration.md +587 -0
  33. package/.pi/skills/xberg/references/nodejs-api.md +485 -0
  34. package/.pi/skills/xberg/references/other-bindings.md +345 -0
  35. package/.pi/skills/xberg/references/python-api.md +657 -0
  36. package/.pi/skills/xberg/references/rust-api.md +412 -0
  37. package/.pi/skills/xberg/references/supported-formats.md +252 -0
  38. package/README.md +40 -0
  39. package/package.json +69 -16
  40. package/scripts/mcp-launch.sh +188 -0
@@ -0,0 +1,51 @@
1
+ {
2
+ "mcp": {
3
+ "xberg": {
4
+ "type": "local",
5
+ "command": [
6
+ "${PLUGIN_ROOT}/scripts/mcp-launch.sh",
7
+ "mcp",
8
+ "--transport",
9
+ "stdio"
10
+ ]
11
+ }
12
+ },
13
+ "skills": {
14
+ "batch-extraction": {
15
+ "name": "batch-extraction",
16
+ "description": "Use when extracting from many files at once with shared config, bounded parallelism, per-file overrides, and error recovery. Covers the `batch` command, `--file-configs`, `--max-concurrent`, and output layout."
17
+ },
18
+ "chunking": {
19
+ "name": "chunking",
20
+ "description": "Use when splitting extracted text into chunks for LLM context windows or RAG ingestion. Covers chunk size, overlap, markdown/yaml/semantic chunkers, tokenizer-based sizing, and the standalone `chunk` command."
21
+ },
22
+ "extracting-keywords": {
23
+ "name": "extracting-keywords",
24
+ "description": "Use when extracting keywords (YAKE/RAKE) from documents — and, secondarily, when detecting document language or generating embeddings for RAG and search. Covers the keyword config (and its feature gating), `--detect-language`, and the standalone `embed` command with real flags."
25
+ },
26
+ "extracting-tables": {
27
+ "name": "extracting-tables",
28
+ "description": "Use when extracting tabular data from PDFs, spreadsheets, or images. Covers layout-aware table detection, table model selection, output formats (markdown / JSON cells), and known limits."
29
+ },
30
+ "extracting-with-ocr": {
31
+ "name": "extracting-with-ocr",
32
+ "description": "Use when extracting text from scanned PDFs, photographed pages, or images that have no embedded text layer. Covers OCR backends, language packs, force-OCR, and performance tuning."
33
+ },
34
+ "picking-a-format": {
35
+ "name": "picking-a-format",
36
+ "description": "Use when choosing an output format for extracted documents — plain text, markdown, djot, HTML, JSON, or DocTags. Maps consumer (LLM, parser, archive) to the right `--format` / `--content-format` pair."
37
+ },
38
+ "xberg": {
39
+ "name": "xberg",
40
+ "description": "Extract text, tables, metadata, and images from 107 document formats (PDF, Office, images, HTML, email, archives, academic) using Xberg. Use when writing code that calls Xberg APIs in Python, Node.js/TypeScript, Rust, or CLI. Covers installation, extraction (sync/async), configuration (OCR, chunking, output format), batch processing, error handling, and plugins."
41
+ }
42
+ },
43
+ "commands": {
44
+ "xberg-extract": {
45
+ "description": "Extract a document locally with Xberg"
46
+ },
47
+ "xberg-ocr": {
48
+ "description": "Extract scanned documents with OCR"
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,167 @@
1
+ // AI-RULEZ :: GENERATED FILE — DO NOT EDIT
2
+ // Content-Hash: blake3:822c510deb007e4f72877ad42f29918d0499f210ae363e82d465a0f0331ab294
3
+ // Source-Hash: blake3:3fca38eb564d0ed9c9194d070fd8dec96de727edc92e82954cb4b9af52cddbaf
4
+ // Schema-Version: v1
5
+
6
+ // Generated by ai-rulez. Registers the skills, commands, agents, and MCP servers bundled with
7
+ // this plugin in OpenCode v2. OpenCode only scans its own config directories, so a plugin installed from
8
+ // npm has to register its bundled content itself.
9
+ import { readdir, readFile } from "node:fs/promises";
10
+ import path from "node:path";
11
+
12
+ const root = import.meta.dirname;
13
+
14
+ // Returns the text after a leading YAML frontmatter block. The frontmatter fields the
15
+ // helper needs (name, description) are resolved when the bundle is generated and read
16
+ // from ai-rulez-bundle.json, so no YAML is parsed here.
17
+ function body(text) {
18
+ const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/u.exec(text);
19
+ return match ? match[1] : text;
20
+ }
21
+
22
+ function skip(kind, name, error) {
23
+ console.warn(`ai-rulez: skipping bundled ${kind} ${name}: ${error?.message ?? error}`);
24
+ }
25
+
26
+ async function entries(dir) {
27
+ try {
28
+ return (await readdir(path.join(root, dir), { withFileTypes: true })).toSorted((a, b) =>
29
+ a.name.localeCompare(b.name),
30
+ );
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ async function readBundle() {
37
+ try {
38
+ return JSON.parse(await readFile(path.join(root, "ai-rulez-bundle.json"), "utf8"));
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ // Plugin install directory: the parent of the .opencode directory this file lives in.
45
+ const pluginRoot = path.dirname(root);
46
+
47
+ // Resolves ${PLUGIN_ROOT} to the install directory and ${NAME} to process.env at runtime,
48
+ // so no install path or secret is baked into the package.
49
+ function expand(value) {
50
+ return value.replaceAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/gu, (_, name) =>
51
+ name === "PLUGIN_ROOT" ? pluginRoot : (process.env[name] ?? ""),
52
+ );
53
+ }
54
+
55
+ function mcpServer(server) {
56
+ if (server.type === "remote") {
57
+ const entry = { type: "remote", url: expand(server.url) };
58
+ if (server.disabled) entry.disabled = true;
59
+ return entry;
60
+ }
61
+ const entry = { type: "local", command: server.command.map(expand) };
62
+ if (server.disabled) entry.disabled = true;
63
+ if (server.environment) {
64
+ entry.environment = Object.fromEntries(
65
+ Object.entries(server.environment).map(([key, value]) => [key, expand(value)]),
66
+ );
67
+ }
68
+ return entry;
69
+ }
70
+
71
+ async function load() {
72
+ const bundle = await readBundle();
73
+ const skills = [];
74
+ for (const entry of await entries("skills")) {
75
+ if (!entry.isDirectory()) continue;
76
+ const file = path.join(root, "skills", entry.name, "SKILL.md");
77
+ try {
78
+ const info = bundle.skills?.[entry.name] ?? {};
79
+ skills.push({
80
+ id: entry.name,
81
+ name: info.name || entry.name,
82
+ description: info.description,
83
+ path: file,
84
+ content: body(await readFile(file, "utf8")),
85
+ });
86
+ } catch (error) {
87
+ skip("skill", entry.name, error);
88
+ }
89
+ }
90
+ const commands = [];
91
+ for (const entry of await entries("commands")) {
92
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
93
+ const name = entry.name.slice(0, -3);
94
+ try {
95
+ const template = body(await readFile(path.join(root, "commands", entry.name), "utf8"));
96
+ commands.push({ name, description: bundle.commands?.[name]?.description, template });
97
+ } catch (error) {
98
+ skip("command", name, error);
99
+ }
100
+ }
101
+ const agents = [];
102
+ for (const entry of await entries("agents")) {
103
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
104
+ const id = entry.name.slice(0, -3);
105
+ try {
106
+ // The bundle holds settings resolved at generation time (the model in particular).
107
+ const system = body(await readFile(path.join(root, "agents", entry.name), "utf8"));
108
+ agents.push({ id, system: system.trim(), settings: bundle.agents?.[id] ?? {} });
109
+ } catch (error) {
110
+ skip("agent", id, error);
111
+ }
112
+ }
113
+ return { skills, commands, agents, mcp: bundle.mcp ?? {} };
114
+ }
115
+
116
+ export async function registerBundledContent(ctx) {
117
+ const { skills, commands, agents, mcp } = await load();
118
+ if (skills.length > 0) {
119
+ await ctx.skill.transform((editor) => {
120
+ for (const skill of skills) if (!editor.get(skill.id)) editor.add(skill);
121
+ });
122
+ }
123
+ if (commands.length > 0) {
124
+ await ctx.command.transform((editor) => {
125
+ for (const command of commands) {
126
+ editor.add({
127
+ name: command.name,
128
+ description: command.description,
129
+ execute: async ({ sessionID, prompt, delivery }) => {
130
+ await ctx.session.prompt({
131
+ ...prompt,
132
+ sessionID,
133
+ text: command.template.replaceAll("$ARGUMENTS", () => String(prompt.text ?? "")),
134
+ delivery,
135
+ });
136
+ },
137
+ });
138
+ }
139
+ });
140
+ }
141
+ if (agents.length > 0) {
142
+ await ctx.agent.transform((editor) => {
143
+ for (const agent of agents) {
144
+ editor.update(agent.id, (item) => {
145
+ const settings = agent.settings;
146
+ if (settings.description) item.description = settings.description;
147
+ if (["subagent", "primary", "all"].includes(settings.mode)) item.mode = settings.mode;
148
+ if (typeof settings.hidden === "boolean") item.hidden = settings.hidden;
149
+ if (typeof settings.model === "string" && settings.model.includes("/")) {
150
+ const split = settings.model.indexOf("/");
151
+ item.model = { providerID: settings.model.slice(0, split), id: settings.model.slice(split + 1) };
152
+ if (settings.variant) item.model.variant = settings.variant;
153
+ }
154
+ for (const key of ["temperature", "top_p"]) {
155
+ if (typeof settings[key] === "number") item.request.body[key] = settings[key];
156
+ }
157
+ if (agent.system) item.system = agent.system;
158
+ });
159
+ }
160
+ });
161
+ }
162
+ if (Object.keys(mcp).length > 0) {
163
+ await ctx.mcp.transform((editor) => {
164
+ for (const [name, server] of Object.entries(mcp)) editor.set(name, mcpServer(server));
165
+ });
166
+ }
167
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ description: "Extract a document locally with Xberg"
3
+ ---
4
+
5
+ <!--
6
+ AI-RULEZ :: GENERATED FILE — DO NOT EDIT
7
+ Content-Hash: blake3:519efaa10fd8e92073089cdc9bdebf7151a312a3f280d8d70987603bf31d9113
8
+ Source-Hash: blake3:3fca38eb564d0ed9c9194d070fd8dec96de727edc92e82954cb4b9af52cddbaf
9
+ Schema-Version: v1
10
+ -->
11
+
12
+ Extract this document: $ARGUMENTS
13
+
14
+ Read the xberg and picking-a-format skills. Use `xberg extract <file> --format json` and choose --content-format for the consumer. Preserve input paths as separate arguments. Report extraction errors and content omissions.
@@ -0,0 +1,14 @@
1
+ ---
2
+ description: "Extract scanned documents with OCR"
3
+ ---
4
+
5
+ <!--
6
+ AI-RULEZ :: GENERATED FILE — DO NOT EDIT
7
+ Content-Hash: blake3:d3a65f45804e9541bb33d628e4a7d7b48f806adb438e1fc8b15c92baef538908
8
+ Source-Hash: blake3:3fca38eb564d0ed9c9194d070fd8dec96de727edc92e82954cb4b9af52cddbaf
9
+ Schema-Version: v1
10
+ -->
11
+
12
+ OCR this document: $ARGUMENTS
13
+
14
+ Read the extracting-with-ocr skill. Confirm the document needs OCR and language packs are installed. Use `xberg extract <file> --force-ocr=true` when forced OCR is appropriate. Preserve paths as separate arguments and report OCR failures.
@@ -1,19 +1,10 @@
1
1
  // AI-RULEZ :: GENERATED FILE — DO NOT EDIT
2
- // Content-Hash: blake3:0c4b20b2428ca03b76e22085bdcc856fbac85091cd3f6d157655d6c25c02714f
3
- // Source-Hash: blake3:0a63e6793dfbd7671ca2cfec5688efa8523d2e4087553f9f6503f2d74cef8b78
2
+ // Content-Hash: blake3:3fe1a33d721dfa8b49134cbe88659999cf9e481c558eb8e4b5ac81bc672c88db
3
+ // Source-Hash: blake3:3fca38eb564d0ed9c9194d070fd8dec96de727edc92e82954cb4b9af52cddbaf
4
4
  // Schema-Version: v1
5
5
 
6
- import { tool } from "@opencode-ai/plugin";
7
6
  import { spawn } from "node:child_process";
8
-
9
- const schema = tool.schema;
10
-
11
- const wireFormat = schema.enum(["text", "json", "toon"]).default("json").describe("CLI output format.");
12
-
13
- const contentFormat = schema
14
- .enum(["plain", "markdown", "djot", "html", "json"])
15
- .optional()
16
- .describe("Document content rendering format.");
7
+ import { registerBundledContent } from "../ai-rulez-content.js";
17
8
 
18
9
  function hasValue(value) {
19
10
  return value !== undefined && value !== null && value !== "";
@@ -37,14 +28,12 @@ function validateJson(value, name) {
37
28
  }
38
29
  }
39
30
 
40
- function runCli(args, context) {
41
- const directory = context?.directory ?? context?.worktree ?? process.cwd();
42
-
31
+ function runCli(args, context, directory) {
43
32
  return new Promise((resolve, reject) => {
44
33
  const child = spawn("xberg", args, {
45
34
  cwd: directory,
46
35
  env: process.env,
47
- signal: context?.abort,
36
+ signal: context?.signal,
48
37
  stdio: ["ignore", "pipe", "pipe"],
49
38
  });
50
39
 
@@ -56,8 +45,7 @@ function runCli(args, context) {
56
45
  child.on("error", (error) => {
57
46
  if (error.code === "ENOENT") {
58
47
  resolve({
59
- title: "xberg CLI not found",
60
- output:
48
+ content:
61
49
  "Install the xberg CLI with `brew install xberg-io/tap/xberg`, or run it via `npx -y @xberg-io/xberg-cli` / `uvx --from xberg-cli xberg`.",
62
50
  metadata: { exitCode: 127, command: "xberg", subcommand: args[0] },
63
51
  });
@@ -71,8 +59,7 @@ function runCli(args, context) {
71
59
  const output = [stdoutText, stderrText && `stderr:\n${stderrText}`].filter(Boolean).join("\n\n");
72
60
 
73
61
  resolve({
74
- title: exitCode === 0 ? `xberg ${args[0]}` : `xberg ${args[0]} failed`,
75
- output: output || "(no output)",
62
+ content: output || "(no output)",
76
63
  metadata: {
77
64
  exitCode,
78
65
  signal,
@@ -84,49 +71,75 @@ function runCli(args, context) {
84
71
  });
85
72
  }
86
73
 
87
- export const XbergPlugin = () =>
88
- Promise.resolve({
89
- tool: {
90
- xberg_extract: tool({
91
- description: "Extract text, tables, metadata, and images from a local document with the xberg CLI.",
92
- args: {
93
- path: schema.string().min(1).describe("Path to the local document."),
94
- format: wireFormat,
95
- content_format: contentFormat,
96
- mime_type: schema.string().min(1).optional().describe("Optional MIME type hint."),
97
- config_json: schema.string().min(2).optional().describe("Optional ExtractionConfig JSON."),
98
- },
99
- async execute(args, context) {
100
- validateJson(args.config_json, "config_json");
74
+ const wireFormat = { type: "string", enum: ["text", "json", "toon"], default: "json" };
75
+ const contentFormat = { type: "string", enum: ["plain", "markdown", "djot", "html", "json"] };
76
+
77
+ function extractTool(directory) {
78
+ return {
79
+ name: "xberg_extract",
80
+ description: "Extract text, tables, metadata, and images from a local document with the xberg CLI.",
81
+ input: {
82
+ type: "object",
83
+ properties: {
84
+ path: { type: "string", minLength: 1 },
85
+ format: wireFormat,
86
+ content_format: contentFormat,
87
+ mime_type: { type: "string", minLength: 1 },
88
+ config_json: { type: "string", minLength: 2 },
89
+ },
90
+ required: ["path"],
91
+ additionalProperties: false,
92
+ },
93
+ execute(args, context) {
94
+ validateJson(args.config_json, "config_json");
95
+ const cliArgs = ["extract", args.path, "--format", args.format ?? "json"];
96
+ pushOption(cliArgs, "--content-format", args.content_format);
97
+ pushOption(cliArgs, "--mime-type", args.mime_type);
98
+ pushOption(cliArgs, "--config-json", args.config_json);
99
+ return runCli(cliArgs, context, directory);
100
+ },
101
+ };
102
+ }
101
103
 
102
- const cliArgs = ["extract", args.path, "--format", args.format];
103
- pushOption(cliArgs, "--content-format", args.content_format);
104
- pushOption(cliArgs, "--mime-type", args.mime_type);
105
- pushOption(cliArgs, "--config-json", args.config_json);
104
+ function detectTool(directory) {
105
+ return {
106
+ name: "xberg_detect",
107
+ description: "Detect the MIME type for a local file with the xberg CLI.",
108
+ input: {
109
+ type: "object",
110
+ properties: { path: { type: "string", minLength: 1 }, format: wireFormat },
111
+ required: ["path"],
112
+ additionalProperties: false,
113
+ },
114
+ execute(args, context) {
115
+ return runCli(["detect", args.path, "--format", args.format ?? "json"], context, directory);
116
+ },
117
+ };
118
+ }
106
119
 
107
- return await runCli(cliArgs, context);
108
- },
109
- }),
110
- xberg_detect: tool({
111
- description: "Detect the MIME type for a local file with the xberg CLI.",
112
- args: {
113
- path: schema.string().min(1).describe("Path to the local file."),
114
- format: wireFormat,
115
- },
116
- async execute(args, context) {
117
- return await runCli(["detect", args.path, "--format", args.format], context);
118
- },
119
- }),
120
- xberg_formats: tool({
121
- description: "List document formats supported by the xberg CLI.",
122
- args: {
123
- format: wireFormat,
124
- },
125
- async execute(args, context) {
126
- return await runCli(["formats", "--format", args.format], context);
127
- },
128
- }),
120
+ function formatsTool(directory) {
121
+ return {
122
+ name: "xberg_formats",
123
+ description: "List document formats supported by the xberg CLI.",
124
+ input: { type: "object", properties: { format: wireFormat }, additionalProperties: false },
125
+ execute(args, context) {
126
+ return runCli(["formats", "--format", args.format ?? "json"], context, directory);
129
127
  },
130
- });
128
+ };
129
+ }
131
130
 
132
- export default XbergPlugin;
131
+ export default {
132
+ id: "xberg",
133
+ async setup(ctx) {
134
+ await registerBundledContent(ctx);
135
+ await ctx.tool.transform((editor) => {
136
+ for (const tool of [
137
+ extractTool(ctx.location.directory),
138
+ detectTool(ctx.location.directory),
139
+ formatsTool(ctx.location.directory),
140
+ ]) {
141
+ editor.add(tool);
142
+ }
143
+ });
144
+ },
145
+ };
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: batch-extraction
3
+ description: Use when extracting from many files at once with shared config, bounded parallelism, per-file overrides, and error recovery. Covers the `batch` command, `--file-configs`, `--max-concurrent`, and output layout.
4
+ ---
5
+
6
+ <!--
7
+ AI-RULEZ :: GENERATED FILE — DO NOT EDIT
8
+ Content-Hash: blake3:3a70b47dd77fcd3f37e83bed3d32b1ad1ea9d6c1642ae8e0ff7b09b55939858a
9
+ Source-Hash: blake3:3fca38eb564d0ed9c9194d070fd8dec96de727edc92e82954cb4b9af52cddbaf
10
+ Schema-Version: v1
11
+ -->
12
+
13
+ # Batch extraction
14
+
15
+ Use this when processing a directory or glob of documents in one pass.
16
+ `xberg batch` shares one extraction config across every file, runs
17
+ extractions concurrently, and returns one structured array — failures on
18
+ individual files do not abort the run.
19
+
20
+ ## Basic usage
21
+
22
+ ```bash
23
+ # Glob expands to many paths; results come back as a JSON array (default)
24
+ xberg batch *.pdf
25
+
26
+ # Mixed formats, markdown content for LLM ingestion
27
+ xberg batch docs/*.docx --content-format markdown
28
+
29
+ # Recurse with the shell, then extract
30
+ xberg batch $(find ./corpus -name '*.pdf')
31
+ ```
32
+
33
+ `batch` defaults to `--format json` (vs `--format text` for single
34
+ `extract`). Each array entry is a full extraction result, so downstream
35
+ code can index by position into the input path list.
36
+
37
+ ```bash
38
+ xberg batch reports/*.pdf \
39
+ | jq '.[] | {chars: (.content | length), mime: .mime_type}'
40
+ ```
41
+
42
+ ## Parallelism
43
+
44
+ `--max-concurrent` caps how many files extract at once. When omitted, the scheduler derives document concurrency from the total thread budget. Lower it on memory-constrained hosts or when OCR/ML
45
+ models are active, since each in-flight extraction holds its own buffers.
46
+ Layout-heavy batches are further limited (1 concurrent extraction for
47
+ all-PDF-layout batches, 2 for mixed layout):
48
+
49
+ ```bash
50
+ # Cap at 4 concurrent extractions
51
+ xberg batch scans/*.pdf --ocr true --max-concurrent 4
52
+ ```
53
+
54
+ `--max-threads` additionally caps *total* internal threads (Rayon, ONNX
55
+ intra-op, the batch semaphore) for tightly constrained environments:
56
+
57
+ ```bash
58
+ xberg batch *.pdf --max-concurrent 2 --max-threads 4
59
+ ```
60
+
61
+ ## Per-file config overrides
62
+
63
+ A single shared config does not always fit. `--file-configs` points at a
64
+ JSON file mapping each path to its own override object, merged on top of
65
+ the shared config for that file only:
66
+
67
+ ```json
68
+ {
69
+ "scan.pdf": { "force_ocr": true },
70
+ "report.pdf": { "output_format": "markdown" },
71
+ "data.xlsx": { "output_format": "json" }
72
+ }
73
+ ```
74
+
75
+ ```bash
76
+ xberg batch scan.pdf report.pdf data.xlsx --file-configs overrides.json
77
+ ```
78
+
79
+ Keys are file paths (matching the paths passed on the command line);
80
+ values are per-file extraction config objects in snake_case, the same
81
+ shape as a config file.
82
+
83
+ ## Output layout
84
+
85
+ For text/toon output with image extraction, `--output-dir` controls where
86
+ referenced image files (e.g. `image_0.png`) are written; the directory
87
+ must already exist. JSON output embeds image bytes inline and ignores
88
+ `--output-dir`.
89
+
90
+ ```bash
91
+ mkdir -p out/images
92
+ xberg batch slides/*.pptx --extract-images true --output-dir out/images --format text
93
+ ```
94
+
95
+ ## Error recovery
96
+
97
+ Batch extraction is fault-tolerant per file: one unreadable or corrupt
98
+ document does not stop the rest. Inspect results for partial content and
99
+ surfaced errors rather than relying on the process exit code alone. Pair
100
+ with `--max-concurrent` to avoid exhausting memory when a few large files
101
+ sit in a big batch.
102
+
103
+ ## Shared config
104
+
105
+ Every `extract` flag also applies to `batch` (OCR, chunking, layout,
106
+ content format, etc.) and is shared across all files unless a
107
+ `--file-configs` entry overrides it:
108
+
109
+ ```bash
110
+ xberg batch invoices/*.pdf \
111
+ --layout --layout-table-model slanet_wireless \
112
+ --content-format markdown --max-concurrent 8
113
+ ```
114
+
115
+ A config file works too and auto-discovers from the cwd upward:
116
+
117
+ ```toml
118
+ output_format = "markdown"
119
+
120
+ [ocr]
121
+ backend = "tesseract"
122
+ language = "eng"
123
+ ```
124
+
125
+ ```bash
126
+ xberg batch corpus/*.pdf --config xberg.toml
127
+ ```
128
+
129
+ ## Programmatic access
130
+
131
+ From Python, `extract_batch` takes a list of `ExtractInput`s and returns one
132
+ envelope whose `results` array holds a document per input:
133
+
134
+ ```python
135
+ from xberg import ExtractInput, extract_batch, ExtractionConfig
136
+
137
+ config = ExtractionConfig(output_format="markdown")
138
+
139
+ inputs = [ExtractInput(uri=p) for p in ["a.pdf", "b.docx", "c.xlsx"]]
140
+ output = await extract_batch(inputs, config)
141
+
142
+ for doc in output.results:
143
+ print(len(doc.content))
144
+ ```
145
+
146
+ Per-input overrides go on `ExtractInput.config` (a `FileExtractionConfig`).
147
+ Node.js mirrors this with `extractBatch`; Rust uses `extract_batch(inputs, &config)`.
148
+ See `references/python-api.md`, `references/nodejs-api.md`, and
149
+ `references/rust-api.md` in the sibling `xberg` skill.
150
+
151
+ ## MCP
152
+
153
+ When the `xberg` MCP server is registered, prefer the
154
+ `extract_batch` tool over shelling out — it takes an array of input objects
155
+ and a config object and returns structured results directly.
156
+
157
+ ## Common pitfalls
158
+
159
+ - **Default format differs** — `batch` defaults to `--format json`,
160
+ `extract` to `--format text`. Set `--format` explicitly if a script
161
+ depends on one shape.
162
+ - **`--output-dir` must exist** — the CLI does not create it.
163
+ - **Memory blowups** — large batches with OCR/layout active may need an explicit
164
+ `--max-concurrent` ceiling.
165
+ - **`--file-configs` path keys** — must match the paths as passed on the
166
+ command line, not absolute-resolved variants.
167
+
168
+ See `references/cli-reference.md` for the full `batch` flag set.