@yeyuan98/opencode-bioresearcher-plugin 1.5.0-alpha.0 → 1.5.1

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 (133) hide show
  1. package/README.md +48 -36
  2. package/dist/index.js +8 -6
  3. package/dist/skills/bioresearcher-tests/README.md +90 -0
  4. package/dist/skills/bioresearcher-tests/SKILL.md +255 -0
  5. package/dist/skills/bioresearcher-tests/pyproject.toml +6 -0
  6. package/dist/skills/bioresearcher-tests/resources/json_samples/in_markdown.md.gz +0 -0
  7. package/dist/skills/bioresearcher-tests/resources/json_samples/nested_object.json.gz +0 -0
  8. package/dist/skills/bioresearcher-tests/resources/json_samples/schema_draft7.json.gz +0 -0
  9. package/dist/skills/bioresearcher-tests/resources/json_samples/simple_array.json.gz +0 -0
  10. package/dist/skills/bioresearcher-tests/resources/json_samples/simple_object.json.gz +0 -0
  11. package/dist/skills/bioresearcher-tests/resources/obo_sample.obo.gz +0 -0
  12. package/dist/skills/bioresearcher-tests/resources/pubmed_sample.xml.gz +0 -0
  13. package/dist/skills/bioresearcher-tests/resources/table_sample.xlsx.gz +0 -0
  14. package/dist/skills/bioresearcher-tests/test_cases/json_tests.md +137 -0
  15. package/dist/skills/bioresearcher-tests/test_cases/misc_tests.md +141 -0
  16. package/dist/skills/bioresearcher-tests/test_cases/parser_tests.md +80 -0
  17. package/dist/skills/bioresearcher-tests/test_cases/skill_tests.md +59 -0
  18. package/dist/skills/bioresearcher-tests/test_cases/table_tests.md +194 -0
  19. package/dist/skills/bioresearcher-tests/test_runner.py +607 -0
  20. package/dist/skills/env-jsonc-setup/SKILL.md +206 -206
  21. package/dist/skills/long-table-summary/SKILL.md +224 -153
  22. package/dist/skills/long-table-summary/combine_outputs.py +55 -9
  23. package/dist/skills/long-table-summary/generate_prompts.py +9 -0
  24. package/dist/skills/pubmed-weekly/pubmed_weekly.py +130 -29
  25. package/dist/{db-tools → tools/db}/backends/mysql/translator.js +23 -23
  26. package/dist/{db-tools → tools/db}/tools.js +34 -34
  27. package/dist/{misc-tools → tools/misc}/json-validate.js +4 -5
  28. package/dist/tools/parser/obo/index.d.ts +2 -0
  29. package/dist/tools/parser/obo/index.js +2 -0
  30. package/dist/tools/parser/obo/obo.d.ts +17 -0
  31. package/dist/tools/parser/obo/obo.js +216 -0
  32. package/dist/tools/parser/obo/types.d.ts +166 -0
  33. package/dist/tools/parser/obo/utils.d.ts +21 -0
  34. package/dist/tools/parser/obo/utils.js +411 -0
  35. package/dist/tools/parser/pubmed/types.js +1 -0
  36. package/dist/{skill-tools → tools/skill}/registry.js +1 -1
  37. package/package.json +1 -1
  38. package/dist/db-tools/executor.d.ts +0 -13
  39. package/dist/db-tools/executor.js +0 -54
  40. package/dist/db-tools/pool.d.ts +0 -8
  41. package/dist/db-tools/pool.js +0 -49
  42. package/dist/db-tools/tools/index.d.ts +0 -27
  43. package/dist/db-tools/tools/index.js +0 -191
  44. package/dist/db-tools/types.d.ts +0 -94
  45. package/dist/db-tools/types.js +0 -40
  46. package/dist/misc-tools/json-tools.d.ts +0 -33
  47. package/dist/misc-tools/json-tools.js +0 -187
  48. package/dist/skill/frontmatter.d.ts +0 -2
  49. package/dist/skill/frontmatter.js +0 -65
  50. package/dist/skill/index.d.ts +0 -3
  51. package/dist/skill/index.js +0 -2
  52. package/dist/skill/registry.d.ts +0 -11
  53. package/dist/skill/registry.js +0 -64
  54. package/dist/skill/tool.d.ts +0 -9
  55. package/dist/skill/tool.js +0 -115
  56. package/dist/skill/types.d.ts +0 -22
  57. package/dist/skill/types.js +0 -7
  58. /package/dist/{db-tools → tools/db}/backends/index.d.ts +0 -0
  59. /package/dist/{db-tools → tools/db}/backends/index.js +0 -0
  60. /package/dist/{db-tools → tools/db}/backends/mongodb/backend.d.ts +0 -0
  61. /package/dist/{db-tools → tools/db}/backends/mongodb/backend.js +0 -0
  62. /package/dist/{db-tools → tools/db}/backends/mongodb/connection.d.ts +0 -0
  63. /package/dist/{db-tools → tools/db}/backends/mongodb/connection.js +0 -0
  64. /package/dist/{db-tools → tools/db}/backends/mongodb/index.d.ts +0 -0
  65. /package/dist/{db-tools → tools/db}/backends/mongodb/index.js +0 -0
  66. /package/dist/{db-tools → tools/db}/backends/mongodb/translator.d.ts +0 -0
  67. /package/dist/{db-tools → tools/db}/backends/mongodb/translator.js +0 -0
  68. /package/dist/{db-tools → tools/db}/backends/mysql/backend.d.ts +0 -0
  69. /package/dist/{db-tools → tools/db}/backends/mysql/backend.js +0 -0
  70. /package/dist/{db-tools → tools/db}/backends/mysql/connection.d.ts +0 -0
  71. /package/dist/{db-tools → tools/db}/backends/mysql/connection.js +0 -0
  72. /package/dist/{db-tools → tools/db}/backends/mysql/index.d.ts +0 -0
  73. /package/dist/{db-tools → tools/db}/backends/mysql/index.js +0 -0
  74. /package/dist/{db-tools → tools/db}/backends/mysql/translator.d.ts +0 -0
  75. /package/dist/{db-tools → tools/db}/core/base.d.ts +0 -0
  76. /package/dist/{db-tools → tools/db}/core/base.js +0 -0
  77. /package/dist/{db-tools → tools/db}/core/config-loader.d.ts +0 -0
  78. /package/dist/{db-tools → tools/db}/core/config-loader.js +0 -0
  79. /package/dist/{db-tools → tools/db}/core/index.d.ts +0 -0
  80. /package/dist/{db-tools → tools/db}/core/index.js +0 -0
  81. /package/dist/{db-tools → tools/db}/core/jsonc-parser.d.ts +0 -0
  82. /package/dist/{db-tools → tools/db}/core/jsonc-parser.js +0 -0
  83. /package/dist/{db-tools → tools/db}/core/validator.d.ts +0 -0
  84. /package/dist/{db-tools → tools/db}/core/validator.js +0 -0
  85. /package/dist/{db-tools → tools/db}/index.d.ts +0 -0
  86. /package/dist/{db-tools → tools/db}/index.js +0 -0
  87. /package/dist/{db-tools → tools/db}/interface/backend.d.ts +0 -0
  88. /package/dist/{db-tools → tools/db}/interface/backend.js +0 -0
  89. /package/dist/{db-tools → tools/db}/interface/connection.d.ts +0 -0
  90. /package/dist/{db-tools → tools/db}/interface/connection.js +0 -0
  91. /package/dist/{db-tools → tools/db}/interface/index.d.ts +0 -0
  92. /package/dist/{db-tools → tools/db}/interface/index.js +0 -0
  93. /package/dist/{db-tools → tools/db}/interface/query.d.ts +0 -0
  94. /package/dist/{db-tools → tools/db}/interface/query.js +0 -0
  95. /package/dist/{db-tools → tools/db}/interface/schema.d.ts +0 -0
  96. /package/dist/{db-tools → tools/db}/interface/schema.js +0 -0
  97. /package/dist/{db-tools → tools/db}/tools.d.ts +0 -0
  98. /package/dist/{db-tools → tools/db}/utils.d.ts +0 -0
  99. /package/dist/{db-tools → tools/db}/utils.js +0 -0
  100. /package/dist/{misc-tools → tools/misc}/calculator.d.ts +0 -0
  101. /package/dist/{misc-tools → tools/misc}/calculator.js +0 -0
  102. /package/dist/{misc-tools → tools/misc}/index.d.ts +0 -0
  103. /package/dist/{misc-tools → tools/misc}/index.js +0 -0
  104. /package/dist/{misc-tools → tools/misc}/json-extract.d.ts +0 -0
  105. /package/dist/{misc-tools → tools/misc}/json-extract.js +0 -0
  106. /package/dist/{misc-tools → tools/misc}/json-infer.d.ts +0 -0
  107. /package/dist/{misc-tools → tools/misc}/json-infer.js +0 -0
  108. /package/dist/{misc-tools → tools/misc}/json-validate.d.ts +0 -0
  109. /package/dist/{misc-tools → tools/misc}/timer.d.ts +0 -0
  110. /package/dist/{misc-tools → tools/misc}/timer.js +0 -0
  111. /package/dist/{parser-tools/pubmed → tools/parser/obo}/types.js +0 -0
  112. /package/dist/{parser-tools → tools/parser}/pubmed/index.d.ts +0 -0
  113. /package/dist/{parser-tools → tools/parser}/pubmed/index.js +0 -0
  114. /package/dist/{parser-tools → tools/parser}/pubmed/pubmed.d.ts +0 -0
  115. /package/dist/{parser-tools → tools/parser}/pubmed/pubmed.js +0 -0
  116. /package/dist/{parser-tools → tools/parser}/pubmed/types.d.ts +0 -0
  117. /package/dist/{parser-tools → tools/parser}/pubmed/utils.d.ts +0 -0
  118. /package/dist/{parser-tools → tools/parser}/pubmed/utils.js +0 -0
  119. /package/dist/{skill-tools → tools/skill}/frontmatter.d.ts +0 -0
  120. /package/dist/{skill-tools → tools/skill}/frontmatter.js +0 -0
  121. /package/dist/{skill-tools → tools/skill}/index.d.ts +0 -0
  122. /package/dist/{skill-tools → tools/skill}/index.js +0 -0
  123. /package/dist/{skill-tools → tools/skill}/registry.d.ts +0 -0
  124. /package/dist/{skill-tools → tools/skill}/tool.d.ts +0 -0
  125. /package/dist/{skill-tools → tools/skill}/tool.js +0 -0
  126. /package/dist/{skill-tools → tools/skill}/types.d.ts +0 -0
  127. /package/dist/{skill-tools → tools/skill}/types.js +0 -0
  128. /package/dist/{table-tools → tools/table}/index.d.ts +0 -0
  129. /package/dist/{table-tools → tools/table}/index.js +0 -0
  130. /package/dist/{table-tools → tools/table}/tools.d.ts +0 -0
  131. /package/dist/{table-tools → tools/table}/tools.js +0 -0
  132. /package/dist/{table-tools → tools/table}/utils.d.ts +0 -0
  133. /package/dist/{table-tools → tools/table}/utils.js +0 -0
@@ -21,6 +21,21 @@ import argparse
21
21
  from datetime import datetime, timedelta
22
22
  from typing import List, Dict, Any
23
23
 
24
+ MONTH_MAP = {
25
+ "Jan": 1,
26
+ "Feb": 2,
27
+ "Mar": 3,
28
+ "Apr": 4,
29
+ "May": 5,
30
+ "Jun": 6,
31
+ "Jul": 7,
32
+ "Aug": 8,
33
+ "Sep": 9,
34
+ "Oct": 10,
35
+ "Nov": 11,
36
+ "Dec": 12,
37
+ }
38
+
24
39
 
25
40
  def calculate_week() -> str:
26
41
  """Calculate the past week's date range (Monday-Sunday).
@@ -46,6 +61,116 @@ def calculate_week() -> str:
46
61
  return f"{week_start}-{week_end}"
47
62
 
48
63
 
64
+ def infer_year(month: int, day: int, hour: int, minute: int) -> int:
65
+ """Infer year for MMM DD HH:MM format.
66
+
67
+ Uses current year if date is not in the future.
68
+ Uses previous year if inferred date is in the future.
69
+
70
+ Args:
71
+ month: Month number (1-12)
72
+ day: Day of month
73
+ hour: Hour (0-23)
74
+ minute: Minute (0-59)
75
+
76
+ Returns:
77
+ Inferred year as integer
78
+ """
79
+ now = datetime.now()
80
+ date_this_year = datetime(now.year, month, day, hour, minute)
81
+
82
+ if date_this_year > now:
83
+ return now.year - 1
84
+ return now.year
85
+
86
+
87
+ def parse_ftp_listing_to_dict(content: str) -> Dict[str, datetime]:
88
+ """Parse FTP directory listing into {filename: datetime} dict.
89
+
90
+ Supports multiple date formats with regex fallback chain:
91
+ 1. Unix ls format - MMM DD HH:MM (current year)
92
+ 2. Unix ls format - MMM DD YYYY (older files)
93
+ 3. ISO 8601 format - YYYY-MM-DD HH:MM
94
+ 4. European format - DD-MMM-YYYY HH:MM
95
+
96
+ Args:
97
+ content: Raw FTP directory listing content
98
+
99
+ Returns:
100
+ Dictionary mapping filename to datetime object
101
+ """
102
+ file_dates = {}
103
+
104
+ for line in content.split("\n"):
105
+ line = line.strip()
106
+ if not line or line.startswith("total"):
107
+ continue
108
+
109
+ filename = None
110
+ file_date = None
111
+
112
+ match = re.match(
113
+ r"^\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+(\w{3})\s+(\d{1,2})\s+(\d{2}:\d{2})\s+(.+)$",
114
+ line,
115
+ )
116
+ if match:
117
+ month_str, day_str, time_str, fn = match.groups()
118
+ month = MONTH_MAP.get(month_str)
119
+ if month:
120
+ day = int(day_str)
121
+ hour, minute = map(int, time_str.split(":"))
122
+ year = infer_year(month, day, hour, minute)
123
+ filename = fn
124
+ file_date = datetime(year, month, day, hour, minute)
125
+
126
+ if not file_date:
127
+ match = re.match(
128
+ r"^\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+(\w{3})\s+(\d{1,2})\s+(\d{4})\s+(.+)$",
129
+ line,
130
+ )
131
+ if match:
132
+ month_str, day_str, year_str, fn = match.groups()
133
+ month = MONTH_MAP.get(month_str)
134
+ if month:
135
+ year = int(year_str)
136
+ day = int(day_str)
137
+ filename = fn
138
+ file_date = datetime(year, month, day)
139
+
140
+ if not file_date:
141
+ match = re.match(r"^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s+(.+)$", line)
142
+ if match:
143
+ date_str, time_str, fn = match.groups()
144
+ datetime_str = f"{date_str} {time_str}"
145
+ try:
146
+ file_date = datetime.strptime(datetime_str, "%Y-%m-%d %H:%M")
147
+ filename = fn
148
+ except ValueError:
149
+ pass
150
+
151
+ if not file_date:
152
+ match = re.match(
153
+ r"^(\d{1,2})-(\w{3})-(\d{4})\s+(\d{2}:\d{2})\s+(.+)$", line
154
+ )
155
+ if match:
156
+ day_str, month_str, year_str, time_str, fn = match.groups()
157
+ month = MONTH_MAP.get(month_str)
158
+ if month:
159
+ day = int(day_str)
160
+ year = int(year_str)
161
+ hour, minute = map(int, time_str.split(":"))
162
+ try:
163
+ file_date = datetime(year, month, day, hour, minute)
164
+ filename = fn
165
+ except ValueError:
166
+ pass
167
+
168
+ if filename and file_date and filename not in file_dates:
169
+ file_dates[filename] = file_date
170
+
171
+ return file_dates
172
+
173
+
49
174
  def parse_date_from_filename(filename: str) -> datetime | None:
50
175
  """Extract date from PubMed filename.
51
176
 
@@ -147,37 +272,13 @@ def filter_files_by_date(week_name: str, file_list: List[str]) -> List[str]:
147
272
  with urllib.request.urlopen(url) as response:
148
273
  content = response.read().decode("utf-8", errors="ignore")
149
274
 
150
- # Parse file listings with dates
151
- # NCBI FTP format uses ISO date: "2026-01-30 14:02"
152
- file_dates = {}
153
-
154
- for filename in file_list:
155
- # Find the line containing this file
156
- # Pattern: filename followed by non-digits, then date YYYY-MM-DD HH:MM
157
- pattern = re.escape(filename)
158
- match = re.search(
159
- rf"({pattern})[^0-9]*(\d{{4}}-\d{{2}}-\d{{2}})\s+(\d{{2}}:\d{{2}})",
160
- content,
161
- )
162
-
163
- if match:
164
- date_str = match.group(2)
165
- time_str = match.group(3)
166
- try:
167
- # Parse date in ISO format: "2026-01-30 14:02"
168
- file_date = datetime.strptime(
169
- f"{date_str} {time_str}", "%Y-%m-%d %H:%M"
170
- )
171
-
172
- file_dates[filename] = file_date
173
- except ValueError:
174
- continue
275
+ file_dates = parse_ftp_listing_to_dict(content)
175
276
 
176
- # Filter files within date range
277
+ # Filter files within date range AND in provided file_list
177
278
  filtered_files = [
178
- filename
179
- for filename, file_date in file_dates.items()
180
- if start_date <= file_date <= end_date
279
+ f
280
+ for f in file_list
281
+ if f in file_dates and start_date <= file_dates[f] <= end_date
181
282
  ]
182
283
 
183
284
  return sorted(filtered_files)
@@ -40,28 +40,28 @@ export function mapMySQLColumnsToSchema(rows) {
40
40
  comment: row.Comment || row.COLUMN_COMMENT || row.comment || null,
41
41
  }));
42
42
  }
43
- export const LIST_TABLES_SQL = `
44
- SELECT
45
- TABLE_NAME,
46
- TABLE_TYPE,
47
- ENGINE,
48
- TABLE_ROWS,
49
- CREATE_TIME,
50
- TABLE_COMMENT
51
- FROM information_schema.TABLES
52
- WHERE TABLE_SCHEMA = DATABASE()
53
- ORDER BY TABLE_NAME
43
+ export const LIST_TABLES_SQL = `
44
+ SELECT
45
+ TABLE_NAME,
46
+ TABLE_TYPE,
47
+ ENGINE,
48
+ TABLE_ROWS,
49
+ CREATE_TIME,
50
+ TABLE_COMMENT
51
+ FROM information_schema.TABLES
52
+ WHERE TABLE_SCHEMA = DATABASE()
53
+ ORDER BY TABLE_NAME
54
54
  `;
55
- export const DESCRIBE_TABLE_SQL = `
56
- SELECT
57
- COLUMN_NAME as Field,
58
- COLUMN_TYPE as Type,
59
- IS_NULLABLE as \`Null\`,
60
- COLUMN_KEY as \`Key\`,
61
- COLUMN_DEFAULT as \`Default\`,
62
- EXTRA as Extra,
63
- COLUMN_COMMENT as Comment
64
- FROM information_schema.COLUMNS
65
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
66
- ORDER BY ORDINAL_POSITION
55
+ export const DESCRIBE_TABLE_SQL = `
56
+ SELECT
57
+ COLUMN_NAME as Field,
58
+ COLUMN_TYPE as Type,
59
+ IS_NULLABLE as \`Null\`,
60
+ COLUMN_KEY as \`Key\`,
61
+ COLUMN_DEFAULT as \`Default\`,
62
+ EXTRA as Extra,
63
+ COLUMN_COMMENT as Comment
64
+ FROM information_schema.COLUMNS
65
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
66
+ ORDER BY ORDINAL_POSITION
67
67
  `;
@@ -13,17 +13,17 @@ async function getOrCreateBackend() {
13
13
  return backend;
14
14
  }
15
15
  export const dbQuery = tool({
16
- description: `Execute a SELECT query on the database with named parameters.
17
-
18
- **Examples:**
19
- - Simple query: \`SELECT * FROM users LIMIT 10\`
20
- - With named params: \`SELECT * FROM users WHERE status = :status AND created_at > :date\`
21
- - Join query: \`SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id\`
22
-
23
- **Parameters:**
24
- - Use named placeholders like \`:paramName\` for parameters
25
- - Pass parameter values in the \`params\` object: \`{ "status": "active", "date": "2024-01-01" }\`
26
-
16
+ description: `Execute a SELECT query on the database with named parameters.
17
+
18
+ **Examples:**
19
+ - Simple query: \`SELECT * FROM users LIMIT 10\`
20
+ - With named params: \`SELECT * FROM users WHERE status = :status AND created_at > :date\`
21
+ - Join query: \`SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id\`
22
+
23
+ **Parameters:**
24
+ - Use named placeholders like \`:paramName\` for parameters
25
+ - Pass parameter values in the \`params\` object: \`{ "status": "active", "date": "2024-01-01" }\`
26
+
27
27
  **Note:** Only SELECT statements are allowed (read-only access).`,
28
28
  args: {
29
29
  sql: z.string().describe('SELECT SQL query to execute. Use named placeholders like :name for parameters.'),
@@ -48,15 +48,15 @@ export const dbQuery = tool({
48
48
  },
49
49
  });
50
50
  export const dbListTables = tool({
51
- description: `List all tables/collections in the current database with metadata.
52
-
53
- **Returns:**
54
- - Table/collection names and types (TABLE, VIEW, COLLECTION)
55
- - Storage engine (InnoDB, MyISAM, etc.) for MySQL
56
- - Approximate row count
57
- - Creation time (MySQL only)
58
- - Table comments
59
-
51
+ description: `List all tables/collections in the current database with metadata.
52
+
53
+ **Returns:**
54
+ - Table/collection names and types (TABLE, VIEW, COLLECTION)
55
+ - Storage engine (InnoDB, MyISAM, etc.) for MySQL
56
+ - Approximate row count
57
+ - Creation time (MySQL only)
58
+ - Table comments
59
+
60
60
  **Usage:** Call this first to discover what tables are available before using dbDescribeTable or dbQuery.`,
61
61
  args: {},
62
62
  execute: async (_args, _context) => {
@@ -72,20 +72,20 @@ export const dbListTables = tool({
72
72
  },
73
73
  });
74
74
  export const dbDescribeTable = tool({
75
- description: `Get the column schema for a specific table/collection.
76
-
77
- **Returns for each column/field:**
78
- - Field name
79
- - Data type (VARCHAR, INT, TEXT, DATETIME, etc.)
80
- - Nullable status
81
- - Key type (PRI for primary key, UNI for unique, MUL for multiple)
82
- - Default value
83
- - Extra info (auto_increment, etc.)
84
- - Column comments
85
-
86
- **Workflow:**
87
- 1. Use \`dbListTables\` first to see available tables/collections
88
- 2. Use this tool to understand the column/field structure
75
+ description: `Get the column schema for a specific table/collection.
76
+
77
+ **Returns for each column/field:**
78
+ - Field name
79
+ - Data type (VARCHAR, INT, TEXT, DATETIME, etc.)
80
+ - Nullable status
81
+ - Key type (PRI for primary key, UNI for unique, MUL for multiple)
82
+ - Default value
83
+ - Extra info (auto_increment, etc.)
84
+ - Column comments
85
+
86
+ **Workflow:**
87
+ 1. Use \`dbListTables\` first to see available tables/collections
88
+ 2. Use this tool to understand the column/field structure
89
89
  3. Use \`dbQuery\` to query the data`,
90
90
  args: {
91
91
  table_name: z.string().describe('Name of the table/collection to describe. Use dbListTables to see available tables.'),
@@ -89,10 +89,8 @@ export const jsonValidate = tool({
89
89
  let parsedData;
90
90
  let parsedSchema;
91
91
  let schemaString = args.schema;
92
- const isWindowsPath = /^[A-Za-z]:[/\\]/.test(args.schema);
93
- const isUnixPath = /^\//.test(args.schema);
94
- const isRelativePath = /^\.{1,2}(\/|\\)/.test(args.schema);
95
- if (isWindowsPath || isUnixPath || isRelativePath) {
92
+ const looksLikeInlineJson = /^\s*[\[{]/.test(args.schema);
93
+ if (!looksLikeInlineJson) {
96
94
  const resolvedPath = path.isAbsolute(args.schema)
97
95
  ? args.schema
98
96
  : path.join(context.directory, args.schema);
@@ -110,7 +108,8 @@ export const jsonValidate = tool({
110
108
  hints: [
111
109
  'Check the file path for typos',
112
110
  'Ensure file exists and is accessible',
113
- 'Use a relative path from the project directory'
111
+ 'Use a relative path from the project directory',
112
+ 'Or provide schema as inline JSON string (starting with { or [)'
114
113
  ]
115
114
  }
116
115
  }, null, 2);
@@ -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
+ });