@yeyuan98/opencode-bioresearcher-plugin 1.5.0-alpha.0 → 1.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 CHANGED
@@ -75,8 +75,20 @@ Parse PubMed XML files to markdown or Excel format. Supports `.xml` and `.xml.gz
75
75
  Download pubmed article data from https://ftp.ncbi.nlm.nih.gov/pubmed/updatefiles/pubmed26n1340.xml.gz and parse to Excel format.
76
76
  ```
77
77
 
78
- Reference: [PubMed Download Data](https://pubmed.ncbi.nlm.nih.gov/download/).
79
-
78
+ Reference: [PubMed Download Data](https://pubmed.ncbi.nlm.nih.gov/download/).
79
+
80
+ ### OBO Parser
81
+
82
+ Parse OBO (Open Biological and Biomedical Ontology) files to CSV format. Handles Term, Typedef, and Instance frames.
83
+
84
+ **Flatten complex ontologies into analysis-ready tables.**
85
+
86
+ ```text
87
+ Parse go.obo file and convert to CSV using parse_obo_file tool.
88
+ ```
89
+
90
+ Reference: [OBO Format](http://owlcollab.github.io/oboformat/doc/obo-syntax.html).
91
+
80
92
  ### JSON Tools
81
93
 
82
94
  Extract, validate, and infer JSON schemas from data.
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { tableTools } from "./table-tools/index";
5
5
  import { dbTools } from "./db-tools/index";
6
6
  import { blockingTimer, calculator, jsonExtract, jsonValidate, jsonInfer } from "./misc-tools/index";
7
7
  import { parse_pubmed_articleSet } from "./parser-tools/pubmed";
8
+ import { parse_obo_file } from "./parser-tools/obo";
8
9
  import { SkillTool } from "./skill-tools";
9
10
  export const BioResearcherPlugin = async () => {
10
11
  return {
@@ -23,7 +24,8 @@ export const BioResearcherPlugin = async () => {
23
24
  jsonExtract,
24
25
  jsonValidate,
25
26
  jsonInfer,
26
- parse_pubmed_articleSet
27
+ parse_pubmed_articleSet,
28
+ parse_obo_file
27
29
  }
28
30
  };
29
31
  };
@@ -0,0 +1,2 @@
1
+ export { parse_obo_file } from './obo.js';
2
+ export * from './types.js';
@@ -0,0 +1,2 @@
1
+ export { parse_obo_file } from './obo.js';
2
+ export * from './types.js';
@@ -0,0 +1,17 @@
1
+ import { ToolContext } from '@opencode-ai/plugin/tool';
2
+ import { z } from 'zod';
3
+ export declare const parse_obo_file: {
4
+ description: string;
5
+ args: {
6
+ filePath: z.ZodString;
7
+ outputFileName: z.ZodDefault<z.ZodOptional<z.ZodString>>;
8
+ outputDir: z.ZodOptional<z.ZodString>;
9
+ verbose: z.ZodDefault<z.ZodBoolean>;
10
+ };
11
+ execute(args: {
12
+ filePath: string;
13
+ outputFileName: string;
14
+ verbose: boolean;
15
+ outputDir?: string | undefined;
16
+ }, context: ToolContext): Promise<string>;
17
+ };
@@ -0,0 +1,216 @@
1
+ import { tool } from '@opencode-ai/plugin/tool';
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
+ import { join, resolve } from 'path';
4
+ import { z } from 'zod';
5
+ import { parseLine, createEmptyTermFrame, createEmptyTypedefFrame, createEmptyInstanceFrame, processTagValue, frameToCSVRow, generateCSV } from './utils.js';
6
+ function formatError(error) {
7
+ const message = error instanceof Error ? error.message : String(error);
8
+ return JSON.stringify({ error: message }, null, 2);
9
+ }
10
+ function resolvePath(filePath, basePath) {
11
+ if (!basePath) {
12
+ return filePath;
13
+ }
14
+ if (filePath.startsWith('./') || filePath.startsWith('../')) {
15
+ return resolve(basePath, filePath);
16
+ }
17
+ if (filePath.startsWith('/') || /^[A-Za-z]:/.test(filePath)) {
18
+ return filePath;
19
+ }
20
+ return resolve(basePath, filePath);
21
+ }
22
+ function ensureDir(dirPath) {
23
+ if (!existsSync(dirPath)) {
24
+ mkdirSync(dirPath, { recursive: true });
25
+ }
26
+ }
27
+ function parseOBOFile(content) {
28
+ const lines = content.split('\n');
29
+ const header = {
30
+ subsetdefs: [],
31
+ synonymtypedefs: []
32
+ };
33
+ const frames = [];
34
+ let currentFrame = null;
35
+ let inHeader = true;
36
+ let termCount = 0;
37
+ let typedefCount = 0;
38
+ let instanceCount = 0;
39
+ for (const line of lines) {
40
+ const trimmed = line.trim();
41
+ if (!trimmed)
42
+ continue;
43
+ if (trimmed.startsWith('!')) {
44
+ continue;
45
+ }
46
+ if (trimmed === '[Term]') {
47
+ inHeader = false;
48
+ if (currentFrame) {
49
+ frames.push(currentFrame);
50
+ }
51
+ currentFrame = createEmptyTermFrame();
52
+ termCount++;
53
+ }
54
+ else if (trimmed === '[Typedef]') {
55
+ inHeader = false;
56
+ if (currentFrame) {
57
+ frames.push(currentFrame);
58
+ }
59
+ currentFrame = createEmptyTypedefFrame();
60
+ typedefCount++;
61
+ }
62
+ else if (trimmed === '[Instance]') {
63
+ inHeader = false;
64
+ if (currentFrame) {
65
+ frames.push(currentFrame);
66
+ }
67
+ currentFrame = createEmptyInstanceFrame();
68
+ instanceCount++;
69
+ }
70
+ else if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
71
+ if (currentFrame) {
72
+ frames.push(currentFrame);
73
+ currentFrame = null;
74
+ }
75
+ }
76
+ else if (inHeader) {
77
+ const parsed = parseLine(line);
78
+ if (parsed) {
79
+ const { tag, value } = parsed;
80
+ switch (tag) {
81
+ case 'format-version':
82
+ header.format_version = value;
83
+ break;
84
+ case 'data-version':
85
+ header.data_version = value;
86
+ break;
87
+ case 'date':
88
+ header.date = value;
89
+ break;
90
+ case 'saved-by':
91
+ header.saved_by = value;
92
+ break;
93
+ case 'auto-generated-by':
94
+ header.auto_generated_by = value;
95
+ break;
96
+ case 'import':
97
+ if (!header.import)
98
+ header.import = [];
99
+ if (Array.isArray(header.import)) {
100
+ header.import.push(value);
101
+ }
102
+ else {
103
+ header.import = [header.import, value];
104
+ }
105
+ break;
106
+ case 'default-namespace':
107
+ header.default_namespace = value;
108
+ break;
109
+ case 'ontology':
110
+ header.ontology = value;
111
+ break;
112
+ case 'remark':
113
+ if (!header.remark)
114
+ header.remark = [];
115
+ if (Array.isArray(header.remark)) {
116
+ header.remark.push(value);
117
+ }
118
+ else {
119
+ header.remark = [header.remark, value];
120
+ }
121
+ break;
122
+ case 'property_value':
123
+ if (!header.property_values)
124
+ header.property_values = [];
125
+ header.property_values.push({ tag: '', value });
126
+ break;
127
+ case 'owl-axioms':
128
+ header.owl_axioms = value;
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ else if (currentFrame) {
134
+ const parsed = parseLine(line);
135
+ if (parsed && parsed.tag) {
136
+ processTagValue(currentFrame, parsed.tag, parsed.value);
137
+ }
138
+ }
139
+ }
140
+ if (currentFrame) {
141
+ frames.push(currentFrame);
142
+ }
143
+ const stats = {
144
+ total: termCount + typedefCount + instanceCount,
145
+ terms: termCount,
146
+ typedefs: typedefCount,
147
+ instances: instanceCount
148
+ };
149
+ return { document: { header, frames }, stats };
150
+ }
151
+ export const parse_obo_file = tool({
152
+ description: 'Parse OBO (Open Biological and Biomedical Ontology) files and convert to CSV format. Handles Term, Typedef, and Instance frames with proper escaping and comment handling.',
153
+ args: {
154
+ filePath: z.string()
155
+ .describe('Path to OBO file (.obo)'),
156
+ outputFileName: z.string()
157
+ .optional()
158
+ .default('obo_output.csv')
159
+ .describe('Custom output CSV file name (default: obo_output.csv)'),
160
+ outputDir: z.string()
161
+ .optional()
162
+ .describe('Custom output directory (default: ./tmp/opencode/<sessionId>/)'),
163
+ verbose: z.boolean()
164
+ .default(false)
165
+ .describe('Enable verbose logging for debugging')
166
+ },
167
+ execute: async (args, context) => {
168
+ const verbose = args.verbose ?? false;
169
+ try {
170
+ const { filePath, outputFileName = 'obo_output.csv', outputDir } = args;
171
+ if (verbose)
172
+ console.log('Starting OBO file parsing...');
173
+ const resolvedPath = resolvePath(outputDir || './tmp/opencode', context.directory);
174
+ ensureDir(resolvedPath);
175
+ if (verbose)
176
+ console.log(`Output directory: ${resolvedPath}`);
177
+ if (verbose)
178
+ console.log(`Reading file: ${filePath}`);
179
+ const fileContent = readFileSync(filePath, 'utf-8');
180
+ if (verbose)
181
+ console.log('Parsing OBO document...');
182
+ const { document, stats } = parseOBOFile(fileContent);
183
+ if (verbose) {
184
+ console.log(`Parsing complete:`);
185
+ console.log(` Total frames: ${stats.total}`);
186
+ console.log(` Terms: ${stats.terms}`);
187
+ console.log(` Typedefs: ${stats.typedefs}`);
188
+ console.log(` Instances: ${stats.instances}`);
189
+ }
190
+ const csvRows = [];
191
+ if (verbose)
192
+ console.log('Flattening frames to CSV rows...');
193
+ for (const frame of document.frames) {
194
+ csvRows.push(frameToCSVRow(frame));
195
+ }
196
+ if (verbose)
197
+ console.log(`Converted ${csvRows.length} frames to CSV rows`);
198
+ const csvContent = generateCSV(csvRows);
199
+ const outputPath = join(resolvedPath, outputFileName);
200
+ writeFileSync(outputPath, csvContent, 'utf-8');
201
+ if (verbose)
202
+ console.log(`Wrote CSV file: ${outputPath}`);
203
+ return JSON.stringify({
204
+ success: true,
205
+ filePath: outputPath,
206
+ stats: stats,
207
+ message: `Successfully parsed ${stats.total} frames (${stats.terms} terms, ${stats.typedefs} typedefs, ${stats.instances} instances) to ${outputPath}`
208
+ });
209
+ }
210
+ catch (error) {
211
+ if (verbose)
212
+ console.error(`Error: ${error.message}`);
213
+ return formatError(error);
214
+ }
215
+ }
216
+ });
@@ -0,0 +1,166 @@
1
+ export interface OBOHeader {
2
+ format_version?: string;
3
+ data_version?: string;
4
+ date?: string;
5
+ saved_by?: string;
6
+ auto_generated_by?: string;
7
+ import?: string | string[];
8
+ subsetdefs: OBOSubsetDef[];
9
+ synonymtypedefs: OBOSynonymTypeDef[];
10
+ default_namespace?: string;
11
+ ontology?: string;
12
+ idspace?: string;
13
+ remark?: string | string[];
14
+ property_values?: OBOPropertyValue[];
15
+ owl_axioms?: string;
16
+ [key: string]: any;
17
+ }
18
+ export interface OBOSubsetDef {
19
+ id: string;
20
+ name: string;
21
+ }
22
+ export interface OBOSynonymTypeDef {
23
+ id: string;
24
+ name: string;
25
+ scope?: string;
26
+ }
27
+ export interface OBOPropertyValue {
28
+ tag: string;
29
+ value: string;
30
+ xsd_type?: string;
31
+ qualifiers?: OBOSynonymTypeDef[];
32
+ }
33
+ export interface TermFrame {
34
+ frame_type: 'term';
35
+ id: string;
36
+ is_anonymous?: boolean;
37
+ name?: string;
38
+ namespace?: string;
39
+ alt_ids?: string[];
40
+ def?: string;
41
+ comment?: string;
42
+ subsets?: string[];
43
+ synonyms?: OBOSynonym[];
44
+ xrefs?: string[];
45
+ builtin?: boolean;
46
+ property_values?: OBOPropertyValue[];
47
+ is_a?: string[];
48
+ intersection_of?: string[];
49
+ union_of?: string[];
50
+ equivalent_to?: string[];
51
+ disjoint_from?: string[];
52
+ relationships?: OBORelationship[];
53
+ is_obsolete?: boolean;
54
+ replaced_by?: string;
55
+ consider?: string;
56
+ created_by?: string;
57
+ creation_date?: string;
58
+ }
59
+ export interface TypedefFrame {
60
+ frame_type: 'typedef';
61
+ id: string;
62
+ is_anonymous?: boolean;
63
+ name?: string;
64
+ namespace?: string;
65
+ alt_ids?: string[];
66
+ def?: string;
67
+ comment?: string;
68
+ subsets?: string[];
69
+ synonyms?: OBOSynonym[];
70
+ xrefs?: string[];
71
+ builtin?: boolean;
72
+ property_values?: OBOPropertyValue[];
73
+ domain?: string;
74
+ range?: string;
75
+ holds_over_chain?: string[];
76
+ is_anti_symmetric?: boolean;
77
+ is_cyclic?: boolean;
78
+ is_reflexive?: boolean;
79
+ is_symmetric?: boolean;
80
+ is_transitive?: boolean;
81
+ is_functional?: boolean;
82
+ is_inverse_functional?: boolean;
83
+ is_a?: string[];
84
+ intersection_of?: string[];
85
+ union_of?: string[];
86
+ equivalent_to?: string[];
87
+ disjoint_from?: string[];
88
+ inverse_of?: string;
89
+ transitive_over?: string;
90
+ equivalent_to_chain?: string[];
91
+ disjoint_over?: string;
92
+ relationships?: OBORelationship[];
93
+ is_obsolete?: boolean;
94
+ replaced_by?: string;
95
+ consider?: string;
96
+ created_by?: string;
97
+ creation_date?: string;
98
+ expand_assertion_to?: string;
99
+ expand_expression_to?: string;
100
+ is_metadata_tag?: boolean;
101
+ is_class_level?: boolean;
102
+ }
103
+ export interface InstanceFrame {
104
+ frame_type: 'instance';
105
+ id: string;
106
+ is_anonymous?: boolean;
107
+ name?: string;
108
+ namespace?: string;
109
+ alt_ids?: string[];
110
+ def?: string;
111
+ comment?: string;
112
+ subsets?: string[];
113
+ synonyms?: OBOSynonym[];
114
+ xrefs?: string[];
115
+ builtin?: boolean;
116
+ property_values?: OBOPropertyValue[];
117
+ instance_of?: string;
118
+ relationships?: OBORelationship[];
119
+ is_obsolete?: boolean;
120
+ replaced_by?: string;
121
+ consider?: string;
122
+ created_by?: string;
123
+ creation_date?: string;
124
+ }
125
+ export interface OBOSynonym {
126
+ text: string;
127
+ scope?: 'EXACT' | 'BROAD' | 'NARROW' | 'RELATED';
128
+ type?: string;
129
+ xrefs?: string[];
130
+ }
131
+ export interface OBORelationship {
132
+ type: string;
133
+ value: string;
134
+ qualifiers?: OBOSynonymTypeDef[];
135
+ }
136
+ export interface OBODocument {
137
+ header: OBOHeader;
138
+ frames: (TermFrame | TypedefFrame | InstanceFrame)[];
139
+ }
140
+ export interface CSVRow {
141
+ frame_type: string;
142
+ id: string;
143
+ name?: string;
144
+ namespace?: string;
145
+ def?: string;
146
+ synonyms?: string;
147
+ subsets?: string;
148
+ xrefs?: string;
149
+ relationships?: string;
150
+ comment?: string;
151
+ is_obsolete?: boolean;
152
+ created_by?: string;
153
+ creation_date?: string;
154
+ alt_ids?: string;
155
+ is_anonymous?: boolean;
156
+ parent?: string;
157
+ domain?: string;
158
+ range?: string;
159
+ instance_of?: string;
160
+ }
161
+ export interface ProcessingStats {
162
+ total: number;
163
+ terms: number;
164
+ typedefs: number;
165
+ instances: number;
166
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { TermFrame, TypedefFrame, InstanceFrame, CSVRow, OBOSynonym, OBORelationship, OBOSubsetDef, OBOSynonymTypeDef } from './types.js';
2
+ export declare function stripComment(value: string): string;
3
+ export declare function parseLine(line: string): {
4
+ tag: string | null;
5
+ value: string;
6
+ } | null;
7
+ export declare function parseQuotedString(value: string): string;
8
+ export declare function parseXrefList(value: string): string[];
9
+ export declare function parseSynonym(value: string): OBOSynonym;
10
+ export declare function parseRelationship(input: string): OBORelationship;
11
+ export declare function createEmptyTermFrame(): TermFrame;
12
+ export declare function createEmptyTypedefFrame(): TypedefFrame;
13
+ export declare function createEmptyInstanceFrame(): InstanceFrame;
14
+ export declare function processTagValue(frame: TermFrame | TypedefFrame | InstanceFrame, tag: string, value: string): void;
15
+ export declare function parsePropertyValue(value: string): any;
16
+ export declare function parseHeader(lines: string[]): OBOSubsetDef[] | OBOSynonymTypeDef[];
17
+ export declare function flattenSynonyms(synonyms?: OBOSynonym[]): string;
18
+ export declare function flattenRelationships(relationships?: OBORelationship[]): string;
19
+ export declare function flattenArray(arr?: string[]): string;
20
+ export declare function frameToCSVRow(frame: TermFrame | TypedefFrame | InstanceFrame): CSVRow;
21
+ export declare function generateCSV(rows: CSVRow[]): string;
@@ -0,0 +1,411 @@
1
+ export function stripComment(value) {
2
+ const exclamationIndex = value.indexOf('!');
3
+ if (exclamationIndex === -1)
4
+ return value.trim();
5
+ return value.substring(0, exclamationIndex).trim();
6
+ }
7
+ export function parseLine(line) {
8
+ if (!line || line.trim().startsWith('!'))
9
+ return null;
10
+ const colonIndex = line.indexOf(':');
11
+ if (colonIndex === -1)
12
+ return null;
13
+ const tag = line.substring(0, colonIndex).trim();
14
+ const value = line.substring(colonIndex + 1).trim();
15
+ return { tag, value };
16
+ }
17
+ export function parseQuotedString(value) {
18
+ if (value.startsWith('"') && value.endsWith('"')) {
19
+ return value.slice(1, -1);
20
+ }
21
+ return value;
22
+ }
23
+ export function parseXrefList(value) {
24
+ const stripped = stripComment(value);
25
+ if (stripped.startsWith('[') && stripped.endsWith(']')) {
26
+ const content = stripped.slice(1, -1).trim();
27
+ if (!content)
28
+ return [];
29
+ return content.split(',').map(x => x.trim()).filter(x => x.length > 0);
30
+ }
31
+ return [stripped];
32
+ }
33
+ export function parseSynonym(value) {
34
+ const stripped = stripComment(value);
35
+ let text = stripped;
36
+ let scope;
37
+ let type;
38
+ let xrefs;
39
+ const quotedMatch = stripped.match(/^"([^"]+)"/);
40
+ if (quotedMatch) {
41
+ text = quotedMatch[1];
42
+ }
43
+ else {
44
+ text = stripped;
45
+ }
46
+ const parts = text.split(/\s+/).filter(p => p.length > 0);
47
+ const scopeIndex = parts.findIndex(p => p === 'EXACT' || p === 'BROAD' || p === 'NARROW' || p === 'RELATED');
48
+ if (scopeIndex !== -1) {
49
+ scope = parts[scopeIndex];
50
+ parts.splice(scopeIndex, 1);
51
+ }
52
+ if (parts.length > 1 && !parts[1].startsWith('[')) {
53
+ type = parts[1];
54
+ }
55
+ const xrefMatch = stripped.match(/\[([^\]]+)\]/);
56
+ if (xrefMatch) {
57
+ const xrefContent = xrefMatch[1].trim();
58
+ xrefs = xrefContent.split(',').map(x => x.trim()).filter(x => x.length > 0);
59
+ }
60
+ return { text, scope, type, xrefs };
61
+ }
62
+ export function parseRelationship(input) {
63
+ const stripped = stripComment(input);
64
+ const parts = stripped.split(/\s+/).filter(p => p.length > 0);
65
+ if (parts.length < 2) {
66
+ return { type: '', value: stripped };
67
+ }
68
+ const type = parts[0];
69
+ const relValue = parts.slice(1).join(' ');
70
+ return { type, value: relValue };
71
+ }
72
+ export function createEmptyTermFrame() {
73
+ return {
74
+ frame_type: 'term',
75
+ id: ''
76
+ };
77
+ }
78
+ export function createEmptyTypedefFrame() {
79
+ return {
80
+ frame_type: 'typedef',
81
+ id: ''
82
+ };
83
+ }
84
+ export function createEmptyInstanceFrame() {
85
+ return {
86
+ frame_type: 'instance',
87
+ id: ''
88
+ };
89
+ }
90
+ export function processTagValue(frame, tag, value) {
91
+ const stripped = stripComment(value);
92
+ const unquoted = parseQuotedString(stripped);
93
+ switch (tag) {
94
+ case 'id':
95
+ frame.id = stripped;
96
+ break;
97
+ case 'name':
98
+ frame.name = unquoted;
99
+ break;
100
+ case 'namespace':
101
+ frame.namespace = stripped;
102
+ break;
103
+ case 'alt_id':
104
+ if (!frame.alt_ids)
105
+ frame.alt_ids = [];
106
+ frame.alt_ids.push(stripped);
107
+ break;
108
+ case 'def':
109
+ frame.def = unquoted;
110
+ break;
111
+ case 'comment':
112
+ frame.comment = unquoted;
113
+ break;
114
+ case 'subset':
115
+ if (!frame.subsets)
116
+ frame.subsets = [];
117
+ frame.subsets.push(stripped);
118
+ break;
119
+ case 'synonym':
120
+ if (!frame.synonyms)
121
+ frame.synonyms = [];
122
+ frame.synonyms.push(parseSynonym(value));
123
+ break;
124
+ case 'xref':
125
+ if (!frame.xrefs)
126
+ frame.xrefs = [];
127
+ frame.xrefs.push(...parseXrefList(value));
128
+ break;
129
+ case 'builtin':
130
+ frame.builtin = stripped.toLowerCase() === 'true';
131
+ break;
132
+ case 'is_a':
133
+ if ('is_a' in frame) {
134
+ if (!frame.is_a)
135
+ frame.is_a = [];
136
+ frame.is_a.push(stripped);
137
+ }
138
+ break;
139
+ case 'intersection_of':
140
+ if ('intersection_of' in frame) {
141
+ if (!frame.intersection_of)
142
+ frame.intersection_of = [];
143
+ frame.intersection_of.push(stripped);
144
+ }
145
+ break;
146
+ case 'union_of':
147
+ if ('union_of' in frame) {
148
+ if (!frame.union_of)
149
+ frame.union_of = [];
150
+ frame.union_of.push(stripped);
151
+ }
152
+ break;
153
+ case 'equivalent_to':
154
+ if ('equivalent_to' in frame) {
155
+ if (!frame.equivalent_to)
156
+ frame.equivalent_to = [];
157
+ frame.equivalent_to.push(stripped);
158
+ }
159
+ break;
160
+ case 'disjoint_from':
161
+ if ('disjoint_from' in frame) {
162
+ if (!frame.disjoint_from)
163
+ frame.disjoint_from = [];
164
+ frame.disjoint_from.push(stripped);
165
+ }
166
+ break;
167
+ case 'relationship':
168
+ if (!frame.relationships)
169
+ frame.relationships = [];
170
+ frame.relationships.push(parseRelationship(value));
171
+ break;
172
+ case 'is_obsolete':
173
+ frame.is_obsolete = stripped.toLowerCase() === 'true';
174
+ break;
175
+ case 'replaced_by':
176
+ frame.replaced_by = stripped;
177
+ break;
178
+ case 'consider':
179
+ frame.consider = stripped;
180
+ break;
181
+ case 'created_by':
182
+ frame.created_by = stripped;
183
+ break;
184
+ case 'creation_date':
185
+ frame.creation_date = stripped;
186
+ break;
187
+ case 'is_anonymous':
188
+ frame.is_anonymous = stripped.toLowerCase() === 'true';
189
+ break;
190
+ case 'property_value':
191
+ if (!frame.property_values)
192
+ frame.property_values = [];
193
+ frame.property_values.push(parsePropertyValue(value));
194
+ break;
195
+ case 'domain':
196
+ if ('domain' in frame)
197
+ frame.domain = stripped;
198
+ break;
199
+ case 'range':
200
+ if ('range' in frame)
201
+ frame.range = stripped;
202
+ break;
203
+ case 'holds_over_chain':
204
+ if ('holds_over_chain' in frame)
205
+ frame.holds_over_chain = [];
206
+ frame.holds_over_chain?.push(stripped);
207
+ break;
208
+ case 'is_anti_symmetric':
209
+ if ('is_anti_symmetric' in frame)
210
+ frame.is_anti_symmetric = stripped.toLowerCase() === 'true';
211
+ break;
212
+ case 'is_cyclic':
213
+ if ('is_cyclic' in frame)
214
+ frame.is_cyclic = stripped.toLowerCase() === 'true';
215
+ break;
216
+ case 'is_reflexive':
217
+ if ('is_reflexive' in frame)
218
+ frame.is_reflexive = stripped.toLowerCase() === 'true';
219
+ break;
220
+ case 'is_symmetric':
221
+ if ('is_symmetric' in frame)
222
+ frame.is_symmetric = stripped.toLowerCase() === 'true';
223
+ break;
224
+ case 'is_transitive':
225
+ if ('is_transitive' in frame)
226
+ frame.is_transitive = stripped.toLowerCase() === 'true';
227
+ break;
228
+ case 'is_functional':
229
+ if ('is_functional' in frame)
230
+ frame.is_functional = stripped.toLowerCase() === 'true';
231
+ break;
232
+ case 'is_inverse_functional':
233
+ if ('is_inverse_functional' in frame)
234
+ frame.is_inverse_functional = stripped.toLowerCase() === 'true';
235
+ break;
236
+ case 'inverse_of':
237
+ if ('inverse_of' in frame)
238
+ frame.inverse_of = stripped;
239
+ break;
240
+ case 'transitive_over':
241
+ if ('transitive_over' in frame)
242
+ frame.transitive_over = stripped;
243
+ break;
244
+ case 'equivalent_to_chain':
245
+ if ('equivalent_to_chain' in frame)
246
+ frame.equivalent_to_chain = [];
247
+ frame.equivalent_to_chain?.push(stripped);
248
+ break;
249
+ case 'disjoint_over':
250
+ if ('disjoint_over' in frame)
251
+ frame.disjoint_over = stripped;
252
+ break;
253
+ case 'expand_assertion_to':
254
+ if ('expand_assertion_to' in frame)
255
+ frame.expand_assertion_to = unquoted;
256
+ break;
257
+ case 'expand_expression_to':
258
+ if ('expand_expression_to' in frame)
259
+ frame.expand_expression_to = unquoted;
260
+ break;
261
+ case 'is_metadata_tag':
262
+ if ('is_metadata_tag' in frame)
263
+ frame.is_metadata_tag = stripped.toLowerCase() === 'true';
264
+ break;
265
+ case 'is_class_level':
266
+ if ('is_class_level' in frame)
267
+ frame.is_class_level = stripped.toLowerCase() === 'true';
268
+ break;
269
+ case 'instance_of':
270
+ if ('instance_of' in frame)
271
+ frame.instance_of = stripped;
272
+ break;
273
+ default:
274
+ break;
275
+ }
276
+ }
277
+ export function parsePropertyValue(value) {
278
+ const stripped = stripComment(value);
279
+ const parts = stripped.split(/\s+/);
280
+ if (parts.length < 1) {
281
+ return { tag: '', value: '' };
282
+ }
283
+ const tag = parts[0];
284
+ const remaining = parts.slice(1).join(' ');
285
+ if (remaining.startsWith('"')) {
286
+ const match = remaining.match(/^"([^"]*)"\s*(.*)$/);
287
+ if (match) {
288
+ return {
289
+ tag,
290
+ value: match[1],
291
+ xsd_type: match[2]?.trim() || undefined
292
+ };
293
+ }
294
+ }
295
+ return { tag, value: remaining };
296
+ }
297
+ export function parseHeader(lines) {
298
+ const subsetdefs = [];
299
+ const synonymtypedefs = [];
300
+ for (const line of lines) {
301
+ const parsed = parseLine(line);
302
+ if (!parsed)
303
+ continue;
304
+ const { tag, value } = parsed;
305
+ if (tag === 'subsetdef') {
306
+ const parts = value.split(/\s+/);
307
+ const id = parts[0];
308
+ const nameMatch = value.match(/"([^"]+)"/);
309
+ const name = nameMatch ? nameMatch[1] : parts[1];
310
+ subsetdefs.push({ id, name });
311
+ }
312
+ else if (tag === 'synonymtypedef') {
313
+ const parts = value.split(/\s+/);
314
+ const id = parts[0];
315
+ const nameMatch = value.match(/"([^"]+)"/);
316
+ const name = nameMatch ? nameMatch[1] : parts[1];
317
+ const scope = parts.find(p => p === 'EXACT' || p === 'BROAD' || p === 'NARROW' || p === 'RELATED');
318
+ synonymtypedefs.push({ id, name, scope });
319
+ }
320
+ }
321
+ return [...subsetdefs, ...synonymtypedefs];
322
+ }
323
+ export function flattenSynonyms(synonyms) {
324
+ if (!synonyms || synonyms.length === 0)
325
+ return '';
326
+ const json = synonyms.map(s => {
327
+ const obj = { v: s.text };
328
+ if (s.scope)
329
+ obj.s = s.scope;
330
+ if (s.type)
331
+ obj.t = s.type;
332
+ if (s.xrefs && s.xrefs.length > 0)
333
+ obj.x = s.xrefs;
334
+ return JSON.stringify(obj);
335
+ });
336
+ return JSON.stringify(json);
337
+ }
338
+ export function flattenRelationships(relationships) {
339
+ if (!relationships || relationships.length === 0)
340
+ return '';
341
+ const json = relationships.map(r => {
342
+ const obj = { v: r.value };
343
+ if (r.qualifiers && r.qualifiers.length > 0)
344
+ obj.q = r.qualifiers;
345
+ return JSON.stringify({ t: r.type, ...obj });
346
+ });
347
+ return JSON.stringify(json);
348
+ }
349
+ export function flattenArray(arr) {
350
+ if (!arr || arr.length === 0)
351
+ return '';
352
+ return JSON.stringify(arr);
353
+ }
354
+ export function frameToCSVRow(frame) {
355
+ const row = {
356
+ frame_type: frame.frame_type,
357
+ id: frame.id,
358
+ name: frame.name,
359
+ namespace: frame.namespace,
360
+ def: frame.def,
361
+ comment: frame.comment,
362
+ is_obsolete: frame.is_obsolete,
363
+ created_by: frame.created_by,
364
+ creation_date: frame.creation_date,
365
+ alt_ids: flattenArray(frame.alt_ids),
366
+ is_anonymous: frame.is_anonymous,
367
+ synonyms: flattenSynonyms(frame.synonyms),
368
+ subsets: flattenArray(frame.subsets),
369
+ xrefs: flattenArray(frame.xrefs),
370
+ relationships: flattenRelationships(frame.relationships)
371
+ };
372
+ if (frame.frame_type === 'term' && 'is_a' in frame) {
373
+ const term = frame;
374
+ row.parent = term.is_a?.[0];
375
+ }
376
+ else if (frame.frame_type === 'typedef' && 'domain' in frame) {
377
+ const typedef = frame;
378
+ row.domain = typedef.domain;
379
+ row.range = typedef.range;
380
+ }
381
+ else if (frame.frame_type === 'instance' && 'instance_of' in frame) {
382
+ const instance = frame;
383
+ row.instance_of = instance.instance_of;
384
+ }
385
+ return row;
386
+ }
387
+ export function generateCSV(rows) {
388
+ if (rows.length === 0)
389
+ return '';
390
+ const headers = [
391
+ 'frame_type', 'id', 'name', 'namespace', 'def', 'comment',
392
+ 'is_obsolete', 'created_by', 'creation_date', 'alt_ids', 'is_anonymous',
393
+ 'synonyms', 'subsets', 'xrefs', 'relationships', 'parent', 'domain', 'range', 'instance_of'
394
+ ];
395
+ const headerRow = headers.join(',');
396
+ const dataRows = rows.map(row => {
397
+ return headers.map(header => {
398
+ const value = row[header];
399
+ if (value === undefined || value === null || value === '')
400
+ return '';
401
+ if (typeof value === 'boolean')
402
+ return value ? 'true' : 'false';
403
+ if (typeof value === 'string') {
404
+ const escaped = value.replace(/"/g, '""');
405
+ return `"${escaped}"`;
406
+ }
407
+ return '';
408
+ }).join(',');
409
+ });
410
+ return [headerRow, ...dataRows].join('\n');
411
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeyuan98/opencode-bioresearcher-plugin",
3
- "version": "1.5.0-alpha.0",
3
+ "version": "1.5.0",
4
4
  "description": "OpenCode plugin that adds a bioresearcher agent",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",