objectivist-ner 0.0.6 → 0.1.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,535 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import path from "path";
4
+ import os from "os";
5
+ import fs from "fs";
6
+ import { getLlama, LlamaChatSession, resolveModelFile } from "node-llama-cpp";
7
+ // === MODELS ===
8
+ const MODELS = {
9
+ fast: "hf:unsloth/Qwen3.5-0.8B-GGUF:Qwen3.5-0.8B-Q8_0.gguf",
10
+ balanced: "hf:unsloth/Qwen3.5-2B-GGUF:Qwen3.5-2B-Q8_0.gguf",
11
+ best: "hf:unsloth/Qwen3.5-4B-GGUF:Qwen3.5-4B-Q8_0.gguf",
12
+ };
13
+ // === SYSTEM PROMPTS ===
14
+ const SYSTEM_PROMPT = `You are a named entity recognition (NER) system. Your task is to extract entities from text.
15
+
16
+ Rules:
17
+ - "text" must be the EXACT substring from the input that refers to the entity. Do NOT paraphrase or include extra words.
18
+ - "class" is the entity type (e.g. person, animal, location, organization).
19
+ - "attributes" are properties of the entity found in context.
20
+ - Return one object per distinct entity mention.
21
+ - If no entities are found, return an empty array [].`;
22
+ const FEW_SHOT_EXAMPLES = `Example 1:
23
+ Input: "the cat is blue and is feeling sad"
24
+ Output: [{"class":"animal","text":"cat","attributes":{"color":"blue","emotional_state":"sad"}}]
25
+
26
+ Example 2:
27
+ Input: "John Smith lives in New York City and works at Google"
28
+ Output: [{"class":"person","text":"John Smith","attributes":{"location":"New York City","employer":"Google"}},{"class":"location","text":"New York City","attributes":{}},{"class":"organization","text":"Google","attributes":{}}]
29
+
30
+ Example 3:
31
+ Input: "The quick brown fox jumps over the lazy dog near the river"
32
+ Output: [{"class":"animal","text":"fox","attributes":{"color":"brown","speed":"quick"}},{"class":"animal","text":"dog","attributes":{"temperament":"lazy"}},{"class":"location","text":"the river","attributes":{}}]
33
+
34
+ Example 4:
35
+ Input: "Researchers at MIT found that the drug Riluzole slows progression of ALS in a trial last March"
36
+ Output: [{"class":"organization","text":"MIT","attributes":{}},{"class":"drug","text":"Riluzole","attributes":{}},{"class":"disease","text":"ALS","attributes":{}},{"class":"event","text":"trial last March","attributes":{"date":"last March"}}]`;
37
+ // === TAXONOMY HELPERS ===
38
+ // Taxonomy format: {"organism": ["person", {"animal": ["dog", "cat"]}], "idea": ["dream", "principle"]}
39
+ // Arrays contain leaf nodes, objects contain nested taxonomies
40
+ function getLeafNodes(taxonomy) {
41
+ const leaves = [];
42
+ function traverse(node) {
43
+ if (Array.isArray(node)) {
44
+ for (const item of node) {
45
+ if (typeof item === "string") {
46
+ leaves.push(item);
47
+ }
48
+ else if (typeof item === "object" && item !== null) {
49
+ traverse(item);
50
+ }
51
+ }
52
+ }
53
+ else if (typeof node === "object" && node !== null) {
54
+ for (const [, value] of Object.entries(node)) {
55
+ traverse(value);
56
+ }
57
+ }
58
+ }
59
+ traverse(taxonomy);
60
+ return leaves;
61
+ }
62
+ function getTaxonomyPath(leaf, taxonomy) {
63
+ function findPath(node, target, currentPath) {
64
+ if (Array.isArray(node)) {
65
+ for (const item of node) {
66
+ if (typeof item === "string" && item === target) {
67
+ return [...currentPath, item];
68
+ }
69
+ else if (typeof item === "object" && item !== null) {
70
+ const result = findPath(item, target, currentPath);
71
+ if (result)
72
+ return result;
73
+ }
74
+ }
75
+ }
76
+ else if (typeof node === "object" && node !== null) {
77
+ for (const [key, value] of Object.entries(node)) {
78
+ const result = findPath(value, target, [...currentPath, key]);
79
+ if (result)
80
+ return result;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ // Try each root node
86
+ for (const [rootKey, rootValue] of Object.entries(taxonomy)) {
87
+ const path = findPath(rootValue, leaf, [rootKey]);
88
+ if (path)
89
+ return path;
90
+ }
91
+ return [leaf];
92
+ }
93
+ function taxonomyToPrompt(taxonomy) {
94
+ function formatNode(node, indent) {
95
+ const lines = [];
96
+ if (Array.isArray(node)) {
97
+ for (const item of node) {
98
+ if (typeof item === "string") {
99
+ lines.push(`${indent}- ${item}`);
100
+ }
101
+ else if (typeof item === "object" && item !== null) {
102
+ lines.push(...formatNode(item, indent));
103
+ }
104
+ }
105
+ }
106
+ else if (typeof node === "object" && node !== null) {
107
+ for (const [key, value] of Object.entries(node)) {
108
+ lines.push(`${indent}- ${key}`);
109
+ lines.push(...formatNode(value, indent + " "));
110
+ }
111
+ }
112
+ return lines;
113
+ }
114
+ const lines = [
115
+ "Use the following class hierarchy. Classify at the most specific (leaf) level:",
116
+ ];
117
+ for (const [rootKey, rootValue] of Object.entries(taxonomy)) {
118
+ lines.push(`- ${rootKey}`);
119
+ lines.push(...formatNode(rootValue, " "));
120
+ }
121
+ return lines.join("\n");
122
+ }
123
+ // === CHUNKING ===
124
+ function chunkText(text, maxChars) {
125
+ if (text.length <= maxChars)
126
+ return [text];
127
+ const chunks = [];
128
+ const sentences = text.split(/(?<=[.!?])\s+/);
129
+ let current = "";
130
+ for (const sentence of sentences) {
131
+ if (current.length + sentence.length > maxChars && current.length > 0) {
132
+ chunks.push(current.trim());
133
+ current = "";
134
+ }
135
+ current += (current ? " " : "") + sentence;
136
+ }
137
+ if (current.trim())
138
+ chunks.push(current.trim());
139
+ return chunks;
140
+ }
141
+ // === CLI ===
142
+ program
143
+ .name("ner")
144
+ .description("Objectivist-inspired named entity recognition with grammar constraints")
145
+ .argument("[text]", "Text to extract entities from (omit to read from stdin)")
146
+ .option("-c, --classes <list>", "Comma-separated allowed entity classes")
147
+ .option("-a, --attributes <list>", "Comma-separated allowed attribute keys")
148
+ .option("--attr-values <json>", 'JSON enum map for attributes e.g. {"color":["blue","red"]}')
149
+ .option("--taxonomy <json>", 'Class hierarchy JSON e.g. {"organism":["animal","plant"]}')
150
+ .option("--relations", "Extract relations between entities")
151
+ .option("--relation-classes <list>", "Comma-separated allowed relation classes (e.g. employment,location,causal)")
152
+ .option("--resolve", "Resolve coreferences (group mentions of same entity)")
153
+ .option("--include-confidence", "Include confidence scores per entity")
154
+ .option("--detect-negation", "Detect negated/hypothetical entities")
155
+ .option("--schema <path>", "Load entity schema definition from a JSON file")
156
+ .option("--file <path>", "Read input from a file (with chunking for long docs)")
157
+ .option("--batch <path>", "Process JSONL file (one text per line) or directory of .txt files")
158
+ .option("--system-prompt <string>", "Replace the built-in system prompt entirely")
159
+ .option("--system-prompt-append <string>", "Append to the built-in system prompt")
160
+ .option("-m, --model <uri>", "Model URI or path to GGUF file")
161
+ .option("--fast", "Use smallest model (0.8B) -- quick, simple text only")
162
+ .option("--balanced", "Use mid-size model (2B) -- good accuracy/speed tradeoff")
163
+ .option("--best", "Use largest model (4B) -- best accuracy (default)")
164
+ .option("--compact", "Output compact JSON (also auto-enabled for non-TTY)")
165
+ .addHelpText("after", `
166
+ Examples:
167
+ ner "the cat is blue"
168
+ ner "John works at Google" --classes person,organization
169
+ ner "sky is blue" --attr-values '{"color":["blue","red"]}'
170
+ ner --relations "Dr. Chen works at MIT"
171
+ ner --resolve "Dr. Chen published a paper. She won an award."
172
+ ner --detect-negation "The patient does not have cancer"
173
+ ner --schema schema.json "complex text"
174
+ ner --file document.txt
175
+ ner --batch inputs.jsonl
176
+ echo "the cat is blue" | ner
177
+ `)
178
+ .parse();
179
+ const opts = program.opts();
180
+ // === VALIDATIONS ===
181
+ const tierFlags = [opts.fast, opts.balanced, opts.best].filter(Boolean).length;
182
+ if (tierFlags > 1) {
183
+ console.error("Error: --fast, --balanced, and --best are mutually exclusive.");
184
+ process.exit(1);
185
+ }
186
+ if (opts.systemPrompt && opts.systemPromptAppend) {
187
+ console.error("Error: --system-prompt and --system-prompt-append are mutually exclusive.");
188
+ process.exit(1);
189
+ }
190
+ // === LOAD SCHEMA FILE ===
191
+ let schemaFile;
192
+ if (opts.schema) {
193
+ try {
194
+ const raw = fs.readFileSync(opts.schema, "utf-8");
195
+ schemaFile = JSON.parse(raw);
196
+ }
197
+ catch (e) {
198
+ console.error(`Error: Failed to load schema file: ${e.message}`);
199
+ process.exit(1);
200
+ }
201
+ }
202
+ // === MERGE OPTIONS (CLI flags override schema file) ===
203
+ const allowedClasses = opts.classes
204
+ ? opts.classes.split(",").map((s) => s.trim())
205
+ : schemaFile?.classes;
206
+ const allowedAttrs = opts.attributes
207
+ ? opts.attributes.split(",").map((s) => s.trim())
208
+ : schemaFile?.attributes;
209
+ let attrValuesMap;
210
+ if (opts.attrValues) {
211
+ try {
212
+ attrValuesMap = JSON.parse(opts.attrValues);
213
+ }
214
+ catch (e) {
215
+ console.error(`Error: Invalid JSON for --attr-values: ${e.message}`);
216
+ process.exit(1);
217
+ }
218
+ }
219
+ else if (schemaFile?.attrValues) {
220
+ attrValuesMap = schemaFile.attrValues;
221
+ }
222
+ let taxonomy;
223
+ if (opts.taxonomy) {
224
+ try {
225
+ taxonomy = JSON.parse(opts.taxonomy);
226
+ }
227
+ catch (e) {
228
+ console.error(`Error: Invalid JSON for --taxonomy: ${e.message}`);
229
+ process.exit(1);
230
+ }
231
+ }
232
+ else if (schemaFile?.taxonomy) {
233
+ taxonomy = schemaFile.taxonomy;
234
+ }
235
+ const enableRelations = opts.relations || !!schemaFile?.relations;
236
+ const relationTypes = schemaFile?.relations || undefined;
237
+ const relationClasses = opts.relationClasses
238
+ ? opts.relationClasses.split(",").map((s) => s.trim())
239
+ : schemaFile?.relationClasses;
240
+ const enableResolve = !!opts.resolve;
241
+ const enableConfidence = !!opts.includeConfidence;
242
+ const enableNegation = !!opts.detectNegation;
243
+ // === READ INPUT ===
244
+ let inputTexts = [];
245
+ if (opts.batch) {
246
+ const batchPath = opts.batch;
247
+ const stat = fs.statSync(batchPath);
248
+ if (stat.isDirectory()) {
249
+ const files = fs
250
+ .readdirSync(batchPath)
251
+ .filter((f) => f.endsWith(".txt"));
252
+ inputTexts = files.map((f) => fs.readFileSync(path.join(batchPath, f), "utf-8").trim());
253
+ }
254
+ else {
255
+ const content = fs.readFileSync(batchPath, "utf-8").trim();
256
+ inputTexts = content.split("\n").map((line) => {
257
+ try {
258
+ const parsed = JSON.parse(line);
259
+ return typeof parsed === "string" ? parsed : parsed.text || line;
260
+ }
261
+ catch {
262
+ return line;
263
+ }
264
+ });
265
+ }
266
+ }
267
+ else if (opts.file) {
268
+ const content = fs.readFileSync(opts.file, "utf-8").trim();
269
+ inputTexts = chunkText(content, 2000);
270
+ }
271
+ else {
272
+ let text = program.args[0];
273
+ if (!text) {
274
+ const chunks = [];
275
+ for await (const chunk of process.stdin) {
276
+ chunks.push(chunk);
277
+ }
278
+ text = Buffer.concat(chunks).toString().trim();
279
+ }
280
+ if (!text) {
281
+ console.error("Error: No input text provided. Pass as argument, --file, --batch, or stdin.");
282
+ process.exit(1);
283
+ }
284
+ inputTexts = [text];
285
+ }
286
+ // === RESOLVE MODEL ===
287
+ const modelUri = opts.model
288
+ ? opts.model
289
+ : opts.fast
290
+ ? MODELS.fast
291
+ : opts.balanced
292
+ ? MODELS.balanced
293
+ : MODELS.best;
294
+ const modelsDir = path.join(os.homedir(), ".fastner", "models");
295
+ const modelPath = await resolveModelFile(modelUri, modelsDir);
296
+ const llama = await getLlama();
297
+ const model = await llama.loadModel({ modelPath });
298
+ const context = await model.createContext();
299
+ // === BUILD SYSTEM PROMPT ===
300
+ function buildSystemPrompt() {
301
+ if (opts.systemPrompt)
302
+ return opts.systemPrompt;
303
+ let base = SYSTEM_PROMPT;
304
+ if (enableNegation) {
305
+ base += `\n- Every entity has a top-level "assertion" field: "present", "negated", or "hypothetical". "negated" means the text explicitly denies it (e.g. "does not have"). "hypothetical" means it is speculative (e.g. "might develop").`;
306
+ }
307
+ if (enableConfidence) {
308
+ base += `\n- Every entity has a top-level "confidence" field: "low", "medium", or "high".`;
309
+ }
310
+ if (enableResolve) {
311
+ base += `\n- Every entity has a top-level "entity_id" field. If multiple text spans refer to the same real-world entity (e.g. "Dr. Chen" and "she"), they share the same entity_id. Use short IDs like "e1", "e2".`;
312
+ base += `\n- When multiple mentions share an entity_id, exactly ONE of them must have "is_canonical": true (the most specific reference like a proper name). The others must have "is_canonical": false.`;
313
+ }
314
+ let prompt = `${base}\n\n${FEW_SHOT_EXAMPLES}`;
315
+ if (opts.systemPromptAppend) {
316
+ prompt += `\n\n${opts.systemPromptAppend}`;
317
+ }
318
+ return prompt;
319
+ }
320
+ // === BUILD GRAMMAR SCHEMA ===
321
+ function buildGrammarSchema() {
322
+ // When using taxonomy, only allow leaf nodes as valid classes.
323
+ // This forces the model to classify at the most specific level.
324
+ const classEnum = taxonomy ? getLeafNodes(taxonomy) : allowedClasses;
325
+ const attributesSchema = {
326
+ type: "object",
327
+ additionalProperties: { type: "string" },
328
+ };
329
+ const properties = {
330
+ class: {
331
+ type: "string",
332
+ ...(classEnum && { enum: classEnum }),
333
+ },
334
+ text: { type: "string" },
335
+ attributes: attributesSchema,
336
+ };
337
+ const required = ["class", "text"];
338
+ // Grammar-enforced fields for enabled features.
339
+ // These are top-level entity properties (not inside attributes)
340
+ // so the grammar can enforce them as required on every entity.
341
+ if (enableNegation) {
342
+ properties.assertion = {
343
+ type: "string",
344
+ enum: ["present", "negated", "hypothetical"],
345
+ };
346
+ required.push("assertion");
347
+ }
348
+ if (enableConfidence) {
349
+ properties.confidence = {
350
+ type: "string",
351
+ enum: ["low", "medium", "high"],
352
+ };
353
+ required.push("confidence");
354
+ }
355
+ if (enableResolve) {
356
+ properties.entity_id = { type: "string" };
357
+ properties.is_canonical = { type: "boolean" };
358
+ required.push("entity_id");
359
+ required.push("is_canonical");
360
+ }
361
+ const schema = {
362
+ type: "array",
363
+ items: {
364
+ type: "object",
365
+ properties,
366
+ required,
367
+ additionalProperties: false,
368
+ },
369
+ };
370
+ return schema;
371
+ }
372
+ // === BUILD RELATIONS SCHEMA ===
373
+ function buildRelationsSchema() {
374
+ const relationProperties = {
375
+ source: { type: "string" },
376
+ target: { type: "string" },
377
+ relation: {
378
+ type: "string",
379
+ ...(relationTypes && { enum: relationTypes }),
380
+ },
381
+ };
382
+ // Add class field if relationClasses is specified
383
+ if (relationClasses) {
384
+ relationProperties.class = {
385
+ type: "string",
386
+ enum: relationClasses,
387
+ };
388
+ }
389
+ const relSchema = {
390
+ type: "object",
391
+ properties: {
392
+ entities: buildGrammarSchema(),
393
+ relations: {
394
+ type: "array",
395
+ items: {
396
+ type: "object",
397
+ properties: relationProperties,
398
+ required: relationClasses
399
+ ? ["source", "target", "relation", "class"]
400
+ : ["source", "target", "relation"],
401
+ additionalProperties: false,
402
+ },
403
+ },
404
+ },
405
+ required: ["entities", "relations"],
406
+ additionalProperties: false,
407
+ };
408
+ return relSchema;
409
+ }
410
+ // === BUILD PROMPT CONSTRAINTS ===
411
+ function buildConstraints() {
412
+ let constraints = "";
413
+ if (taxonomy) {
414
+ constraints += `\n${taxonomyToPrompt(taxonomy)}`;
415
+ }
416
+ else if (allowedClasses) {
417
+ constraints += `\nAllowed entity classes: ${allowedClasses.join(", ")}. Only use these classes.`;
418
+ }
419
+ if (attrValuesMap) {
420
+ const desc = Object.entries(attrValuesMap)
421
+ .map(([k, v]) => `${k}: ${v.join(", ")}`)
422
+ .join("; ");
423
+ constraints += `\nOnly use these attribute keys and values: ${desc}. Omit attributes that don't apply to an entity.`;
424
+ }
425
+ else if (allowedAttrs) {
426
+ constraints += `\nOnly use these attribute keys: ${allowedAttrs.join(", ")}. Omit attributes that don't apply to an entity.`;
427
+ }
428
+ if (enableNegation) {
429
+ constraints += `\nEvery entity has an "assertion" field (not in attributes). Example: [{"class":"disease","text":"diabetes","assertion":"present","attributes":{}},{"class":"disease","text":"cancer","assertion":"negated","attributes":{}}]`;
430
+ }
431
+ if (enableConfidence) {
432
+ constraints += `\nEvery entity has a "confidence" field (not in attributes). Example: [{"class":"person","text":"John","confidence":"high","attributes":{}}]`;
433
+ }
434
+ if (enableResolve) {
435
+ constraints += `\nEvery entity has "entity_id" and "is_canonical" fields. Coreferent mentions share entity_id; exactly one per group has is_canonical:true (the most specific reference). Example: [{"class":"person","text":"Dr. Chen","entity_id":"e1","is_canonical":true,"attributes":{}},{"class":"person","text":"She","entity_id":"e1","is_canonical":false,"attributes":{}}]`;
436
+ }
437
+ if (enableRelations) {
438
+ let relDesc = `\nAlso extract relations between entities.`;
439
+ if (relationClasses) {
440
+ relDesc += ` Each relation must have a "class" field categorizing the relation type.`;
441
+ }
442
+ relDesc += ` Return {"entities": [...], "relations": [{"source": "entity text", "target": "entity text", "relation": "relation type"${relationClasses ? ', "class": "relation class"' : ""}}]}.`;
443
+ constraints += relDesc;
444
+ if (relationTypes) {
445
+ constraints += ` Allowed relation types: ${relationTypes.join(", ")}.`;
446
+ }
447
+ if (relationClasses) {
448
+ constraints += ` Allowed relation classes: ${relationClasses.join(", ")}.`;
449
+ }
450
+ }
451
+ return constraints;
452
+ }
453
+ // === PROCESS A SINGLE TEXT ===
454
+ async function processText(inputText, session) {
455
+ const constraints = buildConstraints();
456
+ const prompt = `Extract all named entities from the following text.${constraints}\n\nText: ${inputText}`;
457
+ const schema = enableRelations
458
+ ? buildRelationsSchema()
459
+ : buildGrammarSchema();
460
+ const grammar = await llama.createGrammarForJsonSchema(schema);
461
+ const res = await session.prompt(prompt, { grammar });
462
+ let parsed;
463
+ try {
464
+ parsed = grammar.parse(res);
465
+ }
466
+ catch {
467
+ try {
468
+ parsed = JSON.parse(res.trim());
469
+ }
470
+ catch {
471
+ console.error("Warning: Failed to parse model output. Raw response:", res);
472
+ parsed = enableRelations ? { entities: [], relations: [] } : [];
473
+ }
474
+ }
475
+ // If taxonomy is used, add taxonomyPath showing full hierarchy
476
+ if (taxonomy && !enableRelations && Array.isArray(parsed)) {
477
+ for (const entity of parsed) {
478
+ if (entity.class && typeof entity.class === "string") {
479
+ entity.taxonomyPath = getTaxonomyPath(entity.class, taxonomy);
480
+ }
481
+ }
482
+ }
483
+ return parsed;
484
+ }
485
+ // === MAIN ===
486
+ const systemPrompt = buildSystemPrompt();
487
+ const compact = opts.compact || !process.stdout.isTTY;
488
+ const contextSequence = context.getSequence();
489
+ if (inputTexts.length === 1) {
490
+ const session = new LlamaChatSession({
491
+ contextSequence,
492
+ systemPrompt,
493
+ });
494
+ const result = await processText(inputTexts[0], session);
495
+ console.log(JSON.stringify(result, null, compact ? 0 : 2));
496
+ }
497
+ else {
498
+ // Batch / chunked: process each text, collect results
499
+ const allResults = [];
500
+ for (const inputText of inputTexts) {
501
+ // Erase context and create fresh session for each input
502
+ await contextSequence.eraseContextTokenRanges([
503
+ { start: 0, end: contextSequence.nextTokenIndex },
504
+ ]);
505
+ const session = new LlamaChatSession({
506
+ contextSequence,
507
+ systemPrompt,
508
+ });
509
+ const result = await processText(inputText, session);
510
+ allResults.push(result);
511
+ }
512
+ if (opts.file) {
513
+ // Merge chunked results into one
514
+ if (enableRelations) {
515
+ const merged = {
516
+ entities: allResults.flatMap((r) => r.entities || []),
517
+ relations: allResults.flatMap((r) => r.relations || []),
518
+ };
519
+ console.log(JSON.stringify(merged, null, compact ? 0 : 2));
520
+ }
521
+ else {
522
+ const merged = allResults.flat();
523
+ console.log(JSON.stringify(merged, null, compact ? 0 : 2));
524
+ }
525
+ }
526
+ else {
527
+ // Batch: output one result per line (JSONL)
528
+ for (const result of allResults) {
529
+ console.log(JSON.stringify(result));
530
+ }
531
+ }
532
+ }
533
+ // === CLEANUP ===
534
+ process.exitCode = 0;
535
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,24 +1,34 @@
1
1
  {
2
2
  "name": "objectivist-ner",
3
- "version": "0.0.6",
3
+ "version": "0.1.0",
4
4
  "description": "Objectivist-inspired Named Entity Recognition with grammar-constrained LLM output",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
5
7
  "bin": {
6
- "ner": "index.ts"
8
+ "ner": "dist/index.js"
7
9
  },
8
- "module": "index.ts",
9
10
  "type": "module",
10
11
  "license": "MIT",
11
12
  "author": "Richard Anaya",
12
13
  "repository": "git@github.com:richardanaya/objectivist-ner.git",
13
- "devDependencies": {
14
- "@types/bun": "latest",
15
- "prettier": "^3.8.1"
14
+ "files": [
15
+ "dist/",
16
+ "LICENSE",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "dev": "tsx index.ts",
21
+ "build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > temp.js && mv temp.js dist/index.js && chmod +x dist/index.js",
22
+ "prepublishOnly": "npm run build"
16
23
  },
17
- "peerDependencies": {
18
- "typescript": "^5"
24
+ "engines": {
25
+ "node": ">=18"
19
26
  },
20
- "scripts": {
21
- "postinstall": "node-llama-cpp download"
27
+ "devDependencies": {
28
+ "@types/node": "^20.0.0",
29
+ "prettier": "^3.8.1",
30
+ "tsx": "^4.0.0",
31
+ "typescript": "^5.0.0"
22
32
  },
23
33
  "dependencies": {
24
34
  "commander": "^14.0.3",
package/index.ts DELETED
@@ -1,635 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { program } from "commander";
3
- import path from "path";
4
- import os from "os";
5
- import fs from "fs";
6
- import { getLlama, LlamaChatSession, resolveModelFile } from "node-llama-cpp";
7
-
8
- // === MODELS ===
9
- const MODELS = {
10
- fast: "hf:unsloth/Qwen3.5-0.8B-GGUF:Qwen3.5-0.8B-Q8_0.gguf",
11
- balanced: "hf:unsloth/Qwen3.5-2B-GGUF:Qwen3.5-2B-Q8_0.gguf",
12
- best: "hf:unsloth/Qwen3.5-4B-GGUF:Qwen3.5-4B-Q8_0.gguf",
13
- } as const;
14
-
15
- // === SYSTEM PROMPTS ===
16
- const SYSTEM_PROMPT = `You are a named entity recognition (NER) system. Your task is to extract entities from text.
17
-
18
- Rules:
19
- - "text" must be the EXACT substring from the input that refers to the entity. Do NOT paraphrase or include extra words.
20
- - "class" is the entity type (e.g. person, animal, location, organization).
21
- - "attributes" are properties of the entity found in context.
22
- - Return one object per distinct entity mention.
23
- - If no entities are found, return an empty array [].`;
24
-
25
- const FEW_SHOT_EXAMPLES = `Example 1:
26
- Input: "the cat is blue and is feeling sad"
27
- Output: [{"class":"animal","text":"cat","attributes":{"color":"blue","emotional_state":"sad"}}]
28
-
29
- Example 2:
30
- Input: "John Smith lives in New York City and works at Google"
31
- Output: [{"class":"person","text":"John Smith","attributes":{"location":"New York City","employer":"Google"}},{"class":"location","text":"New York City","attributes":{}},{"class":"organization","text":"Google","attributes":{}}]
32
-
33
- Example 3:
34
- Input: "The quick brown fox jumps over the lazy dog near the river"
35
- Output: [{"class":"animal","text":"fox","attributes":{"color":"brown","speed":"quick"}},{"class":"animal","text":"dog","attributes":{"temperament":"lazy"}},{"class":"location","text":"the river","attributes":{}}]
36
-
37
- Example 4:
38
- Input: "Researchers at MIT found that the drug Riluzole slows progression of ALS in a trial last March"
39
- Output: [{"class":"organization","text":"MIT","attributes":{}},{"class":"drug","text":"Riluzole","attributes":{}},{"class":"disease","text":"ALS","attributes":{}},{"class":"event","text":"trial last March","attributes":{"date":"last March"}}]`;
40
-
41
- // === SCHEMA FILE TYPES ===
42
- interface SchemaFile {
43
- taxonomy?: Record<string, string[]>;
44
- classes?: string[];
45
- attributes?: string[];
46
- attrValues?: Record<string, string[]>;
47
- relations?: string[];
48
- relationClasses?: string[];
49
- }
50
-
51
- // === TAXONOMY HELPERS ===
52
- // Taxonomy format: {"organism": ["person", {"animal": ["dog", "cat"]}], "idea": ["dream", "principle"]}
53
- // Arrays contain leaf nodes, objects contain nested taxonomies
54
-
55
- function getLeafNodes(taxonomy: Record<string, any>): string[] {
56
- const leaves: string[] = [];
57
-
58
- function traverse(node: any) {
59
- if (Array.isArray(node)) {
60
- for (const item of node) {
61
- if (typeof item === "string") {
62
- leaves.push(item);
63
- } else if (typeof item === "object" && item !== null) {
64
- traverse(item);
65
- }
66
- }
67
- } else if (typeof node === "object" && node !== null) {
68
- for (const [, value] of Object.entries(node)) {
69
- traverse(value);
70
- }
71
- }
72
- }
73
-
74
- traverse(taxonomy);
75
- return leaves;
76
- }
77
-
78
- function getTaxonomyPath(
79
- leaf: string,
80
- taxonomy: Record<string, any>,
81
- ): string[] {
82
- function findPath(
83
- node: any,
84
- target: string,
85
- currentPath: string[],
86
- ): string[] | null {
87
- if (Array.isArray(node)) {
88
- for (const item of node) {
89
- if (typeof item === "string" && item === target) {
90
- return [...currentPath, item];
91
- } else if (typeof item === "object" && item !== null) {
92
- const result = findPath(item, target, currentPath);
93
- if (result) return result;
94
- }
95
- }
96
- } else if (typeof node === "object" && node !== null) {
97
- for (const [key, value] of Object.entries(node)) {
98
- const result = findPath(value, target, [...currentPath, key]);
99
- if (result) return result;
100
- }
101
- }
102
- return null;
103
- }
104
-
105
- // Try each root node
106
- for (const [rootKey, rootValue] of Object.entries(taxonomy)) {
107
- const path = findPath(rootValue, leaf, [rootKey]);
108
- if (path) return path;
109
- }
110
-
111
- return [leaf];
112
- }
113
-
114
- function taxonomyToPrompt(taxonomy: Record<string, any>): string {
115
- function formatNode(node: any, indent: string): string[] {
116
- const lines: string[] = [];
117
-
118
- if (Array.isArray(node)) {
119
- for (const item of node) {
120
- if (typeof item === "string") {
121
- lines.push(`${indent}- ${item}`);
122
- } else if (typeof item === "object" && item !== null) {
123
- lines.push(...formatNode(item, indent));
124
- }
125
- }
126
- } else if (typeof node === "object" && node !== null) {
127
- for (const [key, value] of Object.entries(node)) {
128
- lines.push(`${indent}- ${key}`);
129
- lines.push(...formatNode(value, indent + " "));
130
- }
131
- }
132
-
133
- return lines;
134
- }
135
-
136
- const lines: string[] = [
137
- "Use the following class hierarchy. Classify at the most specific (leaf) level:",
138
- ];
139
-
140
- for (const [rootKey, rootValue] of Object.entries(taxonomy)) {
141
- lines.push(`- ${rootKey}`);
142
- lines.push(...formatNode(rootValue, " "));
143
- }
144
-
145
- return lines.join("\n");
146
- }
147
-
148
- // === CHUNKING ===
149
- function chunkText(text: string, maxChars: number): string[] {
150
- if (text.length <= maxChars) return [text];
151
- const chunks: string[] = [];
152
- const sentences = text.split(/(?<=[.!?])\s+/);
153
- let current = "";
154
- for (const sentence of sentences) {
155
- if (current.length + sentence.length > maxChars && current.length > 0) {
156
- chunks.push(current.trim());
157
- current = "";
158
- }
159
- current += (current ? " " : "") + sentence;
160
- }
161
- if (current.trim()) chunks.push(current.trim());
162
- return chunks;
163
- }
164
-
165
- // === CLI ===
166
- program
167
- .name("ner")
168
- .description(
169
- "Objectivist-inspired named entity recognition with grammar constraints",
170
- )
171
- .argument("[text]", "Text to extract entities from (omit to read from stdin)")
172
- .option("-c, --classes <list>", "Comma-separated allowed entity classes")
173
- .option("-a, --attributes <list>", "Comma-separated allowed attribute keys")
174
- .option(
175
- "--attr-values <json>",
176
- 'JSON enum map for attributes e.g. {"color":["blue","red"]}',
177
- )
178
- .option(
179
- "--taxonomy <json>",
180
- 'Class hierarchy JSON e.g. {"organism":["animal","plant"]}',
181
- )
182
- .option("--relations", "Extract relations between entities")
183
- .option(
184
- "--relation-classes <list>",
185
- "Comma-separated allowed relation classes (e.g. employment,location,causal)",
186
- )
187
- .option("--resolve", "Resolve coreferences (group mentions of same entity)")
188
- .option("--include-confidence", "Include confidence scores per entity")
189
- .option("--detect-negation", "Detect negated/hypothetical entities")
190
- .option("--schema <path>", "Load entity schema definition from a JSON file")
191
- .option(
192
- "--file <path>",
193
- "Read input from a file (with chunking for long docs)",
194
- )
195
- .option(
196
- "--batch <path>",
197
- "Process JSONL file (one text per line) or directory of .txt files",
198
- )
199
- .option(
200
- "--system-prompt <string>",
201
- "Replace the built-in system prompt entirely",
202
- )
203
- .option(
204
- "--system-prompt-append <string>",
205
- "Append to the built-in system prompt",
206
- )
207
- .option("-m, --model <uri>", "Model URI or path to GGUF file")
208
- .option("--fast", "Use smallest model (0.8B) -- quick, simple text only")
209
- .option(
210
- "--balanced",
211
- "Use mid-size model (2B) -- good accuracy/speed tradeoff",
212
- )
213
- .option("--best", "Use largest model (4B) -- best accuracy (default)")
214
- .option("--compact", "Output compact JSON (also auto-enabled for non-TTY)")
215
- .addHelpText(
216
- "after",
217
- `
218
- Examples:
219
- ner "the cat is blue"
220
- ner "John works at Google" --classes person,organization
221
- ner "sky is blue" --attr-values '{"color":["blue","red"]}'
222
- ner --relations "Dr. Chen works at MIT"
223
- ner --resolve "Dr. Chen published a paper. She won an award."
224
- ner --detect-negation "The patient does not have cancer"
225
- ner --schema schema.json "complex text"
226
- ner --file document.txt
227
- ner --batch inputs.jsonl
228
- echo "the cat is blue" | ner
229
- `,
230
- )
231
- .parse();
232
-
233
- const opts = program.opts();
234
-
235
- // === VALIDATIONS ===
236
- const tierFlags = [opts.fast, opts.balanced, opts.best].filter(Boolean).length;
237
- if (tierFlags > 1) {
238
- console.error(
239
- "Error: --fast, --balanced, and --best are mutually exclusive.",
240
- );
241
- process.exit(1);
242
- }
243
-
244
- if (opts.systemPrompt && opts.systemPromptAppend) {
245
- console.error(
246
- "Error: --system-prompt and --system-prompt-append are mutually exclusive.",
247
- );
248
- process.exit(1);
249
- }
250
-
251
- // === LOAD SCHEMA FILE ===
252
- let schemaFile: SchemaFile | undefined;
253
- if (opts.schema) {
254
- try {
255
- const raw = fs.readFileSync(opts.schema, "utf-8");
256
- schemaFile = JSON.parse(raw);
257
- } catch (e) {
258
- console.error(`Error: Failed to load schema file: ${(e as Error).message}`);
259
- process.exit(1);
260
- }
261
- }
262
-
263
- // === MERGE OPTIONS (CLI flags override schema file) ===
264
- const allowedClasses: string[] | undefined = opts.classes
265
- ? opts.classes.split(",").map((s: string) => s.trim())
266
- : schemaFile?.classes;
267
-
268
- const allowedAttrs: string[] | undefined = opts.attributes
269
- ? opts.attributes.split(",").map((s: string) => s.trim())
270
- : schemaFile?.attributes;
271
-
272
- let attrValuesMap: Record<string, string[]> | undefined;
273
- if (opts.attrValues) {
274
- try {
275
- attrValuesMap = JSON.parse(opts.attrValues);
276
- } catch (e) {
277
- console.error(
278
- `Error: Invalid JSON for --attr-values: ${(e as Error).message}`,
279
- );
280
- process.exit(1);
281
- }
282
- } else if (schemaFile?.attrValues) {
283
- attrValuesMap = schemaFile.attrValues;
284
- }
285
-
286
- let taxonomy: Record<string, string[]> | undefined;
287
- if (opts.taxonomy) {
288
- try {
289
- taxonomy = JSON.parse(opts.taxonomy);
290
- } catch (e) {
291
- console.error(
292
- `Error: Invalid JSON for --taxonomy: ${(e as Error).message}`,
293
- );
294
- process.exit(1);
295
- }
296
- } else if (schemaFile?.taxonomy) {
297
- taxonomy = schemaFile.taxonomy;
298
- }
299
-
300
- const enableRelations = opts.relations || !!schemaFile?.relations;
301
- const relationTypes: string[] | undefined = schemaFile?.relations || undefined;
302
- const relationClasses: string[] | undefined = opts.relationClasses
303
- ? opts.relationClasses.split(",").map((s: string) => s.trim())
304
- : schemaFile?.relationClasses;
305
- const enableResolve = !!opts.resolve;
306
- const enableConfidence = !!opts.includeConfidence;
307
- const enableNegation = !!opts.detectNegation;
308
-
309
- // === READ INPUT ===
310
- let inputTexts: string[] = [];
311
-
312
- if (opts.batch) {
313
- const batchPath = opts.batch as string;
314
- const stat = fs.statSync(batchPath);
315
- if (stat.isDirectory()) {
316
- const files = fs
317
- .readdirSync(batchPath)
318
- .filter((f: string) => f.endsWith(".txt"));
319
- inputTexts = files.map((f: string) =>
320
- fs.readFileSync(path.join(batchPath, f), "utf-8").trim(),
321
- );
322
- } else {
323
- const content = fs.readFileSync(batchPath, "utf-8").trim();
324
- inputTexts = content.split("\n").map((line: string) => {
325
- try {
326
- const parsed = JSON.parse(line);
327
- return typeof parsed === "string" ? parsed : parsed.text || line;
328
- } catch {
329
- return line;
330
- }
331
- });
332
- }
333
- } else if (opts.file) {
334
- const content = fs.readFileSync(opts.file, "utf-8").trim();
335
- inputTexts = chunkText(content, 2000);
336
- } else {
337
- let text = program.args[0];
338
- if (!text) {
339
- const chunks: Buffer[] = [];
340
- for await (const chunk of process.stdin) {
341
- chunks.push(chunk as Buffer);
342
- }
343
- text = Buffer.concat(chunks).toString().trim();
344
- }
345
- if (!text) {
346
- console.error(
347
- "Error: No input text provided. Pass as argument, --file, --batch, or stdin.",
348
- );
349
- process.exit(1);
350
- }
351
- inputTexts = [text];
352
- }
353
-
354
- // === RESOLVE MODEL ===
355
- const modelUri = opts.model
356
- ? opts.model
357
- : opts.fast
358
- ? MODELS.fast
359
- : opts.balanced
360
- ? MODELS.balanced
361
- : MODELS.best;
362
-
363
- const modelsDir = path.join(os.homedir(), ".fastner", "models");
364
- const modelPath = await resolveModelFile(modelUri, modelsDir);
365
-
366
- const llama = await getLlama();
367
- const model = await llama.loadModel({ modelPath });
368
- const context = await model.createContext();
369
-
370
- // === BUILD SYSTEM PROMPT ===
371
- function buildSystemPrompt(): string {
372
- if (opts.systemPrompt) return opts.systemPrompt;
373
-
374
- let base = SYSTEM_PROMPT;
375
-
376
- if (enableNegation) {
377
- base += `\n- Every entity has a top-level "assertion" field: "present", "negated", or "hypothetical". "negated" means the text explicitly denies it (e.g. "does not have"). "hypothetical" means it is speculative (e.g. "might develop").`;
378
- }
379
- if (enableConfidence) {
380
- base += `\n- Every entity has a top-level "confidence" field: "low", "medium", or "high".`;
381
- }
382
- if (enableResolve) {
383
- base += `\n- Every entity has a top-level "entity_id" field. If multiple text spans refer to the same real-world entity (e.g. "Dr. Chen" and "she"), they share the same entity_id. Use short IDs like "e1", "e2".`;
384
- base += `\n- When multiple mentions share an entity_id, exactly ONE of them must have "is_canonical": true (the most specific reference like a proper name). The others must have "is_canonical": false.`;
385
- }
386
-
387
- let prompt = `${base}\n\n${FEW_SHOT_EXAMPLES}`;
388
-
389
- if (opts.systemPromptAppend) {
390
- prompt += `\n\n${opts.systemPromptAppend}`;
391
- }
392
-
393
- return prompt;
394
- }
395
-
396
- // === BUILD GRAMMAR SCHEMA ===
397
- function buildGrammarSchema() {
398
- // When using taxonomy, only allow leaf nodes as valid classes.
399
- // This forces the model to classify at the most specific level.
400
- const classEnum = taxonomy ? getLeafNodes(taxonomy) : allowedClasses;
401
-
402
- const attributesSchema: any = {
403
- type: "object",
404
- additionalProperties: { type: "string" },
405
- };
406
-
407
- const properties: any = {
408
- class: {
409
- type: "string",
410
- ...(classEnum && { enum: classEnum }),
411
- },
412
- text: { type: "string" },
413
- attributes: attributesSchema,
414
- };
415
- const required: string[] = ["class", "text"];
416
-
417
- // Grammar-enforced fields for enabled features.
418
- // These are top-level entity properties (not inside attributes)
419
- // so the grammar can enforce them as required on every entity.
420
- if (enableNegation) {
421
- properties.assertion = {
422
- type: "string",
423
- enum: ["present", "negated", "hypothetical"],
424
- };
425
- required.push("assertion");
426
- }
427
- if (enableConfidence) {
428
- properties.confidence = {
429
- type: "string",
430
- enum: ["low", "medium", "high"],
431
- };
432
- required.push("confidence");
433
- }
434
- if (enableResolve) {
435
- properties.entity_id = { type: "string" };
436
- properties.is_canonical = { type: "boolean" };
437
- required.push("entity_id");
438
- required.push("is_canonical");
439
- }
440
-
441
- const schema: any = {
442
- type: "array",
443
- items: {
444
- type: "object",
445
- properties,
446
- required,
447
- additionalProperties: false,
448
- },
449
- };
450
-
451
- return schema;
452
- }
453
-
454
- // === BUILD RELATIONS SCHEMA ===
455
- function buildRelationsSchema() {
456
- const relationProperties: any = {
457
- source: { type: "string" },
458
- target: { type: "string" },
459
- relation: {
460
- type: "string",
461
- ...(relationTypes && { enum: relationTypes }),
462
- },
463
- };
464
-
465
- // Add class field if relationClasses is specified
466
- if (relationClasses) {
467
- relationProperties.class = {
468
- type: "string",
469
- enum: relationClasses,
470
- };
471
- }
472
-
473
- const relSchema: any = {
474
- type: "object",
475
- properties: {
476
- entities: buildGrammarSchema(),
477
- relations: {
478
- type: "array",
479
- items: {
480
- type: "object",
481
- properties: relationProperties,
482
- required: relationClasses
483
- ? ["source", "target", "relation", "class"]
484
- : ["source", "target", "relation"],
485
- additionalProperties: false,
486
- },
487
- },
488
- },
489
- required: ["entities", "relations"],
490
- additionalProperties: false,
491
- };
492
- return relSchema;
493
- }
494
-
495
- // === BUILD PROMPT CONSTRAINTS ===
496
- function buildConstraints(): string {
497
- let constraints = "";
498
-
499
- if (taxonomy) {
500
- constraints += `\n${taxonomyToPrompt(taxonomy)}`;
501
- } else if (allowedClasses) {
502
- constraints += `\nAllowed entity classes: ${allowedClasses.join(", ")}. Only use these classes.`;
503
- }
504
-
505
- if (attrValuesMap) {
506
- const desc = Object.entries(attrValuesMap)
507
- .map(([k, v]) => `${k}: ${v.join(", ")}`)
508
- .join("; ");
509
- constraints += `\nOnly use these attribute keys and values: ${desc}. Omit attributes that don't apply to an entity.`;
510
- } else if (allowedAttrs) {
511
- constraints += `\nOnly use these attribute keys: ${allowedAttrs.join(", ")}. Omit attributes that don't apply to an entity.`;
512
- }
513
-
514
- if (enableNegation) {
515
- constraints += `\nEvery entity has an "assertion" field (not in attributes). Example: [{"class":"disease","text":"diabetes","assertion":"present","attributes":{}},{"class":"disease","text":"cancer","assertion":"negated","attributes":{}}]`;
516
- }
517
- if (enableConfidence) {
518
- constraints += `\nEvery entity has a "confidence" field (not in attributes). Example: [{"class":"person","text":"John","confidence":"high","attributes":{}}]`;
519
- }
520
- if (enableResolve) {
521
- constraints += `\nEvery entity has "entity_id" and "is_canonical" fields. Coreferent mentions share entity_id; exactly one per group has is_canonical:true (the most specific reference). Example: [{"class":"person","text":"Dr. Chen","entity_id":"e1","is_canonical":true,"attributes":{}},{"class":"person","text":"She","entity_id":"e1","is_canonical":false,"attributes":{}}]`;
522
- }
523
- if (enableRelations) {
524
- let relDesc = `\nAlso extract relations between entities.`;
525
- if (relationClasses) {
526
- relDesc += ` Each relation must have a "class" field categorizing the relation type.`;
527
- }
528
- relDesc += ` Return {"entities": [...], "relations": [{"source": "entity text", "target": "entity text", "relation": "relation type"${relationClasses ? ', "class": "relation class"' : ""}}]}.`;
529
- constraints += relDesc;
530
- if (relationTypes) {
531
- constraints += ` Allowed relation types: ${relationTypes.join(", ")}.`;
532
- }
533
- if (relationClasses) {
534
- constraints += ` Allowed relation classes: ${relationClasses.join(", ")}.`;
535
- }
536
- }
537
-
538
- return constraints;
539
- }
540
-
541
- // === PROCESS A SINGLE TEXT ===
542
- async function processText(
543
- inputText: string,
544
- session: LlamaChatSession,
545
- ): Promise<any> {
546
- const constraints = buildConstraints();
547
- const prompt = `Extract all named entities from the following text.${constraints}\n\nText: ${inputText}`;
548
-
549
- const schema = enableRelations
550
- ? buildRelationsSchema()
551
- : buildGrammarSchema();
552
- const grammar = await llama.createGrammarForJsonSchema(schema);
553
-
554
- const res = await session.prompt(prompt, { grammar });
555
-
556
- let parsed: any;
557
- try {
558
- parsed = grammar.parse(res);
559
- } catch {
560
- try {
561
- parsed = JSON.parse(res.trim());
562
- } catch {
563
- console.error(
564
- "Warning: Failed to parse model output. Raw response:",
565
- res,
566
- );
567
- parsed = enableRelations ? { entities: [], relations: [] } : [];
568
- }
569
- }
570
-
571
- // If taxonomy is used, add taxonomyPath showing full hierarchy
572
- if (taxonomy && !enableRelations && Array.isArray(parsed)) {
573
- for (const entity of parsed) {
574
- if (entity.class && typeof entity.class === "string") {
575
- entity.taxonomyPath = getTaxonomyPath(entity.class, taxonomy);
576
- }
577
- }
578
- }
579
-
580
- return parsed;
581
- }
582
-
583
- // === MAIN ===
584
- const systemPrompt = buildSystemPrompt();
585
- const compact = opts.compact || !process.stdout.isTTY;
586
-
587
- const contextSequence = context.getSequence();
588
-
589
- if (inputTexts.length === 1) {
590
- const session = new LlamaChatSession({
591
- contextSequence,
592
- systemPrompt,
593
- });
594
- const result = await processText(inputTexts[0]!, session);
595
- console.log(JSON.stringify(result, null, compact ? 0 : 2));
596
- } else {
597
- // Batch / chunked: process each text, collect results
598
- const allResults: any[] = [];
599
- for (const inputText of inputTexts) {
600
- // Erase context and create fresh session for each input
601
- await contextSequence.eraseContextTokenRanges([
602
- { start: 0, end: contextSequence.nextTokenIndex },
603
- ]);
604
- const session = new LlamaChatSession({
605
- contextSequence,
606
- systemPrompt,
607
- });
608
- const result = await processText(inputText, session);
609
- allResults.push(result);
610
- }
611
-
612
- if (opts.file) {
613
- // Merge chunked results into one
614
- if (enableRelations) {
615
- const merged = {
616
- entities: allResults.flatMap((r) => r.entities || []),
617
- relations: allResults.flatMap((r) => r.relations || []),
618
- };
619
- console.log(JSON.stringify(merged, null, compact ? 0 : 2));
620
- } else {
621
- const merged = allResults.flat();
622
- console.log(JSON.stringify(merged, null, compact ? 0 : 2));
623
- }
624
- } else {
625
- // Batch: output one result per line (JSONL)
626
- for (const result of allResults) {
627
- console.log(JSON.stringify(result));
628
- }
629
- }
630
- }
631
-
632
- // === CLEANUP ===
633
- // Bun segfaults if process.exit() triggers synchronous native addon unloading.
634
- // Setting exitCode lets the event loop drain naturally, avoiding the crash.
635
- process.exitCode = 0;