viatris-exam-cli 1.6.0 → 1.8.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.
package/README.md CHANGED
@@ -5,8 +5,17 @@
5
5
  ## 安装
6
6
 
7
7
  ```bash
8
- npm link
8
+ npm install -g viatris-exam-cli
9
9
  export VIATRIS_API_KEY="replace-with-real-key"
10
+ viatris-exam login
11
+ unset VIATRIS_API_KEY
12
+ ```
13
+
14
+ 开始任务前更新到最新版本:
15
+
16
+ ```bash
17
+ npm install -g viatris-exam-cli@latest
18
+ viatris-exam --version
10
19
  ```
11
20
 
12
21
  默认连接线上环境:
@@ -29,25 +38,27 @@ https://apaaspsc-app_17gbe211qq5-1879207268486311.aiforce.run/app/app_17gbe211qq
29
38
 
30
39
  `VIATRIS_API_ENV` 支持 `production`、`development`,以及别名 `prod`、`dev`。如需临时连接其他部署,可设置 `VIATRIS_API_BASE_URL`;该变量的优先级高于环境预设。
31
40
 
32
- CLI 不接受 `--api-key` 参数,避免密钥进入 shell history、进程列表或 Agent 日志。默认使用:
41
+ `login` 将 Key 写入 `~/.viatris-exam/.credentials.json`。隐藏目录权限为 `0700`,凭证文件权限为 `0600`。CLI 不接受 `--api-key` 参数,也不会输出 Key。
33
42
 
34
- ```text
35
- X-API-Key: <VIATRIS_API_KEY>
36
- ```
43
+ 读取顺序:
37
44
 
38
- 若服务端使用 Bearer Token:
45
+ 1. 优先读取隐藏凭证文件。
46
+ 2. 文件不存在时读取 `VIATRIS_API_KEY`。
39
47
 
40
- ```bash
41
- export VIATRIS_API_KEY_HEADER="Authorization"
42
- export VIATRIS_API_KEY_PREFIX="Bearer "
48
+ 默认认证格式:
49
+
50
+ ```text
51
+ Authorization: Bearer <API key>
43
52
  ```
44
53
 
54
+ 无需设置 `VIATRIS_API_KEY_HEADER` 或 `VIATRIS_API_KEY_PREFIX`。
55
+
45
56
  ## Agent 接口约定
46
57
 
47
58
  - API 成功响应以 JSON 写入 `stdout`。
48
59
  - 错误以单行结构化 JSON 写入 `stderr`。
49
60
  - POST/PUT 请求从文件、`stdin` 或 `--json` 读取 JSON 对象。
50
- - API Key 只从 `VIATRIS_API_KEY` 环境变量读取。
61
+ - API Key 优先从隐藏凭证文件读取,环境变量作为兜底。
51
62
  - 默认使用线上环境,可通过 `VIATRIS_API_ENV=development` 切换到开发环境。
52
63
  - `VIATRIS_API_BASE_URL` 可覆盖当前环境地址。
53
64
  - GET 请求遇到网络错误、HTTP 429 或 5xx 时自动重试,POST/PUT/PATCH/DELETE 不自动重试。
@@ -114,6 +125,8 @@ viatris-exam knowledge documents delete <document-id>
114
125
 
115
126
  `/knowledge` CRUD 响应中的 `level` 是物理存储层级(产品根为 1、固定维度为 2);产品生成接口使用业务层级(产品隐藏、固定维度为 L1、动态节点为 L2-L4)。
116
127
 
128
+ 所有文档写入都会在请求发出前从 `title`、`url`、`summary` 中移除“小荷”“小荷医疗”和“小荷 AI 医生”及其空格变体。当前写接口只接受 `title` 和 `summary`,输入中的 `url` 会在净化后从出站载荷移除,但 URL 净化能力作为永久兼容规则保留。该规则覆盖文档创建、批量 upsert、单文档更新和整树回写中的嵌套文档;标题清洗后为空会直接返回校验错误。
129
+
117
130
  ### 出题任务
118
131
 
119
132
  ```bash
@@ -196,14 +209,14 @@ printf '%s' '{"query":"阿托伐他汀 适应症","limit":20}' |
196
209
 
197
210
  ```text
198
211
  --base-url <url> 覆盖当前环境地址
199
- --key-header <name> 覆盖 VIATRIS_API_KEY_HEADER
200
- --key-prefix <value> 覆盖 VIATRIS_API_KEY_PREFIX
212
+ --key-header <name> 覆盖认证头,默认 Authorization
213
+ --key-prefix <value> 覆盖认证前缀,默认 "Bearer "
201
214
  --timeout <ms> 超时时间,默认 30000
202
215
  --retries <n> GET 重试次数,默认 2
203
216
  --compact 输出单行紧凑 JSON
204
217
  ```
205
218
 
206
- 地址选择优先级为 `--base-url`、`VIATRIS_API_BASE_URL`、`VIATRIS_API_ENV` 对应的预设地址。认证 Key 仍必须通过 `VIATRIS_API_KEY` 提供。
219
+ 地址选择优先级为 `--base-url`、`VIATRIS_API_BASE_URL`、`VIATRIS_API_ENV` 对应的预设地址。认证 Key 优先使用隐藏凭证文件,文件不存在时使用 `VIATRIS_API_KEY`。
207
220
 
208
221
  ## 项目结构
209
222
 
@@ -217,7 +230,10 @@ src/
217
230
  ├── args.js # 参数解析与校验
218
231
  ├── input.js # JSON 文件、stdin 与内联输入
219
232
  ├── config.js # 环境、地址与认证配置
233
+ ├── credentials.js # 隐藏凭证文件读写
220
234
  ├── http.js # HTTP、重试与响应处理
235
+ ├── sanitizers/
236
+ │ └── documents.js # 文档永久净化规则
221
237
  ├── metadata.js # 版本、命令和环境元数据
222
238
  ├── help.js # 帮助文本
223
239
  ├── output.js # 标准 JSON 输出
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viatris-exam-cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.1",
4
4
  "description": "Agent-friendly CLI for the Viatris exam OpenAPI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "test": "node --test",
15
- "check": "node --check src/*.js && node --check src/command-handlers/*.js",
15
+ "check": "node --check src/*.js && node --check src/command-handlers/*.js && node --check src/sanitizers/*.js",
16
16
  "check:versions": "node scripts/check-versions.js",
17
17
  "check:skill": "node scripts/check-skill.js",
18
18
  "pack:skill": "cd skills && /bin/rm -f viatris-exam-cli.zip && /usr/bin/zip -qr viatris-exam-cli.zip viatris-exam-cli -x '*.DS_Store'"
@@ -1,27 +1,31 @@
1
1
  ---
2
2
  name: viatris-exam-cli
3
- description: Operate the Viatris exam OpenAPI through its agent-friendly CLI. Use for products, knowledge nodes or documents, generation tasks, exam records, scoring, exams, questions, or medical search.
4
- version: 1.6.0
3
+ description: Operate the Viatris exam OpenAPI through its agent-friendly CLI. Use for login, products, knowledge, generation tasks, exam records, scoring, exams, questions, or medical search.
4
+ version: 1.8.1
5
5
  ---
6
6
 
7
7
  # Viatris Exam CLI
8
8
 
9
- Use the repository-local CLI for every supported API operation. Do not call the HTTP endpoints directly when the CLI covers the task.
9
+ Use the installed CLI for every supported API operation. Do not call the HTTP endpoints directly when the CLI covers the task.
10
10
 
11
11
  ## Start
12
12
 
13
- 1. Run commands from the repository root.
14
- 2. Require `VIATRIS_API_KEY` in the environment. Never place the key in arguments, files, output, or logs.
15
- 3. Use production by default. Set `VIATRIS_API_ENV=development` only when the user requests development or testing.
16
- 4. Run `node src/cli.js describe --compact` when capability discovery or exact machine-readable metadata is needed.
17
- 5. Read [references/cli-reference.md](references/cli-reference.md) before constructing a write payload or handling a nonzero exit code.
13
+ 1. Read the install and update sections in [references/cli-reference.md](references/cli-reference.md).
14
+ 2. Install `viatris-exam-cli` from npm when unavailable.
15
+ 3. Update to `viatris-exam-cli@latest` at the start of a task so capability instructions match the installed CLI.
16
+ 4. Verify with `viatris-exam --version`.
17
+ 5. Prefer the saved credential. To initialize or rotate it, set `VIATRIS_API_KEY` and run `viatris-exam login`; never place the key in command arguments, output, or logs.
18
+ 6. Use production by default. Set `VIATRIS_API_ENV=development` only when the user requests development or testing.
19
+ 7. Run `viatris-exam describe --compact` when capability discovery or exact machine-readable metadata is needed.
20
+ 8. Read the relevant command section in the reference before constructing a write payload or handling a nonzero exit code.
18
21
 
19
22
  ## Execute
20
23
 
21
- - Invoke the CLI as `node src/cli.js ...`. Do not require `npm link`.
24
+ - Invoke commands through the installed `viatris-exam` binary.
22
25
  - Prefer `--input <file>` for existing payloads and stdin for generated payloads.
23
26
  - Parse successful `stdout` as JSON.
24
27
  - Parse failed `stderr` as JSON and branch on both exit code and `error.type`.
28
+ - Authentication defaults to `Authorization: Bearer <key>`. Read the hidden credential file first and use `VIATRIS_API_KEY` only when no saved credential exists.
25
29
  - Use `products list` to resolve product master IDs and stable codes before product-specific workflows. `products create` requires only a Chinese name and also initializes the canonical knowledge-tree root.
26
30
  - Use `products update` only for verified optional medical master data. Do not infer English names, generic names, indications, dosage forms, or remarks without evidence.
27
31
  - Preserve `externalRef` when retrying question submission. It is the idempotency key.
@@ -29,6 +33,8 @@ Use the repository-local CLI for every supported API operation. Do not call the
29
33
  - Apply every returned generation rule. Product-scoped rules identify applicable product codes through `products`; global rules return an empty `products` array.
30
34
  - Prefer scoring rule V2 for generated questions and scoring results. Use stable point and deduction-rule IDs.
31
35
  - Treat product master records as outside business knowledge-tree levels. Fixed dimensions are business L1; generated nodes are L2-L4. Product detail lookup also accepts a historical knowledge-tree root ID.
36
+ - Document writes automatically remove 小荷 source-brand labels, including 小荷医疗 and whitespace variants of 小荷 AI 医生, from title, URL, and summary. Never reintroduce these labels after CLI sanitization.
37
+ - Current document write APIs accept only `title` and `summary`; the CLI drops an input `url` before sending. Keep URL sanitization in the permanent sanitizer for backward and future compatibility.
32
38
  - Treat knowledge-tree and recursive node deletion as destructive. Inspect the target first and perform them only when the user explicitly requests deletion.
33
39
  - Do not automatically retry POST, PUT, PATCH, or DELETE commands. Inspect the returned error and determine whether the server accepted the request.
34
40
  - Keep temporary payloads outside the repository unless they are requested deliverables.
@@ -1,13 +1,40 @@
1
1
  # CLI Reference
2
2
 
3
+ ## Install
4
+
5
+ Install the CLI globally so every example below can use the `viatris-exam` command directly:
6
+
7
+ ```bash
8
+ npm install -g viatris-exam-cli
9
+ viatris-exam --version
10
+ ```
11
+
12
+ ## Update
13
+
14
+ Update before starting a task so the installed capabilities and this Skill remain aligned:
15
+
16
+ ```bash
17
+ npm install -g viatris-exam-cli@latest
18
+ viatris-exam --version
19
+ ```
20
+
3
21
  ## Authentication And Environment
4
22
 
5
- Required:
23
+ Login once:
6
24
 
7
25
  ```bash
8
26
  export VIATRIS_API_KEY="..."
27
+ viatris-exam login
28
+ unset VIATRIS_API_KEY
9
29
  ```
10
30
 
31
+ Login writes `~/.viatris-exam/.credentials.json` with file mode `0600`; its containing directory uses mode `0700`. The command never prints the key. Run login again with a new environment value to rotate the saved key.
32
+
33
+ API key lookup order:
34
+
35
+ 1. Hidden credential file.
36
+ 2. `VIATRIS_API_KEY` environment variable when the file does not exist.
37
+
11
38
  Environment selection:
12
39
 
13
40
  | Value | Target |
@@ -17,25 +44,25 @@ Environment selection:
17
44
 
18
45
  `VIATRIS_API_BASE_URL` overrides the selected environment. Use it only for an explicitly supplied custom deployment or local mock.
19
46
 
20
- Default authentication header:
47
+ Default authentication:
21
48
 
22
49
  ```text
23
- X-API-Key: <VIATRIS_API_KEY>
50
+ Authorization: Bearer <API key>
24
51
  ```
25
52
 
26
- For Bearer authentication:
53
+ No header or prefix environment variables are required. Advanced overrides remain available when connecting to a different compatible service:
27
54
 
28
55
  ```bash
29
- export VIATRIS_API_KEY_HEADER="Authorization"
30
- export VIATRIS_API_KEY_PREFIX="Bearer "
56
+ export VIATRIS_API_KEY_HEADER="Custom-Header"
57
+ export VIATRIS_API_KEY_PREFIX=""
31
58
  ```
32
59
 
33
60
  ## Capability Discovery
34
61
 
35
62
  ```bash
36
- node src/cli.js describe --compact
37
- node src/cli.js --help
38
- node src/cli.js --version
63
+ viatris-exam describe --compact
64
+ viatris-exam --help
65
+ viatris-exam --version
39
66
  ```
40
67
 
41
68
  `describe` does not require credentials. It returns commands, paths, environment metadata, authentication metadata, and exit codes as JSON.
@@ -45,7 +72,7 @@ node src/cli.js --version
45
72
  List product master data:
46
73
 
47
74
  ```bash
48
- node src/cli.js products list --compact
75
+ viatris-exam products list --compact
49
76
  ```
50
77
 
51
78
  The response is `{ "items": [...] }`, ordered by `sortOrder` and then name. Each record includes the stable `code`, Chinese name, optional English name, generic name, indication, dosage form, `active` or `inactive` status, remark, and timestamps.
@@ -53,7 +80,7 @@ The response is `{ "items": [...] }`, ordered by `sortOrder` and then name. Each
53
80
  Create a product and initialize its canonical knowledge-tree root:
54
81
 
55
82
  ```bash
56
- node src/cli.js products create --json '{
83
+ viatris-exam products create --json '{
57
84
  "name": "络活喜",
58
85
  "genericName": "苯磺酸氨氯地平片",
59
86
  "indication": "高血压、冠心病",
@@ -68,7 +95,7 @@ Only `name` is required and the Chinese name must be unique. `code` is optional;
68
95
  Get a product and its fixed L1 dimensions:
69
96
 
70
97
  ```bash
71
- node src/cli.js products get "$PRODUCT_ID" --compact
98
+ viatris-exam products get "$PRODUCT_ID" --compact
72
99
  ```
73
100
 
74
101
  Use the product master `id`; historical knowledge-tree root IDs remain accepted for compatibility. The response also contains nullable `nameEn`, `genericName`, `indication`, `dosageForm`, and `remark`. Product master data is outside business levels L1-L4. Use each returned `fixedDimensions[].clientRef` as the parent reference for an L2 node. Each fixed dimension also returns `level: 1` and a display `codePrefix`.
@@ -76,7 +103,7 @@ Use the product master `id`; historical knowledge-tree root IDs remain accepted
76
103
  Update verified optional medical master data:
77
104
 
78
105
  ```bash
79
- node src/cli.js products update "$PRODUCT_ID" --json '{
106
+ viatris-exam products update "$PRODUCT_ID" --json '{
80
107
  "nameEn": "Lipitor",
81
108
  "genericName": "阿托伐他汀钙片",
82
109
  "indication": "高胆固醇血症、混合型血脂异常及心血管疾病预防",
@@ -90,7 +117,7 @@ Only supplied fields are updated. Each supplied value must be a nonempty string
90
117
  Replace a product knowledge tree:
91
118
 
92
119
  ```bash
93
- node src/cli.js products update-knowledge-tree "$PRODUCT_ID" \
120
+ viatris-exam products update-knowledge-tree "$PRODUCT_ID" \
94
121
  --input knowledge-tree.json \
95
122
  --compact
96
123
  ```
@@ -126,7 +153,6 @@ Payload shape:
126
153
  "documents": [
127
154
  {
128
155
  "title": "药品说明书",
129
- "url": "https://example.com/source",
130
156
  "summary": "与该知识点直接对应的证据摘录"
131
157
  }
132
158
  ]
@@ -140,7 +166,7 @@ Submit the complete L2/L3/L4 node set, between 15 and 500 nodes. Every `clientRe
140
166
  Delete all dynamic knowledge-tree content:
141
167
 
142
168
  ```bash
143
- node src/cli.js products delete-knowledge-tree "$PRODUCT_ID"
169
+ viatris-exam products delete-knowledge-tree "$PRODUCT_ID"
144
170
  ```
145
171
 
146
172
  This destructive operation preserves the product root and five fixed L1 dimensions, deletes all L2-L4 nodes and evidence documents, and unbinds affected questions. Inspect the product and tree first, and execute only when the user explicitly requests the full reset.
@@ -150,7 +176,7 @@ This destructive operation preserves the product root and five fixed L1 dimensio
150
176
  Get the complete physical tree and all product documents:
151
177
 
152
178
  ```bash
153
- node src/cli.js knowledge tree lipitor --compact
179
+ viatris-exam knowledge tree lipitor --compact
154
180
  ```
155
181
 
156
182
  The incremental `/knowledge` responses expose physical storage levels where the product root is 1 and fixed dimensions are 2. This differs from the product-generation business taxonomy, where the product is hidden, fixed dimensions are L1, and generated content is L2-L4.
@@ -158,7 +184,7 @@ The incremental `/knowledge` responses expose physical storage levels where the
158
184
  Create or reuse a visible path:
159
185
 
160
186
  ```bash
161
- node src/cli.js knowledge nodes by-path --json \
187
+ viatris-exam knowledge nodes by-path --json \
162
188
  '{"product":"lipitor","path":["疾病知识","血脂异常基础","ASCVD 风险评估"]}'
163
189
  ```
164
190
 
@@ -167,20 +193,20 @@ The path has one to three entries. The first entry must be a fixed dimension nam
167
193
  Create or reuse one node under a known parent:
168
194
 
169
195
  ```bash
170
- node src/cli.js knowledge nodes create --json \
196
+ viatris-exam knowledge nodes create --json \
171
197
  '{"parentId":"PARENT_ID","name":"ASCVD 风险评估","sortOrder":20}'
172
198
  ```
173
199
 
174
200
  Read, update, move, or delete a node:
175
201
 
176
202
  ```bash
177
- node src/cli.js knowledge nodes get "$NODE_ID"
178
- node src/cli.js knowledge nodes update "$NODE_ID" \
203
+ viatris-exam knowledge nodes get "$NODE_ID"
204
+ viatris-exam knowledge nodes update "$NODE_ID" \
179
205
  --json '{"name":"血脂异常与他汀治疗","sortOrder":5}'
180
- node src/cli.js knowledge nodes move "$NODE_ID" \
206
+ viatris-exam knowledge nodes move "$NODE_ID" \
181
207
  --json '{"newParentId":"NEW_PARENT_ID","sortOrder":30}'
182
- node src/cli.js knowledge nodes delete "$NODE_ID"
183
- node src/cli.js knowledge nodes delete "$NODE_ID" --recursive
208
+ viatris-exam knowledge nodes delete "$NODE_ID"
209
+ viatris-exam knowledge nodes delete "$NODE_ID" --recursive
184
210
  ```
185
211
 
186
212
  Product roots and fixed dimensions are protected from rename, move, and deletion. Moving cannot cross products, target the node itself or its descendants, or produce levels deeper than L4. Plain deletion is the safe default and returns HTTP 409 when children or question bindings exist. `--recursive` deletes the full subtree and documents and unbinds affected questions. Inspect the node first and use recursive deletion only when explicitly requested.
@@ -188,22 +214,34 @@ Product roots and fixed dimensions are protected from rename, move, and deletion
188
214
  Manage documents:
189
215
 
190
216
  ```bash
191
- node src/cli.js knowledge documents list "$NODE_ID"
192
- node src/cli.js knowledge documents create "$NODE_ID" \
193
- --json '{"title":"说明书","url":"https://example.com/source","summary":"摘录"}'
194
- node src/cli.js knowledge documents upsert "$NODE_ID" \
217
+ viatris-exam knowledge documents list "$NODE_ID"
218
+ viatris-exam knowledge documents create "$NODE_ID" \
219
+ --json '{"title":"说明书","summary":"摘录"}'
220
+ viatris-exam knowledge documents upsert "$NODE_ID" \
195
221
  --json '{"items":[{"title":"说明书","summary":"更新后的摘录"}]}'
196
- node src/cli.js knowledge documents update "$DOCUMENT_ID" \
197
- --json '{"url":null,"summary":"更新后的摘要"}'
198
- node src/cli.js knowledge documents delete "$DOCUMENT_ID"
222
+ viatris-exam knowledge documents update "$DOCUMENT_ID" \
223
+ --json '{"summary":"更新后的摘要"}'
224
+ viatris-exam knowledge documents delete "$DOCUMENT_ID"
199
225
  ```
200
226
 
201
- Create reuses a document with the same title under the node. Batch upsert accepts 1-20 items and updates `url` and `summary` for existing titles. A single-document update changes only supplied fields; send `null` to clear `url` or `summary`. Inspect a document before an explicitly requested delete.
227
+ Create reuses a document with the same title under the node. Batch upsert accepts 1-20 items and updates `summary` for existing titles. A single-document update changes only supplied fields; send `summary: null` to clear the summary. Current write contracts accept only `title` and `summary`; if input contains `url`, the CLI drops it before sending. Read responses may still contain historical URL data. Inspect a document before an explicitly requested delete.
228
+
229
+ ### Document Sanitization
230
+
231
+ Before every document write, the CLI removes source-brand labels from `title`, `url`, and `summary`. This applies to document create, batch upsert, single-document update, and documents nested in `products update-knowledge-tree`. URL filtering remains a permanent compatibility rule even though the current API drops URL from write payloads.
232
+
233
+ Removed forms include:
234
+
235
+ - `小荷`
236
+ - `小荷医疗`, including whitespace between `小荷` and `医疗`
237
+ - `小荷AI医生` with arbitrary whitespace, including `小荷 AI 医生`, `小荷 AI医生`, and `小荷AI 医生`
238
+
239
+ Whitespace is normalized after removal, and leading or trailing title separators are removed. If a supplied title becomes empty, the CLI returns `validation_error` without sending the request.
202
240
 
203
241
  List generation tasks:
204
242
 
205
243
  ```bash
206
- node src/cli.js gen-tasks list \
244
+ viatris-exam gen-tasks list \
207
245
  --status generating \
208
246
  --limit 20 \
209
247
  --compact
@@ -214,7 +252,7 @@ node src/cli.js gen-tasks list \
214
252
  Get one generation task:
215
253
 
216
254
  ```bash
217
- node src/cli.js gen-tasks get "$TASK_ID" --compact
255
+ viatris-exam gen-tasks get "$TASK_ID" --compact
218
256
  ```
219
257
 
220
258
  For a leaf-node task, `knowledgeContext` is the mandatory target context. For a product-wide task, `dimension` is `all`, `knowledgeContext` is null, and `knowledgeContexts` contains every leaf context. Generate `questionsPerNode` questions for every listed context and follow all returned `rules`.
@@ -224,7 +262,7 @@ Each rule has `scope` and `products`. For `scope: "product"`, `products` contain
224
262
  Submit generated questions:
225
263
 
226
264
  ```bash
227
- node src/cli.js gen-tasks submit "$TASK_ID" --input payload.json --compact
265
+ viatris-exam gen-tasks submit "$TASK_ID" --input payload.json --compact
228
266
  ```
229
267
 
230
268
  Payload shape:
@@ -295,13 +333,13 @@ Omit `knowledgeNodeId` for ordinary tasks. For a product-wide task it is require
295
333
  Get a submitted exam record and its frozen answers:
296
334
 
297
335
  ```bash
298
- node src/cli.js exam-records get "$RECORD_ID" --compact
336
+ viatris-exam exam-records get "$RECORD_ID" --compact
299
337
  ```
300
338
 
301
339
  Get the complete scoring rules bound to the record:
302
340
 
303
341
  ```bash
304
- node src/cli.js exam-records scoring-rules "$RECORD_ID" --compact
342
+ viatris-exam exam-records scoring-rules "$RECORD_ID" --compact
305
343
  ```
306
344
 
307
345
  The response includes the scoring `taskId`, `schemaVersion`, frozen question versions, evidence, V2 deduction rules, score caps, and item IDs. Use its `taskId` for scoring submission.
@@ -309,13 +347,13 @@ The response includes the scoring `taskId`, `schemaVersion`, frozen question ver
309
347
  Get a scoring task:
310
348
 
311
349
  ```bash
312
- node src/cli.js scoring get "$TASK_ID" --compact
350
+ viatris-exam scoring get "$TASK_ID" --compact
313
351
  ```
314
352
 
315
353
  Submit scoring results:
316
354
 
317
355
  ```bash
318
- node src/cli.js scoring submit "$TASK_ID" --input results.json --compact
356
+ viatris-exam scoring submit "$TASK_ID" --input results.json --compact
319
357
  ```
320
358
 
321
359
  Payload shape:
@@ -347,7 +385,7 @@ For V2, submit exactly one `pointJudgement` per scoring point. IDs must match th
347
385
  Create an exam:
348
386
 
349
387
  ```bash
350
- node src/cli.js exams create --input exam.json --compact
388
+ viatris-exam exams create --input exam.json --compact
351
389
  ```
352
390
 
353
391
  Required top-level fields are `name`, `product`, `questionCount`, `startAt`, `endAt`, `resultPolicy`, and `assigneeIds`. `questionCount` is 1 through 150. Use either `questions` or legacy `questionIds`, never both. Each question score must be a positive integer and the complete exam must total exactly 100.
@@ -355,7 +393,7 @@ Required top-level fields are `name`, `product`, `questionCount`, `startAt`, `en
355
393
  Create questions directly:
356
394
 
357
395
  ```bash
358
- node src/cli.js questions create --input questions.json --compact
396
+ viatris-exam questions create --input questions.json --compact
359
397
  ```
360
398
 
361
399
  Payload shape is `{ "items": [...] }`. Each item requires `product`, `dimension`, and `content`; provide `externalRef` for idempotency. Prefer `scoringRuleVersion: 2`, stable IDs on scoring points and deduction rules, and explicit `scoreCapRules`.
@@ -365,7 +403,7 @@ The API has partial-success semantics: inspect `accepted`, `duplicates`, and eve
365
403
  Add a question version:
366
404
 
367
405
  ```bash
368
- node src/cli.js questions version "$QUESTION_ID" --input version.json --compact
406
+ viatris-exam questions version "$QUESTION_ID" --input version.json --compact
369
407
  ```
370
408
 
371
409
  Required fields are `stem`, `referenceAnswer`, nonempty `scoringPoints`, `evidence`, and `editReason`. V2 versions may also include `scoringRuleVersion: 2`, deduction rules, and `scoreCapRules`.
@@ -373,7 +411,7 @@ Required fields are `stem`, `referenceAnswer`, nonempty `scoringPoints`, `eviden
373
411
  Search medical evidence:
374
412
 
375
413
  ```bash
376
- node src/cli.js medical-search search "阿托伐他汀 适应症" \
414
+ viatris-exam medical-search search "阿托伐他汀 适应症" \
377
415
  --file-type drug \
378
416
  --limit 20 \
379
417
  --max-length 1200 \
@@ -384,7 +422,7 @@ Or use JSON:
384
422
 
385
423
  ```bash
386
424
  printf '%s' '{"query":"阿托伐他汀 适应症","limit":20}' |
387
- node src/cli.js medical-search search --compact
425
+ viatris-exam medical-search search --compact
388
426
  ```
389
427
 
390
428
  Do not combine query flags with JSON input. `limit` accepts 1-20 and defaults to 20 when omitted. `maxLength` defaults to 1200.
@@ -396,10 +434,10 @@ The response `rawJson` is already a structured object. Read `rawJson.documents`
396
434
  Choose exactly one input form:
397
435
 
398
436
  ```bash
399
- node src/cli.js questions create --input payload.json
400
- node src/cli.js questions create --input - < payload.json
401
- node src/cli.js questions create --json '{"items":[]}'
402
- printf '%s' '{"items":[]}' | node src/cli.js questions create
437
+ viatris-exam questions create --input payload.json
438
+ viatris-exam questions create --input - < payload.json
439
+ viatris-exam questions create --json '{"items":[]}'
440
+ printf '%s' '{"items":[]}' | viatris-exam questions create
403
441
  ```
404
442
 
405
443
  The top-level JSON value must be an object.
Binary file
package/src/app.js CHANGED
@@ -1,6 +1,11 @@
1
- import { assertNoExtraPositionals, parseArgs } from "./args.js";
1
+ import {
2
+ assertNoExtraPositionals,
3
+ assertOptions,
4
+ parseArgs,
5
+ } from "./args.js";
2
6
  import { resolveCommand } from "./commands.js";
3
7
  import { configFrom } from "./config.js";
8
+ import { loginWithEnvironmentKey } from "./credentials.js";
4
9
  import { helpText } from "./help.js";
5
10
  import { request } from "./http.js";
6
11
  import { describeCli, VERSION } from "./metadata.js";
@@ -28,9 +33,16 @@ export async function run(
28
33
  writeJson(stdout, describeCli(), options.compact);
29
34
  return;
30
35
  }
36
+ if (positionals[0] === "login") {
37
+ assertNoExtraPositionals(positionals, 1);
38
+ assertOptions(options);
39
+ const result = await loginWithEnvironmentKey(environmentVariables);
40
+ writeJson(stdout, result, options.compact);
41
+ return;
42
+ }
31
43
 
32
44
  const command = await resolveCommand(positionals, options);
33
- const config = configFrom(options, environmentVariables);
45
+ const config = await configFrom(options, environmentVariables);
34
46
  const response = await request(command, config);
35
47
  writeJson(stdout, response, options.compact);
36
48
  }
package/src/cli.js CHANGED
File without changes
@@ -4,10 +4,23 @@ import {
4
4
  requiredId,
5
5
  } from "../args.js";
6
6
  import { readJsonInput } from "../input.js";
7
-
8
- function jsonCommand(method, path, options) {
7
+ import {
8
+ sanitizeDocumentBatchPayload,
9
+ sanitizeDocumentPayload,
10
+ } from "../sanitizers/documents.js";
11
+
12
+ async function jsonCommand(
13
+ method,
14
+ path,
15
+ options,
16
+ transform = (body) => body,
17
+ ) {
9
18
  assertOptions(options, ["input", "json"]);
10
- return readJsonInput(options).then((body) => ({ method, path, body }));
19
+ return {
20
+ method,
21
+ path,
22
+ body: transform(await readJsonInput(options)),
23
+ };
11
24
  }
12
25
 
13
26
  function nodePath(positionals, label) {
@@ -96,6 +109,7 @@ export async function resolveKnowledge(positionals, options) {
96
109
  "POST",
97
110
  `/openapi/knowledge/nodes/${id}/documents`,
98
111
  options,
112
+ sanitizeDocumentPayload,
99
113
  );
100
114
  }
101
115
 
@@ -106,6 +120,7 @@ export async function resolveKnowledge(positionals, options) {
106
120
  "PUT",
107
121
  `/openapi/knowledge/nodes/${id}/documents`,
108
122
  options,
123
+ sanitizeDocumentBatchPayload,
109
124
  );
110
125
  }
111
126
 
@@ -116,6 +131,7 @@ export async function resolveKnowledge(positionals, options) {
116
131
  "PUT",
117
132
  `/openapi/knowledge/documents/${id}`,
118
133
  options,
134
+ sanitizeDocumentPayload,
119
135
  );
120
136
  }
121
137
 
@@ -4,6 +4,7 @@ import {
4
4
  requiredId,
5
5
  } from "../args.js";
6
6
  import { readJsonInput } from "../input.js";
7
+ import { sanitizeKnowledgeTreePayload } from "../sanitizers/documents.js";
7
8
 
8
9
  export async function resolveProducts(positionals, options) {
9
10
  const [resource, action] = positionals;
@@ -50,7 +51,7 @@ export async function resolveProducts(positionals, options) {
50
51
  return {
51
52
  method: "PUT",
52
53
  path: `/openapi/products/${id}/knowledge-tree`,
53
- body: await readJsonInput(options),
54
+ body: sanitizeKnowledgeTreePayload(await readJsonInput(options)),
54
55
  };
55
56
  }
56
57
 
package/src/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { integerOption } from "./args.js";
2
+ import { readStoredApiKey } from "./credentials.js";
2
3
  import { CliError } from "./errors.js";
3
4
  import {
4
5
  API_ENVIRONMENT_ALIASES,
@@ -7,7 +8,10 @@ import {
7
8
  DEFAULT_TIMEOUT_MS,
8
9
  } from "./metadata.js";
9
10
 
10
- export function configFrom(options, environmentVariables = process.env) {
11
+ export async function configFrom(
12
+ options,
13
+ environmentVariables = process.env,
14
+ ) {
11
15
  const baseUrlOverride =
12
16
  options.base_url ?? environmentVariables.VIATRIS_API_BASE_URL;
13
17
  let environment = null;
@@ -28,13 +32,16 @@ export function configFrom(options, environmentVariables = process.env) {
28
32
  baseUrl = API_ENVIRONMENTS[environment];
29
33
  }
30
34
 
31
- const apiKey = environmentVariables.VIATRIS_API_KEY;
35
+ const storedApiKey = await readStoredApiKey(environmentVariables);
36
+ const apiKey = storedApiKey ?? environmentVariables.VIATRIS_API_KEY;
32
37
  const keyHeader =
33
38
  options.key_header ??
34
39
  environmentVariables.VIATRIS_API_KEY_HEADER ??
35
- "X-API-Key";
40
+ "Authorization";
36
41
  const keyPrefix =
37
- options.key_prefix ?? environmentVariables.VIATRIS_API_KEY_PREFIX ?? "";
42
+ options.key_prefix ??
43
+ environmentVariables.VIATRIS_API_KEY_PREFIX ??
44
+ "Bearer ";
38
45
  const timeoutMs = integerOption(
39
46
  options.timeout ?? String(DEFAULT_TIMEOUT_MS),
40
47
  "timeout",
@@ -63,9 +70,11 @@ export function configFrom(options, environmentVariables = process.env) {
63
70
  );
64
71
  }
65
72
  if (!apiKey) {
66
- throw new CliError("config_error", "VIATRIS_API_KEY is required.", {
67
- exitCode: 3,
68
- });
73
+ throw new CliError(
74
+ "config_error",
75
+ "API key not found. Run `viatris-exam login` with VIATRIS_API_KEY set, or set VIATRIS_API_KEY for this request.",
76
+ { exitCode: 3 },
77
+ );
69
78
  }
70
79
  if (!/^[A-Za-z0-9-]+$/.test(keyHeader)) {
71
80
  throw new CliError("config_error", "API key header name is invalid.", {
@@ -77,6 +86,7 @@ export function configFrom(options, environmentVariables = process.env) {
77
86
  baseUrl: baseUrl.replace(/\/+$/, ""),
78
87
  environment,
79
88
  apiKey,
89
+ apiKeySource: storedApiKey === null ? "environment" : "credential_file",
80
90
  keyHeader,
81
91
  keyPrefix,
82
92
  timeoutMs,
@@ -0,0 +1,100 @@
1
+ import {
2
+ chmod,
3
+ mkdir,
4
+ readFile,
5
+ rename,
6
+ rm,
7
+ writeFile,
8
+ } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { CliError } from "./errors.js";
12
+
13
+ export const CREDENTIAL_FILE_DISPLAY =
14
+ "~/.viatris-exam/.credentials.json";
15
+
16
+ export function credentialFilePath(environmentVariables = process.env) {
17
+ const home = environmentVariables.HOME || homedir();
18
+ return join(home, ".viatris-exam", ".credentials.json");
19
+ }
20
+
21
+ export async function readStoredApiKey(
22
+ environmentVariables = process.env,
23
+ ) {
24
+ const path = credentialFilePath(environmentVariables);
25
+ let content;
26
+ try {
27
+ content = await readFile(path, "utf8");
28
+ } catch (error) {
29
+ if (error.code === "ENOENT") return null;
30
+ throw new CliError(
31
+ "config_error",
32
+ `Unable to read the credential file: ${error.message}`,
33
+ { exitCode: 3, details: { credentialFile: path } },
34
+ );
35
+ }
36
+
37
+ try {
38
+ const credentials = JSON.parse(content);
39
+ if (
40
+ credentials === null ||
41
+ typeof credentials !== "object" ||
42
+ typeof credentials.apiKey !== "string" ||
43
+ !credentials.apiKey
44
+ ) {
45
+ throw new Error("apiKey is missing or invalid");
46
+ }
47
+ return credentials.apiKey;
48
+ } catch (error) {
49
+ throw new CliError(
50
+ "config_error",
51
+ `Invalid credential file: ${error.message}`,
52
+ { exitCode: 3, details: { credentialFile: path } },
53
+ );
54
+ }
55
+ }
56
+
57
+ export async function loginWithEnvironmentKey(
58
+ environmentVariables = process.env,
59
+ ) {
60
+ const apiKey = environmentVariables.VIATRIS_API_KEY;
61
+ if (!apiKey) {
62
+ throw new CliError(
63
+ "config_error",
64
+ "VIATRIS_API_KEY is required for login.",
65
+ { exitCode: 3 },
66
+ );
67
+ }
68
+
69
+ const path = credentialFilePath(environmentVariables);
70
+ const directory = dirname(path);
71
+ const temporaryPath = join(
72
+ directory,
73
+ `.credentials.${process.pid}.${Date.now()}.tmp`,
74
+ );
75
+
76
+ try {
77
+ await mkdir(directory, { recursive: true, mode: 0o700 });
78
+ await chmod(directory, 0o700);
79
+ await writeFile(
80
+ temporaryPath,
81
+ `${JSON.stringify({ version: 1, apiKey }, null, 2)}\n`,
82
+ { encoding: "utf8", mode: 0o600 },
83
+ );
84
+ await rename(temporaryPath, path);
85
+ await chmod(path, 0o600);
86
+ } catch (error) {
87
+ await rm(temporaryPath, { force: true });
88
+ throw new CliError(
89
+ "config_error",
90
+ `Unable to save the credential file: ${error.message}`,
91
+ { exitCode: 3, details: { credentialFile: path } },
92
+ );
93
+ }
94
+
95
+ return {
96
+ status: "saved",
97
+ credentialFile: path,
98
+ permissions: "0600",
99
+ };
100
+ }
package/src/help.js CHANGED
@@ -5,9 +5,11 @@ export function helpText() {
5
5
 
6
6
  Usage:
7
7
  viatris-exam <resource> <action> [id|query] [options]
8
+ viatris-exam login
8
9
  viatris-exam describe
9
10
 
10
11
  Commands:
12
+ login
11
13
  products list
12
14
  products create --input <file|->
13
15
  products get <product-id>
@@ -48,8 +50,8 @@ Medical search:
48
50
 
49
51
  Global options:
50
52
  --base-url <url> Override the selected environment URL.
51
- --key-header <name> Override VIATRIS_API_KEY_HEADER (default: X-API-Key).
52
- --key-prefix <value> Override VIATRIS_API_KEY_PREFIX (default: empty).
53
+ --key-header <name> Override the auth header (default: Authorization).
54
+ --key-prefix <value> Override the auth prefix (default: "Bearer ").
53
55
  --timeout <ms> Request timeout (default: 30000).
54
56
  --retries <n> GET retries for 429/5xx/network errors (default: 2).
55
57
  --compact Emit compact JSON.
@@ -59,9 +61,12 @@ Global options:
59
61
  Environment:
60
62
  VIATRIS_API_ENV API environment: production (default) or development.
61
63
  VIATRIS_API_BASE_URL Optional URL override with highest priority.
62
- VIATRIS_API_KEY Required API key. It cannot be passed as a CLI argument.
64
+ VIATRIS_API_KEY Login input or fallback key when no saved key exists.
63
65
  VIATRIS_API_KEY_HEADER Optional authentication header name.
64
- VIATRIS_API_KEY_PREFIX Optional header value prefix, for example "Bearer ".
66
+ VIATRIS_API_KEY_PREFIX Optional authentication header value prefix.
67
+
68
+ Credentials:
69
+ ~/.viatris-exam/.credentials.json (read before VIATRIS_API_KEY, mode 0600)
65
70
 
66
71
  Agent contract:
67
72
  Successful API JSON is written to stdout. Structured errors are written to
package/src/metadata.js CHANGED
@@ -1,4 +1,7 @@
1
- export const VERSION = "1.6.0";
1
+ import { CREDENTIAL_FILE_DISPLAY } from "./credentials.js";
2
+ import { DOCUMENT_SANITIZATION_POLICY } from "./sanitizers/documents.js";
3
+
4
+ export const VERSION = "1.8.1";
2
5
  export const DEFAULT_TIMEOUT_MS = 30_000;
3
6
  export const DEFAULT_RETRIES = 2;
4
7
 
@@ -15,6 +18,12 @@ export const API_ENVIRONMENT_ALIASES = Object.freeze({
15
18
  });
16
19
 
17
20
  export const COMMANDS = Object.freeze([
21
+ {
22
+ command: "login",
23
+ local: true,
24
+ input: "VIATRIS_API_KEY environment variable",
25
+ summary: "Save the API key to the local hidden credential file.",
26
+ },
18
27
  {
19
28
  command: "products list",
20
29
  method: "GET",
@@ -113,7 +122,7 @@ export const COMMANDS = Object.freeze([
113
122
  command: "knowledge documents create <node-id>",
114
123
  method: "POST",
115
124
  path: "/openapi/knowledge/nodes/{id}/documents",
116
- input: "{title, url?, summary?}",
125
+ input: "{title, summary?}",
117
126
  summary: "Idempotently create a document under a node.",
118
127
  },
119
128
  {
@@ -127,7 +136,7 @@ export const COMMANDS = Object.freeze([
127
136
  command: "knowledge documents update <document-id>",
128
137
  method: "PUT",
129
138
  path: "/openapi/knowledge/documents/{id}",
130
- input: "{title?, url?, summary?}",
139
+ input: "{title?, summary?}",
131
140
  summary: "Update supplied fields on a knowledge document.",
132
141
  },
133
142
  {
@@ -235,7 +244,10 @@ export function describeCli() {
235
244
  authentication: {
236
245
  keyEnvironmentVariable: "VIATRIS_API_KEY",
237
246
  baseUrlEnvironmentVariable: "VIATRIS_API_BASE_URL",
238
- defaultHeader: "X-API-Key",
247
+ defaultHeader: "Authorization",
248
+ defaultPrefix: "Bearer ",
249
+ credentialFile: CREDENTIAL_FILE_DISPLAY,
250
+ keySourcePriority: ["credential_file", "environment"],
239
251
  },
240
252
  environments: {
241
253
  environmentVariable: "VIATRIS_API_ENV",
@@ -244,6 +256,7 @@ export function describeCli() {
244
256
  urls: API_ENVIRONMENTS,
245
257
  baseUrlOverrideEnvironmentVariable: "VIATRIS_API_BASE_URL",
246
258
  },
259
+ documentSanitization: DOCUMENT_SANITIZATION_POLICY,
247
260
  commands: COMMANDS,
248
261
  exitCodes: EXIT_CODES,
249
262
  };
@@ -0,0 +1,113 @@
1
+ import { CliError } from "../errors.js";
2
+
3
+ export const DOCUMENT_SANITIZATION_POLICY = Object.freeze({
4
+ fields: ["title", "url", "summary"],
5
+ writableFields: ["title", "summary"],
6
+ droppedWriteFields: ["url"],
7
+ removedLabels: [
8
+ "小荷",
9
+ "小荷医疗",
10
+ "小荷AI医生(AI 与医生之间允许任意空格)",
11
+ ],
12
+ appliesTo: [
13
+ "products update-knowledge-tree",
14
+ "knowledge documents create",
15
+ "knowledge documents upsert",
16
+ "knowledge documents update",
17
+ ],
18
+ emptyTitle: "validation_error",
19
+ });
20
+
21
+ const PROHIBITED_LABEL = /小荷\s*(?:AI\s*医生|医疗)?/giu;
22
+ const TITLE_EDGE_SEPARATOR = /^[\s\-—–_::||·]+|[\s\-—–_::||·]+$/gu;
23
+
24
+ function sanitizeText(value) {
25
+ return value
26
+ .replace(PROHIBITED_LABEL, "")
27
+ .replace(/[ \t]{2,}/g, " ")
28
+ .trim();
29
+ }
30
+
31
+ function sanitizeTitle(value) {
32
+ return sanitizeText(value).replace(TITLE_EDGE_SEPARATOR, "").trim();
33
+ }
34
+
35
+ export function sanitizeDocument(document, location = "document") {
36
+ if (
37
+ document === null ||
38
+ Array.isArray(document) ||
39
+ typeof document !== "object"
40
+ ) {
41
+ return document;
42
+ }
43
+
44
+ const sanitized = { ...document };
45
+ if (typeof sanitized.title === "string") {
46
+ sanitized.title = sanitizeTitle(sanitized.title);
47
+ if (!sanitized.title) {
48
+ throw new CliError(
49
+ "validation_error",
50
+ `${location}.title is empty after prohibited source labels are removed.`,
51
+ );
52
+ }
53
+ }
54
+ for (const field of ["url", "summary"]) {
55
+ if (typeof sanitized[field] === "string") {
56
+ sanitized[field] = sanitizeText(sanitized[field]);
57
+ }
58
+ }
59
+ return sanitized;
60
+ }
61
+
62
+ function prepareDocumentForWrite(document, location) {
63
+ const sanitized = sanitizeDocument(document, location);
64
+ if (
65
+ sanitized === null ||
66
+ Array.isArray(sanitized) ||
67
+ typeof sanitized !== "object"
68
+ ) {
69
+ return sanitized;
70
+ }
71
+ const { url: _unsupportedUrl, ...writableDocument } = sanitized;
72
+ return writableDocument;
73
+ }
74
+
75
+ export function sanitizeDocumentPayload(body) {
76
+ return prepareDocumentForWrite(body, "document");
77
+ }
78
+
79
+ export function sanitizeDocumentBatchPayload(body) {
80
+ if (!Array.isArray(body.items)) return body;
81
+ return {
82
+ ...body,
83
+ items: body.items.map((document, index) =>
84
+ prepareDocumentForWrite(document, `items[${index}]`),
85
+ ),
86
+ };
87
+ }
88
+
89
+ export function sanitizeKnowledgeTreePayload(body) {
90
+ if (!Array.isArray(body.nodes)) return body;
91
+ return {
92
+ ...body,
93
+ nodes: body.nodes.map((node, nodeIndex) => {
94
+ if (
95
+ node === null ||
96
+ Array.isArray(node) ||
97
+ typeof node !== "object" ||
98
+ !Array.isArray(node.documents)
99
+ ) {
100
+ return node;
101
+ }
102
+ return {
103
+ ...node,
104
+ documents: node.documents.map((document, documentIndex) =>
105
+ prepareDocumentForWrite(
106
+ document,
107
+ `nodes[${nodeIndex}].documents[${documentIndex}]`,
108
+ ),
109
+ ),
110
+ };
111
+ }),
112
+ };
113
+ }