claude-token-saver 3.28.0 → 3.28.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.en.md +8 -2
- package/README.md +8 -2
- package/package.json +1 -1
- package/src/doc2md.cjs +163 -19
package/README.en.md
CHANGED
|
@@ -397,12 +397,18 @@ Three situations, three different interception points:
|
|
|
397
397
|
That second row is measured, not assumed: a `.pdf` Read fires the hook, and a `.pptx` Read in the same session leaves no hook log entry at all.
|
|
398
398
|
|
|
399
399
|
```bash
|
|
400
|
-
claude-token-saver doc2md
|
|
401
|
-
claude-token-saver doc2md on # register the Read hook
|
|
400
|
+
claude-token-saver doc2md on # register the hooks (the converter installs itself)
|
|
402
401
|
claude-token-saver doc2md # check converter + hook registration
|
|
403
402
|
claude-token-saver doc2md report.pptx # convert by hand and see the result
|
|
403
|
+
claude-token-saver doc2md install-converter # only to get the install out of the way early
|
|
404
404
|
```
|
|
405
405
|
|
|
406
|
+
**The converter installs itself.** Any rollout step a person has to be told about is a step some of them skip, so the converter installs in the background the moment a document first shows up, and converts as soon as it is ready. Measured: about 30s for the first document (15s install plus markitdown's first import), then 3.7s for a new document and 0.1s on a cache hit. The `.fig` parser installs in half a second on the first Figma file.
|
|
407
|
+
|
|
408
|
+
It installs on first use rather than at `install` time: the venv is 47MB, and someone who never opens a document should not pay for it. Set `CTS_DOC2MD_NO_AUTOINSTALL=1` to turn the automatic install off.
|
|
409
|
+
|
|
410
|
+
**Python 3.10+ is required** — markitdown's own floor, and macOS still ships 3.9 as `/usr/bin/python3`. The venv is built on an interpreter chosen by version rather than by PATH order. Built on 3.9, pip resolves markitdown to a 2019 placeholder release (0.0.1a1): the install looks like it worked and every conversion then dies at import. This was found by walking into it. When nothing on the machine is new enough, the message points at `brew install python` instead of at an install command that cannot succeed.
|
|
411
|
+
|
|
406
412
|
The converter goes into a venv this tool owns (`<state dir>/doc2md-venv`): no system interpreter is touched, and uninstalling the CLI takes it along. An existing markitdown on `uv tool` or `PATH` is preferred over building a new one.
|
|
407
413
|
|
|
408
414
|
Conversion is [markitdown](https://github.com/microsoft/markitdown). Slide numbers, heading levels, tables, speaker notes and per-sheet headings all survive, and non-Latin text comes through intact.
|
package/README.md
CHANGED
|
@@ -389,12 +389,18 @@ doc2md 는 그 파일을 한 번 변환해 두고 원본 대신 변환본을 읽
|
|
|
389
389
|
두 번째 줄의 제약은 실측으로 확인한 것입니다. `.pdf` 를 Read 하면 훅이 실행되고, 같은 세션에서 `.pptx` 를 Read 하면 훅 로그에 아무 기록도 남지 않습니다.
|
|
390
390
|
|
|
391
391
|
```bash
|
|
392
|
-
claude-token-saver doc2md
|
|
393
|
-
claude-token-saver doc2md on # Read 훅 등록
|
|
392
|
+
claude-token-saver doc2md on # 훅 등록 (변환기는 첫 문서에서 자동 설치)
|
|
394
393
|
claude-token-saver doc2md # 변환기·훅 등록 상태 확인
|
|
395
394
|
claude-token-saver doc2md 보고서.pptx # 직접 변환해 결과 확인
|
|
395
|
+
claude-token-saver doc2md install-converter # 설치를 미리 끝내 두고 싶을 때만
|
|
396
396
|
```
|
|
397
397
|
|
|
398
|
+
**변환기는 알아서 깔립니다.** 팀에 배포할 때 각자 설치 명령을 실행하게 만들면 그 단계에서 빠지는 사람이 생깁니다. 그래서 문서가 처음 등장하는 시점에 변환기가 백그라운드로 설치되고, 설치가 끝나는 대로 곧바로 변환합니다. 실측으로 첫 문서는 약 30초(설치 15초 + markitdown 최초 임포트), 이후로는 새 문서 3.7초, 캐시 적중 0.1초입니다. `.fig` 파서는 첫 Figma 파일에서 0.5초 만에 깔립니다.
|
|
399
|
+
|
|
400
|
+
설치는 `install` 단계가 아니라 첫 사용 시점에 합니다. venv 가 47MB 라서, 문서를 다루지 않는 사람은 낼 이유가 없는 비용입니다. 자동 설치를 끄려면 `CTS_DOC2MD_NO_AUTOINSTALL=1` 을 설정하십시오.
|
|
401
|
+
|
|
402
|
+
**파이썬 3.10 이상이 필요합니다.** markitdown 의 요구 사항이고, macOS 기본 `/usr/bin/python3` 는 3.9 입니다. 이 도구는 PATH 순서를 따르지 않고 3.10 이상인 인터프리터를 골라 venv 를 만듭니다. 3.9 로 만들면 pip 가 markitdown 을 2019 년 자리표시자 릴리스(0.0.1a1)로 해석해서, 설치는 성공한 것처럼 보이지만 모든 변환이 임포트 단계에서 죽습니다. 실제로 이 함정을 밟고 잡았습니다. 3.10 이상이 아예 없으면 설치 명령을 안내하는 대신 `brew install python` 을 안내합니다.
|
|
403
|
+
|
|
398
404
|
변환기는 도구 전용 venv(`<상태 디렉터리>/doc2md-venv`)에 설치합니다. 시스템 파이썬을 건드리지 않고, CLI를 지우면 함께 사라집니다. 이미 `uv tool` 이나 다른 경로에 markitdown 이 있으면 그쪽을 먼저 씁니다.
|
|
399
405
|
|
|
400
406
|
변환은 [markitdown](https://github.com/microsoft/markitdown)이 담당하며, 슬라이드 번호와 제목 계층, 표, 발표자 노트, 시트 구분이 모두 남습니다. 한글도 깨지지 않습니다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.28.
|
|
3
|
+
"version": "3.28.1",
|
|
4
4
|
"description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet — post-hoc session analysis, no realtime router, no extra LLM calls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/doc2md.cjs
CHANGED
|
@@ -26,7 +26,7 @@ const fs = require('node:fs');
|
|
|
26
26
|
const os = require('node:os');
|
|
27
27
|
const path = require('node:path');
|
|
28
28
|
const crypto = require('node:crypto');
|
|
29
|
-
const { spawnSync } = require('node:child_process');
|
|
29
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
30
30
|
const ledger = require('./doc2md-ledger.cjs');
|
|
31
31
|
|
|
32
32
|
// Formats where the original is of no use to the model. Images are absent
|
|
@@ -226,6 +226,13 @@ function writeCache(filePath, markdown, extra) {
|
|
|
226
226
|
* memoized for the life of this process, and the caller memoizes across
|
|
227
227
|
* processes through the notice file.
|
|
228
228
|
*/
|
|
229
|
+
// The readiness test for an interpreter. Importing the *class* matters: a pip
|
|
230
|
+
// install creates the package directory long before it finishes writing into
|
|
231
|
+
// it, so a bare `import markitdown` reports success mid-install and the
|
|
232
|
+
// conversion then fails with "cannot import name 'MarkItDown'". Measured
|
|
233
|
+
// during the first-use auto-install, where the window is about a second wide.
|
|
234
|
+
const PROBE = 'from markitdown import MarkItDown';
|
|
235
|
+
|
|
229
236
|
let interpreterCache;
|
|
230
237
|
function findInterpreter() {
|
|
231
238
|
if (interpreterCache !== undefined) return interpreterCache;
|
|
@@ -244,7 +251,7 @@ function findInterpreter() {
|
|
|
244
251
|
);
|
|
245
252
|
for (const bin of candidates) {
|
|
246
253
|
try {
|
|
247
|
-
const probe = spawnSync(bin, ['-c',
|
|
254
|
+
const probe = spawnSync(bin, ['-c', PROBE], { timeout: 20_000, stdio: 'ignore' });
|
|
248
255
|
if (probe.status === 0) {
|
|
249
256
|
interpreterCache = bin;
|
|
250
257
|
return bin;
|
|
@@ -281,19 +288,53 @@ const EDIT_LIBS = ['python-pptx', 'python-docx', 'openpyxl'];
|
|
|
281
288
|
* for, it goes somewhere this tool owns, so uninstalling the CLI takes the
|
|
282
289
|
* whole thing with it and no system interpreter is touched.
|
|
283
290
|
*/
|
|
291
|
+
/**
|
|
292
|
+
* A Python new enough to run markitdown (3.10+), or null.
|
|
293
|
+
*
|
|
294
|
+
* Explicit version names are tried before the bare `python3` so a modern
|
|
295
|
+
* Homebrew interpreter wins over the system one regardless of PATH order.
|
|
296
|
+
*/
|
|
297
|
+
function findVenvBase() {
|
|
298
|
+
const candidates = [
|
|
299
|
+
'python3.14', 'python3.13', 'python3.12', 'python3.11', 'python3.10',
|
|
300
|
+
'python3', 'python',
|
|
301
|
+
];
|
|
302
|
+
for (const bin of candidates) {
|
|
303
|
+
const r = spawnSync(bin, ['-c', 'import sys; print("%d.%d" % sys.version_info[:2])'], {
|
|
304
|
+
encoding: 'utf8',
|
|
305
|
+
timeout: 20_000,
|
|
306
|
+
});
|
|
307
|
+
if (r.status !== 0) continue;
|
|
308
|
+
const [major, minor] = String(r.stdout).trim().split('.').map(Number);
|
|
309
|
+
if (major > 3 || (major === 3 && minor >= 10)) return { bin, version: `python ${major}.${minor}` };
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
284
314
|
function installConverter({ onProgress = () => {} } = {}) {
|
|
285
315
|
const venv = path.join(userDataDir(), 'doc2md-venv');
|
|
286
316
|
const target = managedPython();
|
|
287
317
|
|
|
288
318
|
if (!fs.existsSync(target)) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
319
|
+
// The base interpreter is chosen by version, not by whichever `python3`
|
|
320
|
+
// comes first on PATH. markitdown needs 3.10+, and macOS still ships 3.9
|
|
321
|
+
// as /usr/bin/python3: building the venv on that one installs a
|
|
322
|
+
// seven-year-old placeholder release (0.0.1a1) that has no MarkItDown
|
|
323
|
+
// class in it, and every conversion then fails at import time. Measured
|
|
324
|
+
// on this machine, where /usr/bin/python3 precedes Homebrew's 3.14.
|
|
325
|
+
const base = findVenvBase();
|
|
326
|
+
if (!base) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
reason: 'no-python',
|
|
330
|
+
detail: 'markitdown needs Python 3.10 or newer; none was found on PATH '
|
|
331
|
+
+ '(macOS /usr/bin/python3 is 3.9 — install a newer one, e.g. `brew install python`)',
|
|
332
|
+
};
|
|
294
333
|
}
|
|
295
|
-
|
|
296
|
-
|
|
334
|
+
onProgress(`creating ${venv} (${base.version})`);
|
|
335
|
+
const r = spawnSync(base.bin, ['-m', 'venv', venv], { encoding: 'utf8', timeout: 180_000 });
|
|
336
|
+
if (r.status !== 0) {
|
|
337
|
+
return { ok: false, reason: 'no-python', detail: (r.stderr || 'venv creation failed').slice(0, 300) };
|
|
297
338
|
}
|
|
298
339
|
}
|
|
299
340
|
|
|
@@ -308,7 +349,7 @@ function installConverter({ onProgress = () => {} } = {}) {
|
|
|
308
349
|
|
|
309
350
|
// The probe is the actual acceptance test: pip can exit 0 and still leave an
|
|
310
351
|
// interpreter that cannot import what was asked for.
|
|
311
|
-
const probe = spawnSync(target, ['-c',
|
|
352
|
+
const probe = spawnSync(target, ['-c', PROBE], { timeout: 60_000, stdio: 'ignore' });
|
|
312
353
|
if (probe.status !== 0) {
|
|
313
354
|
return { ok: false, reason: 'import-failed', detail: 'installed, but markitdown does not import' };
|
|
314
355
|
}
|
|
@@ -320,8 +361,8 @@ function installConverter({ onProgress = () => {} } = {}) {
|
|
|
320
361
|
/**
|
|
321
362
|
* Convert one file. Returns `{ ok: true, cacheFile, meta }`, or
|
|
322
363
|
* `{ ok: false, reason, detail }` where reason is one of:
|
|
323
|
-
* no-markitdown | too-
|
|
324
|
-
* convert-failed | timeout
|
|
364
|
+
* no-markitdown | python-too-old | no-figparser | too-large | sensitive |
|
|
365
|
+
* unsafe-archive | no-text | convert-failed | timeout
|
|
325
366
|
*
|
|
326
367
|
* Every failure is a reason to leave the original Read alone, never to break
|
|
327
368
|
* it. That is the whole contract with the hook.
|
|
@@ -370,7 +411,14 @@ function convert(filePath, { converter = CONVERTER, python: pythonOverride = nul
|
|
|
370
411
|
// Figma files take the Node converter; everything else goes to markitdown.
|
|
371
412
|
if (path.extname(filePath).toLowerCase() === '.fig') {
|
|
372
413
|
const fig2md = require('./fig2md.cjs');
|
|
373
|
-
|
|
414
|
+
let result = spawnFigConvert(fig2md, filePath);
|
|
415
|
+
// The .fig parser is an npm install of a few hundred KB, fast enough to
|
|
416
|
+
// wait for inline the first time a Figma file turns up.
|
|
417
|
+
if (!result.ok && result.reason === 'no-figparser'
|
|
418
|
+
&& process.env.CTS_DOC2MD_NO_AUTOINSTALL !== '1') {
|
|
419
|
+
fig2md.installFigParser(userDataDir());
|
|
420
|
+
result = spawnFigConvert(fig2md, filePath);
|
|
421
|
+
}
|
|
374
422
|
if (!result.ok) return result;
|
|
375
423
|
const written = writeCache(filePath, result.markdown, {
|
|
376
424
|
note: result.note, truncated: false, rows: 0, pages: 0, markupBytes: 0,
|
|
@@ -381,7 +429,21 @@ function convert(filePath, { converter = CONVERTER, python: pythonOverride = nul
|
|
|
381
429
|
// The override exists so tests can drive a stub converter with any Python at
|
|
382
430
|
// all: the normal search insists the interpreter can import markitdown,
|
|
383
431
|
// which would make the whole path untestable without the real package.
|
|
384
|
-
|
|
432
|
+
let python = pythonOverride || findInterpreter();
|
|
433
|
+
if (!python && !pythonOverride) {
|
|
434
|
+
// Telling someone to run the install command is wrong when the install
|
|
435
|
+
// cannot succeed on this machine. A too-old interpreter is a different
|
|
436
|
+
// problem with a different fix, so it gets its own reason rather than
|
|
437
|
+
// hiding behind "no converter".
|
|
438
|
+
if (!findVenvBase()) {
|
|
439
|
+
return {
|
|
440
|
+
ok: false,
|
|
441
|
+
reason: 'python-too-old',
|
|
442
|
+
detail: 'markitdown needs Python 3.10 or newer; none was found on PATH',
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (ensureConverterInstalled()) python = findInterpreter();
|
|
446
|
+
}
|
|
385
447
|
if (!python) return { ok: false, reason: 'no-markitdown' };
|
|
386
448
|
|
|
387
449
|
const run = spawnSync(python, [converter, filePath], {
|
|
@@ -421,6 +483,72 @@ function convert(filePath, { converter = CONVERTER, python: pythonOverride = nul
|
|
|
421
483
|
return { ok: true, cached: false, cacheFile: written.cacheFile, meta: written.meta };
|
|
422
484
|
}
|
|
423
485
|
|
|
486
|
+
/**
|
|
487
|
+
* First-use install, so nobody has to be told to run a setup command.
|
|
488
|
+
*
|
|
489
|
+
* A team rollout dies on any step a person has to be told about, so the
|
|
490
|
+
* converter installs itself the first time a document actually shows up. It
|
|
491
|
+
* is still lazy rather than part of `install`: the venv is 47MB, and someone
|
|
492
|
+
* who never opens a document should never pay for it.
|
|
493
|
+
*
|
|
494
|
+
* The install runs detached and the caller waits only briefly. A cold install
|
|
495
|
+
* measured 6 seconds on a fast connection, but a corporate network can be far
|
|
496
|
+
* slower, and a hook that blocks a prompt for a minute is worse than a
|
|
497
|
+
* document that converts on the next turn. So: start it, wait up to
|
|
498
|
+
* `waitMs`, and if it is still going, say so and let this turn proceed
|
|
499
|
+
* without the conversion.
|
|
500
|
+
*
|
|
501
|
+
* Returns true when a converter is ready to use right now.
|
|
502
|
+
*/
|
|
503
|
+
function ensureConverterInstalled({ waitMs = 15_000 } = {}) {
|
|
504
|
+
if (process.env.CTS_DOC2MD_NO_AUTOINSTALL === '1') return false;
|
|
505
|
+
if (findInterpreter()) return true;
|
|
506
|
+
|
|
507
|
+
const lock = path.join(userDataDir(), 'doc2md-install.lock');
|
|
508
|
+
let running = false;
|
|
509
|
+
try {
|
|
510
|
+
const started = JSON.parse(fs.readFileSync(lock, 'utf8')).startedAt;
|
|
511
|
+
// A lock older than the pip timeout is a crashed run, not a live one.
|
|
512
|
+
running = Number.isFinite(started) && Date.now() - started < 900_000;
|
|
513
|
+
} catch { /* no lock, or an unreadable one: treat as not running */ }
|
|
514
|
+
|
|
515
|
+
if (!running) {
|
|
516
|
+
try {
|
|
517
|
+
fs.mkdirSync(userDataDir(), { recursive: true });
|
|
518
|
+
fs.writeFileSync(lock, JSON.stringify({ startedAt: Date.now() }));
|
|
519
|
+
const cli = path.join(__dirname, '..', 'bin', 'cli.js');
|
|
520
|
+
// Detached, so a session that ends mid-install does not take the
|
|
521
|
+
// install with it — the next session finds it finished.
|
|
522
|
+
const child = spawn(process.execPath, [cli, 'doc2md', 'install-converter'], {
|
|
523
|
+
detached: true,
|
|
524
|
+
stdio: 'ignore',
|
|
525
|
+
});
|
|
526
|
+
child.unref();
|
|
527
|
+
} catch {
|
|
528
|
+
try { fs.rmSync(lock, { force: true }); } catch { /* best effort */ }
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Poll cheaply: the interpreter file appearing is the first sign, and the
|
|
534
|
+
// import probe is the acceptance test. interpreterCache has to be cleared
|
|
535
|
+
// or the memoized null from the top of this function would stick.
|
|
536
|
+
const deadline = Date.now() + waitMs;
|
|
537
|
+
while (Date.now() < deadline) {
|
|
538
|
+
if (fs.existsSync(managedPython())) {
|
|
539
|
+
interpreterCache = undefined;
|
|
540
|
+
if (findInterpreter()) {
|
|
541
|
+
try { fs.rmSync(lock, { force: true }); } catch { /* best effort */ }
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// A synchronous sleep, because every caller here is synchronous. 400ms
|
|
546
|
+
// keeps the poll count low over a 15s wait.
|
|
547
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 400);
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
424
552
|
/** Where the "markitdown is not installed" notice records that it was shown. */
|
|
425
553
|
function noticePath() {
|
|
426
554
|
return path.join(userDataDir(), 'doc2md-notice.json');
|
|
@@ -490,9 +618,19 @@ function decideForRead(context, opts = {}) {
|
|
|
490
618
|
markNoticeShown();
|
|
491
619
|
return {
|
|
492
620
|
deny: false,
|
|
493
|
-
reason: `[doc2md] ${name} 를
|
|
494
|
-
+
|
|
495
|
-
+
|
|
621
|
+
reason: `[doc2md] ${name} 를 변환할 변환기를 지금 설치하고 있습니다(첫 실행에만 걸립니다).\n`
|
|
622
|
+
+ ' 설치가 끝나면 다음 요청부터 자동으로 변환합니다. 이번 turn 은 원본을 그대로 읽습니다.\n'
|
|
623
|
+
+ ` 진행 상황: ${INSTALL_HINT} 를 직접 실행하면 설치 로그를 볼 수 있습니다.`,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (result.reason === 'python-too-old') {
|
|
627
|
+
if (noticeAlreadyShown()) return null;
|
|
628
|
+
markNoticeShown();
|
|
629
|
+
return {
|
|
630
|
+
deny: false,
|
|
631
|
+
reason: `[doc2md] ${name} 를 변환하지 못했습니다. 변환기(markitdown)는 Python 3.10 이상이 필요한데 PATH 에서 찾지 못했습니다.\n`
|
|
632
|
+
+ ' macOS 기본 /usr/bin/python3 는 3.9 입니다. `brew install python` 으로 새 버전을 설치한 뒤 다시 시도하십시오.\n'
|
|
633
|
+
+ ' 그때까지는 원본을 그대로 읽습니다. 이 안내는 한 번만 표시됩니다.',
|
|
496
634
|
};
|
|
497
635
|
}
|
|
498
636
|
if (result.reason === 'no-text') {
|
|
@@ -613,8 +751,12 @@ function contextForPrompt(payload, opts = {}) {
|
|
|
613
751
|
}
|
|
614
752
|
} else if (result.reason === 'no-markitdown') {
|
|
615
753
|
lines.push(lang === 'ko'
|
|
616
|
-
? ` ${name}:
|
|
617
|
-
: ` ${name}:
|
|
754
|
+
? ` ${name}: 변환기를 설치하는 중입니다(첫 실행에만 걸립니다). 설치가 끝나면 다음 요청부터 자동 변환됩니다.`
|
|
755
|
+
: ` ${name}: the converter is installing now (first run only). It will convert automatically from the next request.`);
|
|
756
|
+
} else if (result.reason === 'python-too-old') {
|
|
757
|
+
lines.push(lang === 'ko'
|
|
758
|
+
? ` ${name}: 변환기가 Python 3.10 이상을 요구하는데 PATH 에 없습니다(macOS 기본은 3.9). \`brew install python\` 후 다시 시도하도록 사용자에게 안내하십시오.`
|
|
759
|
+
: ` ${name}: the converter needs Python 3.10+, and none is on PATH (macOS ships 3.9). Tell the user to install a newer Python, e.g. \`brew install python\`.`);
|
|
618
760
|
} else if (result.reason === 'no-figparser') {
|
|
619
761
|
lines.push(lang === 'ko'
|
|
620
762
|
? ` ${name}: .fig 파서가 없어 변환하지 못했습니다. 설치: claude-token-saver doc2md install-converter`
|
|
@@ -744,6 +886,8 @@ module.exports = {
|
|
|
744
886
|
MARKITDOWN_SPEC,
|
|
745
887
|
managedPython,
|
|
746
888
|
installConverter,
|
|
889
|
+
ensureConverterInstalled,
|
|
890
|
+
findVenvBase,
|
|
747
891
|
clearNotice,
|
|
748
892
|
cacheDir,
|
|
749
893
|
cachePathFor,
|