@uehaj/sys1grep 0.5.0-next.2 → 0.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
@@ -13,15 +13,15 @@ sys1grep asks "does this line match the meaning *network failure*?", gets a prob
13
13
  and applies a threshold.
14
14
 
15
15
  ```sh
16
- ./sys1grep -n -e "customer is angry or frustrated" tickets.txt
16
+ sys1grep -n -e "customer is angry or frustrated" tickets.txt
17
17
  ```
18
18
 
19
19
  [![sys1grep demo: a Japanese meaning finds refund requests in six languages; "asking for a refund" vs "about a refund"; -Q finds the answer](docs/demo.svg)](https://sys1grep.js.org/)
20
20
 
21
- <sub>▶ Click the demo, or open <a href="https://sys1grep.js.org/">uehaj.github.io/sys1grep</a>, for the full demo on the landing page.</sub>
21
+ <sub>▶ Click the demo, or open <a href="https://sys1grep.js.org/">sys1grep.js.org</a>, for the full demo on the landing page.</sub>
22
22
 
23
- - Zero dependencies. One file, Node.js 20.16+ and `fetch`.
24
- - Fast. 30 lines go into one request, requests run 8 at a time. A 210-line file finishes in under a second.
23
+ - Zero dependencies. Node.js 20.16+ and `fetch`, nothing else.
24
+ - Fast. 30 lines go into one request, requests run 8 at a time (1 for a local URL). A 210-line file finishes in under a second.
25
25
  - Meanings combine with AND / OR / NOT.
26
26
  - **Language-agnostic.** The meaning and the text can each be in any language. A Japanese meaning finds French, Russian, Chinese and Korean lines alike. No translation step, same speed, same cost.
27
27
 
@@ -33,7 +33,7 @@ so one query finds matching lines in every language the file contains.
33
33
  An **English** meaning finds **Japanese** lines. None of the hits contain "angry" or "frustrated", and two of them are in Japanese:
34
34
 
35
35
  ```sh
36
- $ ./sys1grep -n -e "customer is angry or frustrated" tests/corpus.txt
36
+ $ sys1grep -n -e "customer is angry or frustrated" tests/corpus.txt
37
37
  14:ユーザー山田さんからの問い合わせ: 返金してほしい、商品が壊れていた
38
38
  16:ユーザー佐藤さんからの問い合わせ: 注文した覚えのない請求が来ています。至急確認してください
39
39
  18:I want my money back. The item arrived broken and customer service ignored me.
@@ -44,7 +44,7 @@ $ ./sys1grep -n -e "customer is angry or frustrated" tests/corpus.txt
44
44
  A **Japanese** meaning finds **English** lines, with the same confidence as the Japanese ones:
45
45
 
46
46
  ```sh
47
- $ ./sys1grep -n -p -e "返金の要求" tests/corpus.txt
47
+ $ sys1grep -n -p -e "返金の要求" tests/corpus.txt
48
48
  14:ユーザー山田さんからの問い合わせ: 返金してほしい、商品が壊れていた [0.97]
49
49
  18:I want my money back. The item arrived broken and customer service ignored me. [0.95]
50
50
  ```
@@ -54,7 +54,7 @@ Russian, German, Spanish, Chinese and Korean. One Japanese meaning finds all six
54
54
  meaning does the same:
55
55
 
56
56
  ```sh
57
- $ ./sys1grep -n -p -e "顧客が返金を求めている" tests/multi.txt
57
+ $ sys1grep -n -p -e "顧客が返金を求めている" tests/multi.txt
58
58
  1:Je veux être remboursé, le produit est arrivé cassé. [0.98]
59
59
  3:Я требую вернуть деньги, товар не работает. [0.97]
60
60
  5:Ich möchte mein Geld zurück, das Gerät ist defekt. [0.97]
@@ -62,7 +62,7 @@ $ ./sys1grep -n -p -e "顧客が返金を求めている" tests/multi.txt
62
62
  9:我要求退款,商品坏了。 [0.97]
63
63
  11:환불해 주세요. 제품이 고장났어요. [0.97]
64
64
 
65
- $ ./sys1grep -n -p -e "клиент требует возврат денег" tests/multi.txt
65
+ $ sys1grep -n -p -e "клиент требует возврат денег" tests/multi.txt
66
66
  1:Je veux être remboursé, le produit est arrivé cassé. [0.94]
67
67
  3:Я требую вернуть деньги, товар не работает. [0.97]
68
68
  5:Ich möchte mein Geld zurück, das Gerät ist defekt. [0.92]
@@ -84,12 +84,12 @@ Jev reads the line and the question together (a cross-encoder shape), so who did
84
84
  "asked for" versus "already done" all change the answer. An embedding of the line is fixed before it
85
85
  ever sees your query, so it can only measure topical closeness.
86
86
 
87
- All eight lines below are "about a refund" (the same contrast as `tests/contrast.txt`, translated to
87
+ The first six lines below are "about a refund", and the last two are the pair the AND example further down uses (the same contrast as `tests/contrast.txt`, translated to
88
88
  English as `tests/contrast.en.txt` — see [Search across languages](#search-across-languages) above for
89
89
  the case where the meaning and the text differ in language). Only two are a customer asking for one:
90
90
 
91
91
  ```sh
92
- $ ./sys1grep -n -p -t 0 -e "customer is asking for a refund" tests/contrast.en.txt
92
+ $ sys1grep -n -p -t 0 -e "customer is asking for a refund" tests/contrast.en.txt
93
93
  1:I want a refund. The item was broken. [0.99]
94
94
  2:The refund has been processed. Please check your account. [0.11]
95
95
  3:Our refund policy is within 30 days of purchase. [0.09]
@@ -105,14 +105,14 @@ operations**, not a trick with set differences or "negative queries":
105
105
 
106
106
  ```sh
107
107
  # about a refund, but NOT a customer asking for one → completed, policy, denied, timelines
108
- $ ./sys1grep -n -e "about a refund" -v "the customer is asking for a refund" tests/contrast.en.txt
108
+ $ sys1grep -n -e "about a refund" -v "the customer is asking for a refund" tests/contrast.en.txt
109
109
  2:The refund has been processed. Please check your account.
110
110
  3:Our refund policy is within 30 days of purchase.
111
111
  4:The manager denied the refund request yesterday
112
112
  6:Refunds are processed within 5 business days
113
113
 
114
114
  # angry AND it is the customer, not the staff
115
- $ ./sys1grep -n -e "someone is angry" -a "the customer, not the staff, is the one acting" tests/contrast.en.txt
115
+ $ sys1grep -n -e "someone is angry" -a "the customer, not the staff, is the one acting" tests/contrast.en.txt
116
116
  5:I demand a full refund immediately
117
117
  8:The customer got angry and hung up the phone.
118
118
  ```
@@ -131,18 +131,24 @@ repeated queries over a large, fixed corpus a vector index is cheaper and faster
131
131
 
132
132
  ## Regex terms
133
133
 
134
- `-e`/`-a`/`-v '/pattern/flags'` (first and last character `/`, JavaScript flags) is matched locally,
135
- as a plain regex, with no request at all. It prefilters its AND term: only the lines it holds for
136
- ever ask that term's meanings, so a cheap regex in front of a meaning cuts both the bill and the wait. A line is sent
137
- only if some term's regexes all hold for it (a term with no regex holds for every line), so with
138
- `-e '/re/' -a A -e B` a line without `re` is still sent, asked `B` only. A query of regex terms alone
139
- sends nothing, even with `--unit=sentence-by-jev`: the rules alone then decide where wrapped lines join. Anything that isn't shaped like `/…/flags` is still a meaning, so
140
- `-e '/etc 以下のファイルを変更している'` (no closing `/`) is unaffected; a meaning that really starts
141
- and ends with `/` can be written with a leading space to dodge the regex reading.
134
+ `-P '/pattern/flags'` (first and last character `/`, JavaScript flags) starts a regex term, OR'd like
135
+ `-e`, and is matched locally with no request at all. `-a`/`-v '/pattern/flags'` AND / AND NOT a regex
136
+ onto the preceding term, the same shape deciding regex from meaning there. It prefilters its AND term:
137
+ only the lines it holds for ever ask that term's meanings, so a cheap regex in front of a meaning cuts
138
+ both the bill and the wait. A line is sent only if some term's regexes all hold for it (a term with no
139
+ regex holds for every line), so with `-P '/re/' -a A -e B` a line without `re` is still sent, asked `B`
140
+ only. A query of regex terms alone sends nothing, even with `--unit=sentence-by-jev`: the rules alone
141
+ then decide where wrapped lines join. `-P '!/RE/'` is not allowed; negate a regex with `-v '/RE/'`
142
+ instead. `-e` never reads its value as a regex: a `/RE/flags`-shaped value there is refused, with a
143
+ hint naming `-P` (e.g. `-e '/catch/' is no longer a regex: use -P '/catch/'`). `-Q` is unaffected: it
144
+ was already always a question, never a regex, so `-e '/etc 以下のファイルを変更している'` (no closing
145
+ `/`) still goes through as a meaning, unrefused. A meaning for `-e`/`-a`/`-v` that really starts and
146
+ ends with `/` can still be written with a leading space to dodge the regex reading (`-e ' /cat/'`
147
+ sends the text `" /cat/"`, not a refusal).
142
148
 
143
149
  ```sh
144
- $ ./sys1grep -e '/ERROR|FATAL/' app.log # no requests at all
145
- $ ./sys1grep -e '/timeout/i' -a '顧客に影響が出ている' app.log # only lines with "timeout" go to Jev
150
+ $ sys1grep -P '/ERROR|FATAL/' app.log # no requests at all
151
+ $ sys1grep -P '/timeout/i' -a '顧客に影響が出ている' app.log # only lines with "timeout" go to Jev
146
152
  ```
147
153
 
148
154
  A regex's named and numbered groups pass to the other meanings of the *same* AND term as
@@ -152,7 +158,7 @@ an error rather than an empty string (so is a reference to a negated regex's gro
152
158
  no group stays literal, as in ECMAScript, so `$100 以上の請求` is unaffected.
153
159
 
154
160
  ```sh
155
- $ ./sys1grep -e '/(?<date>\d{4}-\d\d-\d\d) (?<time>\d\d:\d\d)/' \
161
+ $ sys1grep -P '/(?<date>\d{4}-\d\d-\d\d) (?<time>\d\d:\d\d)/' \
156
162
  -a '$<time> が深夜(0時〜5時)であり、$<date> が週末である' app.log
157
163
  # 2026-09-19 03:12 ... → asks "03:12 が深夜(0時〜5時)であり、2026-09-19 が週末である"
158
164
  ```
@@ -165,7 +171,7 @@ is never colored. `-o` prints each match on a line of its own, as `grep -o`; a l
165
171
  whole, since a meaning has no matching part.
166
172
 
167
173
  ```sh
168
- $ ./sys1grep -o -n -e '/[A-Z]+-\d+/' -a 'the ticket is still open' notes.txt # the ticket ids, one per line
174
+ $ sys1grep -o -n -P '/[A-Z]+-\d+/' -a 'the ticket is still open' notes.txt # the ticket ids, one per line
169
175
  ```
170
176
 
171
177
  ## Sending less
@@ -184,9 +190,16 @@ to the narrowest:
184
190
  - **How many times.** [`--dedup`](#one-line-per-template---dedup) judges one line per template: lines that
185
191
  differ only in ids, numbers, times or paths share one answer. `auto`/`always`/`never`, off by default for
186
192
  now: a hint says when `--dedup=auto` would pay.
193
+ - **Never twice.** `--cache` keeps Jev's answers in `~/.config/sys1grep/cache`, keyed by the request sent (model,
194
+ lines, questions) and the endpoint's host, so the same search again sends nothing and costs nothing. It is off
195
+ by default, since the cache holds the lines searched; `SYS1GREP_CACHE=on` (in the environment or
196
+ `~/.config/sys1grep/.env`, not settings.json) sets the default. `--cache=only` answers from the cache alone: a
197
+ request not there is exit 2 unless `-q` already found a match, named, and nothing is sent. `--dry-run` says how
198
+ many requests are cached and how many it would send. There is no expiry: the model is in the key, and Jev
199
+ drifts about ±0.05 between runs, so a cached answer is one of those runs. `--cache=clear` empties it.
187
200
  - **Check before paying.** `--dry-run` sends nothing and prints the settings the search would run with, the
188
201
  files, how many lines each would send and every request with its questions. Its last line estimates the input
189
- tokens and, for TypeSafe itself, the price (`~3178 input tokens, ~$0.000133`; within about 10%). `-i` shows the
202
+ tokens and the price (`~3178 input tokens, ~$0.000133`; within about 10%; see "Other endpoints" for whose price). `-i` shows the
190
203
  same totals on the terminal and sends only after `y`.
191
204
  Before the bulk of requests, every target is sized (`--max-filesize`, default 10M); one over it is skipped
192
205
  outright, like `rg`'s own `--max-filesize`, named on stderr (`-y` does not affect it). A `.gz` is sized
@@ -197,20 +210,24 @@ to the narrowest:
197
210
  is sent. The input about to be sent is priced too (`--max-cost`,
198
211
  default 1 USD); over it, one question asks to continue on the terminal, `-y` answers it yes, and without a
199
212
  terminal it is exit 2 (this applies with `-q` too — scripts pass `-y`).
213
+ With `-r` or `git sys1grep`, a term no regex narrows that is about to send more than 10,000 lines gets one
214
+ stderr line with the same totals, naming the term (`sys1grep: sending 214,913 of 231,502 lines from 1,247 files
215
+ (~9.1M input tokens, ~$0.38); the term "…" has no regex to narrow it. Add -a '/RE/' to it, …`). The search goes
216
+ on; `-q` silences the line, `-y` does not.
200
217
 
201
218
  ```sh
202
- $ sys1grep --dry-run -r --include='*.log' --changed-within=today -e '/ERROR|FATAL/' -a 'a customer is affected' logs/
219
+ $ sys1grep --dry-run -r --include='*.log' --changed-within=today -P '/ERROR|FATAL/' -a 'a customer is affected' logs/
203
220
  ```
204
221
 
205
222
  `--verbose` (or `--dry-run`) also shows *where* a setting that was not typed on the command line came from
206
- (`SYS1GREP_OPTS`, an environment variable, `~/.config/sys1grep/.env`, or a preset's default), so a result that
223
+ (`SYS1GREP_OPTS`, an environment variable, `~/.config/sys1grep/settings.json` or `.env`, or a preset's default), so a result that
207
224
  surprises you can be traced back to its source. The key's value never appears, only which option or variable
208
225
  supplied it:
209
226
 
210
227
  ```sh
211
228
  $ SYS1GREP_OPTS='--level strict' sys1grep --verbose -e "the API key is read from a file" .
212
229
  sys1grep: endpoint api.typesafe.ai/v1/systemone (default), model jev-latest (default)
213
- sys1grep: key: SYS1GREP_API_KEY (~/.config/sys1grep/.env)
230
+ sys1grep: key: key (~/.config/sys1grep/settings.json)
214
231
  sys1grep: SYS1GREP_OPTS: --level strict
215
232
  sys1grep: options: --level strict (SYS1GREP_OPTS) = -t 0.7 -T 0.3, --chunk 30, -j 8, scope on, -M 2000, --max-filesize 10M, --max-cost 1
216
233
  sys1grep: file ./a.py: 120 lines, 120 to send
@@ -220,7 +237,7 @@ sys1grep: file ./a.py: 120 lines, 120 to send
220
237
  ## Install
221
238
 
222
239
  Two ways to use it: as a command-line tool (this section), or as a Claude Code skill
223
- (see [Use it from Claude Code](#use-it-from-claude-code) below). The skill falls back to `npx @uehaj/sys1grep`,
240
+ (see [Use it from Claude Code](#use-it-from-claude-code) below). The skill runs sys1grep through `npx`,
224
241
  so if you only use it through Claude Code you can skip the install here entirely and just set the API key.
225
242
 
226
243
  Requires Node.js 20.16 or later. No other dependencies.
@@ -240,14 +257,34 @@ Then give it an API key from the [TypeSafe console](https://console.typesafe.ai/
240
257
 
241
258
  ```sh
242
259
  export SYS1GREP_API_KEY=your-key # environment variable
243
- echo 'SYS1GREP_API_KEY=your-key' > ~/.config/sys1grep/.env # per user (mkdir -p first)
260
+ # per user: ~/.config/sys1grep/settings.json (mkdir -p first; keep it at 0600)
261
+ (umask 077; echo '{"key": "your-key"}' > ~/.config/sys1grep/settings.json)
244
262
  ```
245
263
 
246
- Variables already in the environment win; otherwise `~/.config/sys1grep/.env` fills them in. A `.env` in the current
264
+ Variables already in the environment win; otherwise `~/.config/sys1grep/settings.json` fills them in, one field per
265
+ variable:
266
+
267
+ | field | variable |
268
+ |---|---|
269
+ | `url` | `SYS1GREP_URL` |
270
+ | `key` | `SYS1GREP_API_KEY` (and `TYPESAFE_API_KEY`) |
271
+ | `model` | `SYS1GREP_MODEL` |
272
+ | `opts` | `SYS1GREP_OPTS`, as an array: `["--level", "strict", "-n"]` |
273
+ | `summarizer` | `SYS1GREP_SUMMARIZER` |
274
+ | `summarizerModel` | `SYS1GREP_SUMMARIZER_MODEL` |
275
+ | `summarizerKey` | `SYS1GREP_SUMMARIZER_API_KEY` |
276
+ | `lang` | `SYS1GREP_LANG` (`en` or `ja`: `--help`'s language, and `--serve`'s page default) |
277
+
278
+ Every field is optional. A value of the wrong type is an error, as is a file that is not JSON. A field the file does
279
+ not know only warns (on stderr, naming the field) and is ignored, so an older sys1grep keeps working once a newer one
280
+ adds a field. A file that holds a key and that others can read gets a warning. `~/.config/sys1grep/.env` (`NAME=value`
281
+ lines) still works, below settings.json: a value in settings.json wins over the same variable there. A `.env` in the current
247
282
  directory is never read: it may belong to a repository you just cloned, and could send your key elsewhere through
248
283
  `SYS1GREP_URL`. For per-project settings, load a file yourself: `node --env-file=.env "$(command -v sys1grep)" ...`.
249
284
  `TYPESAFE_API_KEY` is accepted too when `SYS1GREP_API_KEY` is not set.
250
285
 
286
+ The `--serve` page's **Settings** dialog edits this settings.json too (see [A search page](#a-search-page---serve)).
287
+
251
288
  ### Default options
252
289
 
253
290
  `SYS1GREP_OPTS` holds options to apply on every call, read like the settings above. It is split on spaces and put in
@@ -259,15 +296,18 @@ sys1grep -e "payment failed" app.log # strict, 8 at once, line
259
296
  sys1grep --level loose --no-n -e "payment failed" app.log
260
297
  ```
261
298
 
299
+ The same defaults can live in settings.json as `"opts": ["--level", "strict", "-j", "8", "-n"]`, one argument per
300
+ element (so a value may hold spaces). `SYS1GREP_OPTS`, when set, replaces them rather than adding to them.
301
+
262
302
  A script calling sys1grep would pick these up too (grep dropped `GREP_OPTIONS` for that reason). Call it as
263
- `SYS1GREP_OPTS= sys1grep ...` in scripts.
303
+ `SYS1GREP_OPTS= sys1grep ...` in scripts: the empty variable drops settings.json's `opts` as well.
264
304
 
265
305
  ### Other endpoints
266
306
 
267
307
  The API is configured by exactly three settings: `SYS1GREP_API_KEY` (or `TYPESAFE_API_KEY`), `SYS1GREP_URL` and `SYS1GREP_MODEL`.
268
308
  Any endpoint that speaks TypeSafe's `POST /v1/systemone` works. The key is sent to `SYS1GREP_URL` as is, so set the
269
309
  two together. On the command line, `--sys1-model=ID`, `--sys1-url=URL` and `--sys1-api-key=KEY` override
270
- the three. A key given this way shows up in `ps` and shell history, so prefer `~/.config/sys1grep/.env` for it.
310
+ the three. A key given this way shows up in `ps` and shell history, so prefer `~/.config/sys1grep/settings.json` for it.
271
311
 
272
312
  ```sh
273
313
  # OpenRouter
@@ -278,8 +318,31 @@ SYS1GREP_URL=https://ai-gateway.vercel.sh/typesafe/v1/systemone SYS1GREP_MODEL=t
278
318
  SYS1GREP_URL=http://localhost:8000/v1/systemone sys1grep -e ...
279
319
  ```
280
320
 
281
- The summary line shows the cost the endpoint reports (`usage.cost`), or for TypeSafe itself an estimate at list
282
- price marked `~`.
321
+ Every price sys1grep shows (`--dry-run`, the large-send warning, the `--max-cost` question, the summary line) is the cost
322
+ the endpoint reports (`usage.cost`), or else an estimate marked `~`. The estimate's price per million input tokens: a local
323
+ URL is free (`localhost`, `127.0.0.1` or `[::1]`, judged by the host as typed with no DNS lookup, and only for a URL you set
324
+ with `SYS1GREP_URL` or `--sys1-url`); a model in this table has its own price, `jev-latest` 0.042, `clef` 0.24 and
325
+ `clef-flash` 0.09 (USD); any other model is priced as Jev, 0.042. With another URL the estimate says whose price it is:
326
+ `at TypeSafe's list price`, `at clef's list price` or `(local URL, free)`. `--max-cost` uses the same price, so it asks
327
+ about nothing on a local URL.
328
+
329
+ **Local decision models.** A model that serves the same `POST /v1/systemone` can be used instead of Jev, with no key and no
330
+ network, but both tried so far are less accurate than Jev: raise `-t`, tune with `npm run judge`, and expect more false
331
+ positives. Both also run one request at a time, so a local URL (`localhost`, `127.0.0.1`, `[::1]`) defaults to `-j 1`;
332
+ a `-j` you give (on the command line, in `SYS1GREP_OPTS` or settings.json) still wins.
333
+
334
+ - [Jeff](https://github.com/firelex/jeff) (`jeff-serve`, 0.8B, base v1.2, zero-shot, MLX): works with
335
+ `--sys1-url=http://127.0.0.1:8765/v1/systemone --sys1-model=jeff-latest`. It answers a second request at once with
336
+ `529 "The model is busy"`, and `-j 8` runs out of retries. On `npm run judge` (51 lines, 10 cases, `-j 1`)
337
+ precision 0.68, recall 0.61, F1 0.64 at the default thresholds and F1 0.77 at `-t 0.75 -T 0.85`, against F1 0.96 for Jev
338
+ (a run of another day): not a drop-in. v1.2 is a community preview; adapters do not carry across versions.
339
+ - [strands-decider](https://github.com/strands-labs/strands-decider) (`strands-decider serve`, 2B, an M4 with `mps`):
340
+ `SYS1GREP_URL=http://127.0.0.1:8000/v1/systemone` with no key works as is (30 lines in 2.7 s). Two requests at once crash
341
+ its server (exit 134, a Metal assertion). On the same `npm run judge` set: precision 0.64, recall 0.94, F1 0.76 at the
342
+ default thresholds and F1 0.81 at `-t 0.6 -T 0.45`.
343
+ - Not tried with sys1grep yet: Cloudflare's Clef.
344
+
345
+ A request carries at most 64 questions for every backend.
283
346
 
284
347
  From source: `git clone https://github.com/uehaj/sys1grep.git && cd sys1grep && npm install -g .`,
285
348
  or run it in place with `node sys1grep.mjs ...`.
@@ -292,10 +355,12 @@ otherwise show up in the output below, this section instead uses [`tests/corpus.
292
355
  the same 51 lines with the Japanese ones translated to English — the cross-language behaviour itself is
293
356
  shown once, in [Search across languages](#search-across-languages) above.
294
357
 
358
+ For examples organised by job (on-call, triage, release notes, ...) and run on real data, see [docs/use-cases.md](docs/use-cases.md).
359
+
295
360
  ### Find lines by a concept, in any language
296
361
 
297
362
  ```sh
298
- $ ./sys1grep -n -e "customer is angry or frustrated" tests/corpus.en.txt
363
+ $ sys1grep -n -e "customer is angry or frustrated" tests/corpus.en.txt
299
364
  14:Inquiry from user Yamada: I want a refund, the item was broken
300
365
  16:Inquiry from user Sato: I'm being charged for an order I never placed, it looks like fraud, please check urgently
301
366
  18:I want my money back. The item arrived broken and customer service ignored me.
@@ -312,8 +377,8 @@ None of these lines contain the words "angry" or "frustrated".
312
377
  so you can write the question as you would ask it:
313
378
 
314
379
  ```sh
315
- $ echo "The job failed." | ./sys1grep -e "Did the job succeed?"
316
- $ echo "The job failed." | ./sys1grep -Q "Did the job succeed?"
380
+ $ echo "The job failed." | sys1grep -e "Did the job succeed?"
381
+ $ echo "The job failed." | sys1grep -Q "Did the job succeed?"
317
382
  The job failed.
318
383
  ```
319
384
 
@@ -322,7 +387,7 @@ the line answers it: no, it failed (0.83). A line that *asks* the question is cl
322
387
  answers nothing, and for a yes/no question a line that *denies* it still answers it:
323
388
 
324
389
  ```sh
325
- $ ./sys1grep -n -Q "whether the server is down" tests/intent.txt
390
+ $ sys1grep -n -Q "whether the server is down" tests/intent.txt
326
391
  5:The server is down.
327
392
  6:The server is healthy and responding normally.
328
393
  2 of 17 lines matched; 17 sent to Jev in 1 request, 1087 input tokens, ~$0.000046
@@ -336,7 +401,7 @@ would match it instead. `-Q X` is shorthand for `-e "the line answers: X"`, so i
336
401
  ### OR: two meanings, and see the probabilities with `-p`
337
402
 
338
403
  ```sh
339
- $ ./sys1grep -n -p -e "customer is asking for a refund" -e "delivery address change request" tests/corpus.en.txt
404
+ $ sys1grep -n -p -e "customer is asking for a refund" -e "delivery address change request" tests/corpus.en.txt
340
405
  14:Inquiry from user Yamada: I want a refund, the item was broken [0.99 0.01]
341
406
  17:Inquiry from user Takahashi: I'd like to change the delivery address [0.01 0.99]
342
407
  18:I want my money back. The item arrived broken and customer service ignored me. [0.96 0.01]
@@ -354,7 +419,7 @@ green at or above `-t`, red below `-T`, yellow in between. Line numbers and file
354
419
  ### AND NOT: network errors, excluding retries
355
420
 
356
421
  ```sh
357
- $ ./sys1grep -n -e "network or remote connection failure" -v "a retry is happening or was attempted" tests/corpus.txt
422
+ $ sys1grep -n -e "network or remote connection failure" -v "a retry is happening or was attempted" tests/corpus.txt
358
423
  4:2026-09-19 08:02:30 ERROR connection reset by peer while calling payment-gateway
359
424
  6:2026-09-19 08:02:35 ERROR timeout after 5000ms waiting for payment-gateway
360
425
  9:2026-09-19 08:10:44 ERROR DNS lookup failed for api.example.com
@@ -362,7 +427,7 @@ $ ./sys1grep -n -e "network or remote connection failure" -v "a retry is happeni
362
427
  13:network unreachable: no route to host 10.0.0.5
363
428
  30:except ConnectionError as e:
364
429
  31: logger.error("upstream unreachable: %s", e)
365
- 7 of 51 lines matched; 51 sent to Jev in 2 requests, 5112 input tokens
430
+ 7 of 51 lines matched; 51 sent to Jev in 2 requests, 5112 input tokens, ~$0.000215
366
431
  ```
367
432
 
368
433
  Line 5, `retrying payment-gateway request (attempt 2/3)`, is a network failure but is dropped by `-v`.
@@ -370,7 +435,7 @@ Line 5, `retrying payment-gateway request (attempt 2/3)`, is a network failure b
370
435
  ### Mixed: (finance AND negative) OR weather
371
436
 
372
437
  ```sh
373
- $ ./sys1grep -n -e "about economy, finance or markets" -a "the news is negative or a decline" -e "about weather" tests/corpus.en.txt
438
+ $ sys1grep -n -e "about economy, finance or markets" -a "the news is negative or a decline" -e "about weather" tests/corpus.en.txt
374
439
  36:Today's weather is sunny, high of 28 degrees
375
440
  43:Stock prices fell 3% after the earnings report missed expectations.
376
441
  48:Tomorrow's forecast is rain so I'll bring an umbrella
@@ -382,11 +447,11 @@ $ ./sys1grep -n -e "about economy, finance or markets" -a "the news is negative
382
447
  ### Strictness presets
383
448
 
384
449
  ```sh
385
- $ ./sys1grep --level strict -n -e "a security risk or dangerous destructive operation" tests/corpus.en.txt
450
+ $ sys1grep --level strict -n -e "a security risk or dangerous destructive operation" tests/corpus.en.txt
386
451
  33:DROP TABLE sessions;
387
452
  49:API keys must never be committed to the repository.
388
453
 
389
- $ ./sys1grep --level loose -n -e "a security risk or dangerous destructive operation" tests/corpus.en.txt
454
+ $ sys1grep --level loose -n -e "a security risk or dangerous destructive operation" tests/corpus.en.txt
390
455
  11:2026-09-19 09:00:00 ERROR SSL handshake failed: certificate expired
391
456
  16:Inquiry from user Sato: I'm being charged for an order I never placed, it looks like fraud, please check urgently
392
457
  33:DROP TABLE sessions;
@@ -398,11 +463,11 @@ $ ./sys1grep --level loose -n -e "a security risk or dangerous destructive opera
398
463
  ### Recursive search and file names only
399
464
 
400
465
  ```sh
401
- $ ./sys1grep -r -n -e "customer is asking for a refund" tests/tickets/
466
+ $ sys1grep -r -n -e "customer is asking for a refund" tests/tickets/
402
467
  tests/tickets/a.txt:7:Ticket #16: I want a refund, the item was broken.
403
468
  tests/tickets/sub/b.txt:1:The customer wants a refund for the broken lamp.
404
469
 
405
- $ ./sys1grep -rl -e "customer is asking for a refund" tests/tickets/
470
+ $ sys1grep -rl -e "customer is asking for a refund" tests/tickets/
406
471
  tests/tickets/a.txt
407
472
  tests/tickets/sub/b.txt
408
473
  ```
@@ -412,8 +477,8 @@ tests/tickets/sub/b.txt
412
477
  that usually hold secrets (`.env*`, `.netrc`, `.npmrc`, `.pypirc`, `.pgpass`, `.git-credentials`, `*.pem`,
413
478
  `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `id_rsa*` and friends; names compared without case) and
414
479
  generated files (`*.map`, `*.min.js`, `*.min.css`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`,
415
- `Cargo.lock`, `poetry.lock`, `composer.lock`, `Gemfile.lock`, `go.sum`). **Every line that is searched is sent
416
- to the TypeSafe API**, so point `-r` at a directory
480
+ `Cargo.lock`, `poetry.lock`, `composer.lock`, `Gemfile.lock`, `go.sum`), symlinks and `~/.config/sys1grep/settings.json` itself. **Every line that is searched is sent
481
+ to the TypeSafe API** (or wherever `SYS1GREP_URL` points), so point `-r` at a directory
417
482
  you mean to scan. Inside a git repository, `-r` also skips what git ignores (`.gitignore`, `.git/info/exclude`, the
418
483
  global excludes file), so build output and local files stay home; a tracked file is searched even if it matches.
419
484
  A file named `*.gz` (a rotated log: `app.log.1.gz`) is read decompressed, as `zgrep` does, and printed by its
@@ -437,7 +502,7 @@ $ sys1grep -r -e 'Python でリトライ処理を書いている箇所' .
437
502
  sys1grep: scope: *.py *.pyi *.pyw (from "Python files: 0.94")
438
503
  sys1grep: scope: 12 of 340 files
439
504
  $ sys1grep -r -e '昨日変えた箇所で認証を扱っている' src/
440
- sys1grep: scope: modified since 2026-09-25 00:00 (from "what was changed yesterday: 0.91")
505
+ sys1grep: scope: changed since 2026-09-25 00:00 (git commits; the mtime for files git does not have committed) (from "what was changed yesterday: 0.91")
441
506
  sys1grep: scope: 3 of 120 files
442
507
  ```
443
508
 
@@ -506,12 +571,25 @@ tree, not `git ls-tree`'s own literal/directory-prefix match); `--include` / `--
506
571
  **An old `<tree>` can hold a secret an ordinary file once carried and was later removed from**: the skip list
507
572
  drops files by name only, not by what changed since.
508
573
 
574
+ ### Commits instead of files (`-g`)
575
+
576
+ `-g` (`--gitlog`) searches the commits of `git log` instead of files, one record each (hash, date, subject, body):
577
+
578
+ ```sh
579
+ $ sys1grep -g -e 'a performance fix to the .mjs files today'
580
+ ```
581
+
582
+ With one term, auto-scope becomes `git log`'s own arguments, printed on stderr: a time becomes `--since-as-filter`
583
+ (`--since` on git before 2.37, and `--until` when the span has an end: yesterday, last week, last month), a language
584
+ a pathspec, an author or "mine" `--author`, "this branch" or "not pushed" a range. FILE arguments are pathspecs.
585
+ Each commit is a record, as with `-z`, so `-M` defaults to 8000. Not with `-r` or `git sys1grep`.
586
+
509
587
  ### Everything that is *not* something
510
588
 
511
589
  ```sh
512
- ./sys1grep -v "a timestamped server log line" mixed.txt # like grep -v
513
- ./sys1grep -e "source code or SQL" -v "SQL" src.txt # code, but not SQL
514
- cat app.log | ./sys1grep -e "the deploy failed or was rolled back"
590
+ sys1grep -v "a timestamped server log line" mixed.txt # like grep -v
591
+ sys1grep -e "source code or SQL" -v "SQL" src.txt # code, but not SQL
592
+ cat app.log | sys1grep -e "the deploy failed or was rolled back"
515
593
  ```
516
594
 
517
595
  ### Records that span several lines (`-z`)
@@ -524,9 +602,9 @@ A proposition like "this commit changes user-visible behaviour" is true of a who
524
602
  line in it:
525
603
 
526
604
  ```sh
527
- $ git log -z --format='%h %s %b' | ./sys1grep -z -n -e "the change alters user-visible behaviour" -v "documentation only"
528
- 7:21120e9 Revert "feat: ship the /sys1grep Claude Code skill" ...
529
- 8:51ae333 feat: ship the /sys1grep Claude Code skill
605
+ $ git log -z --format='%h %s %b' | sys1grep -z -n -e "the change alters user-visible behaviour" -v "documentation only"
606
+ 7:21120e9 Revert "feat: ship the /semgrep Claude Code skill" ...
607
+ 8:51ae333 feat: ship the /semgrep Claude Code skill
530
608
  ```
531
609
 
532
610
  Matching records are printed NUL-terminated too, so pipe them through `tr '\0' '\n'` to read them.
@@ -541,7 +619,7 @@ Wrapped lines are joined before splitting, so a sentence that runs over several
541
619
  [`tests/prose.txt`](tests/prose.txt) wraps an English paragraph and a Japanese one:
542
620
 
543
621
  ```sh
544
- $ ./sys1grep -n --unit=sentence-by-jev -e "the author admits they made a mistake" tests/prose.txt
622
+ $ sys1grep -n --unit=sentence-by-jev -e "the author admits they made a mistake" tests/prose.txt
545
623
  1:I should have checked the input
546
624
  2:before shipping, and that was my
547
625
  3:mistake. Next time I will add a test
@@ -560,7 +638,7 @@ inside a line: lines 3 and 9 are colored only up to the end of the matching sent
560
638
  Thai, Lao, Khmer, Myanmar and Tibetan, which do not put spaces between words:
561
639
 
562
640
  ```sh
563
- $ ./sys1grep -n -o --unit=sentence-by-jev -e "the author admits they made a mistake" -e "customer is asking for a refund" tests/prose.txt
641
+ $ sys1grep -n -o --unit=sentence-by-jev -e "the author admits they made a mistake" -e "customer is asking for a refund" tests/prose.txt
564
642
  1:I should have checked the input before shipping, and that was my mistake.
565
643
  8:先週買った掃除機が初日から動かないので返金してほしいです。
566
644
  ```
@@ -597,7 +675,7 @@ In code the question is usually about a function ("retries on network failure"),
597
675
  says so. `--unit=function` judges each function and prints its lines, with `-n` giving file line numbers:
598
676
 
599
677
  ```sh
600
- $ ./sys1grep -n --unit=function -e "retries on network failure" src/net.js
678
+ $ sys1grep -H -n --unit=function -e "retries on network failure" src/net.js
601
679
  src/net.js:3:async function fetchWithRetry(url) {
602
680
  src/net.js:4: for (let i = 0; i < 5; i++) {
603
681
  src/net.js:5: try { return await fetch(url); }
@@ -643,7 +721,7 @@ start and end send nothing, which makes a free reachability search. In Python 3.
643
721
  that `load` reaches and that hold a `raise`:
644
722
 
645
723
  ```
646
- $ cd /usr/lib64/python3.6/json && sys1grep -e '/^\s*def load\b/' --step-to '/\braise\b/' *.py
724
+ $ cd /usr/lib64/python3.6/json && sys1grep -P '/^\s*def load\b/' --step-to -P '/\braise\b/' *.py
647
725
  sys1grep: walk: 1 at hop 0, 1 at hop 1, 3 at hop 2, 1 at hop 3, 1 at hop 4, 1 at hop 5; stopped: no new unit
648
726
  0 __init__.py-274-load
649
727
  1 __init__.py:302:loads
@@ -653,19 +731,22 @@ sys1grep: walk: 1 at hop 0, 1 at hop 1, 3 at hop 2, 1 at hop 3, 1 at hop 4, 1 at
653
731
  5 scanner.py:28:_scan_once
654
732
  ```
655
733
 
656
- - **The expressions.** The expression before `--step-to` is the start's, as in any search; every `-e` / `-a` /
657
- `-v` / `-Q` after it is the end's. `--step-to MEANING` and `--step-to '/RE/'` are `--step-to -e ...`; the file
658
- names follow. A step is a search, not a hop: one `--step-to` is two steps, whatever the hops between them.
734
+ - **The expressions.** The expression before `--step-to` is the start's, as in any search; every `-e` / `-P` /
735
+ `-a` / `-v` / `-Q` after it is the end's. A bare `--step-to MEANING` is `--step-to -e MEANING` and a bare
736
+ `--step-to '/RE/'` is auto-detected and becomes `--step-to -P '/RE/'`; the file names follow. Write the
737
+ explicit `-P` form for a regex end, as in the example above: it reads the same but names the option. A step
738
+ is a search, not a hop: one `--step-to` is two steps, whatever the hops between them.
659
739
  - **The edges.** Without `--edges` the unit is a function (`--unit=function`) and an edge is a call: `name(` in
660
740
  a function's body, its comments, docstrings and strings left out, links to every function of that name. A name
661
741
  comes from the funcname line (the `def` under a decorator). `--edges=FILE` walks any other relation instead,
662
742
  one edge per line, `FROM_FILE:LINE<TAB>FROM_NAME<TAB>TO_FILE:LINE<TAB>TO_NAME`; a line stands for the unit that
663
- holds it. `--reverse` walks the edges backwards, callee to caller.
743
+ holds it. `--step-reverse-to` in place of `--step-to` walks the edges backwards, callee to caller: the same
744
+ arguments, with `--hops` and `--edges`, and not together with `--step-to`.
664
745
  - **The walk.** Breadth first from every start at once. A function's hop is its shortest distance from any
665
746
  start, and each function is reached once, so a cycle ends. `--hops=N`, `--hops=M..N` and `--hops=M..` pick the
666
747
  hops an end may be at (default `0..`: a start that matches the end expression is an end at hop 0). A path that
667
748
  reaches no end is left out. stderr says how many functions each hop reached and why the walk stopped: no new
668
- function, or `--hops`.
749
+ unit, or `--hops`.
669
750
  - **The cost.** Jev judges every function against the start expression in one batch, then the functions reached
670
751
  within `--hops` against the end expression in another. `--max-cost` asks again before the second, counting the
671
752
  first. `--dry-run` answers every question 0, so it walks from every function the start expression could hold
@@ -690,11 +771,11 @@ $ sys1grep -e "a request failed" app.log
690
771
  sys1grep: 2040 units fold to at most 31 templates; --dedup=auto would save ~68 requests (~44k tokens)
691
772
  ```
692
773
 
693
- `--dedup=auto` asks and folds only when that estimate says it pays; a file that barely repeats sends every
774
+ `--dedup=auto` folds only when that estimate says it pays; a file that barely repeats sends every
694
775
  line, asking nothing extra. `--verbose` / `--dry-run` print the decision and its numbers for every value.
695
776
 
696
777
  ```sh
697
- $ ./sys1grep --dedup -n -e "a request failed" app.log
778
+ $ sys1grep --dedup -n -e "a request failed" app.log
698
779
  1:worker request 3fa9c1e27b failed: connection reset
699
780
  2:worker request 88d0e41a5c failed: connection reset
700
781
  4:worker request 0b7f2a9e13 failed: connection reset
@@ -702,9 +783,21 @@ $ ./sys1grep --dedup -n -e "a request failed" app.log
702
783
  ```
703
784
 
704
785
  Whether a value may be folded depends on the meaning: a number decides "disk usage is above 90%", a time
705
- decides "happened at night". So Jev is first asked, one small request per meaning, which kinds of value
706
- could change a match, and those kinds are kept apart (one of the two requests above). With
707
- `-e "disk usage is above 90%"` the same file keeps `95%` and `12%` apart and prints only `5:disk usage 95%`.
786
+ decides "happened at night". So `--dedup=always` (and a bare `--dedup`) first asks Jev, one small request per
787
+ meaning, which kinds of value could change a match, and keeps those kinds apart (one of the two requests above).
788
+ With `-e "disk usage is above 90%"` the same file keeps `95%` and `12%` apart and prints only `5:disk usage 95%`.
789
+
790
+ `--dedup=auto` asks no such question (#152). It folds every kind and judges a sample of each template instead:
791
+ up to 5 members whose values differ, for each varying value the member holding its lowest, its highest and its
792
+ middle value. When every sample gets the same verdict, the members not sampled share the first sample's answers,
793
+ and the samples keep their own (`-p` prints each sample's own probability). When the verdicts differ, the rest are
794
+ sent in a second wave, and `--max-cost` is checked again before it. A split costs slightly more than `never`, at
795
+ worst one extra request per wave boundary, a few percent: one template of 600 members sent 21 requests where
796
+ `never` sent 20. A value only between the samples is missed: among `usage 1` to `usage 120`, the meaning
797
+ `usage 50` matches no sample, so `auto` prints nothing where `never` prints line 50. `--verbose` prints how many
798
+ templates agreed and how many units were sent again. `--dry-run` assumes every sample agrees, so its count is the
799
+ floor: 100 templates of 6 members show 10 requests and 300 of 600 lines, and a run where every template splits
800
+ sends all 600 lines in 20 requests.
708
801
 
709
802
  Measured on real logs, with nothing kept: a 43,071-line system log folds into 550 templates (1.2% of the
710
803
  bytes), `install.log` to 21.1%, a Claude Code transcript (jsonl) only to 56.8%. It is for machine-generated
@@ -761,6 +854,16 @@ $ sys1grep -r -n --summarize --summarize-prompt="3 lines or fewer, just which fi
761
854
  It needs `--summarize`; empty TEXT is the same as leaving it out. It comes after the `--format` sentence,
762
855
  so TEXT can override the format.
763
856
 
857
+ To decide after seeing the lines, `--summarize=ask` prints them as usual and then asks on the terminal
858
+ `Summarize these matches? [y/N]`. On `y` the lines just printed go to the TOOL of a bare `--summarize`: no second
859
+ search, nothing more sent to Jev. Enter or anything else ends there.
860
+
861
+ - The question goes over `/dev/tty`, so a stdin target still works. With no terminal, or stdout not one (a pipe,
862
+ `$(...)`, a script), nothing is asked and nothing hangs.
863
+ - `-y` summarizes without asking, so `-y` in `SYS1GREP_OPTS` makes it summarize every time; `-q` and `--dry-run` never ask.
864
+ - With `--dedup`, every matching line prints; the TOOL gets each template's representative, as with `--summarize`.
865
+ - The exit status is the search's, whatever the summary does. Over 200 KB, a `y` says so instead of sending.
866
+
764
867
  Other TOOLs:
765
868
 
766
869
  ```sh
@@ -777,6 +880,72 @@ OpenAI-compatible server. All three need `SYS1GREP_SUMMARIZER_MODEL`: none has a
777
880
  `SYS1GREP_SUMMARIZER_API_KEY` goes as `Authorization: Bearer` to a URL TOOL only (never `SYS1GREP_API_KEY`,
778
881
  which is Jev's). `OLLAMA_HOST` moves ollama's host, as it does for the `ollama` CLI itself.
779
882
 
883
+ ### A search page (`--serve`)
884
+
885
+ `--rank --format=html` answers one question per run. `--serve` opens such a page (the `search` template, unless `--template` is given) with a search box, so you can rephrase
886
+ and narrow without rerunning the command:
887
+
888
+ ```
889
+ $ sys1grep --serve -r src/
890
+ http://127.0.0.1:51234/?k=3f9e...
891
+ $ sys1grep --serve=8080 -y --level=strict -r src/ # a fixed port; the options given here are the page's first values
892
+ ```
893
+
894
+ From a clone, `npm run serve -- -r src/` is the same (`--` keeps npm from reading the options; `npm run serve -- --serve=8080 -r src/` fixes the port). With no target, `--serve` searches its own bundled sample corpus (`examples/`, #237) instead of reading an empty stdin, so every example button has something to find; name a target to search that instead. A folder named without `-r` is refused at launch, with the command that adds it (`--serve: src is a directory: add -r to search it (sys1grep --serve -r src)`, #222).
895
+
896
+ The printed URL carries a token made fresh at launch (`?k=...`). Every route needs it, the page itself included: without
897
+ it (in the query, or in a header for the page's own requests) the server answers 404 to everything, so another local
898
+ account or process cannot read the page, its token, or a search's results merely by knowing the port.
899
+
900
+ `EN` / `日本語` in the title bar switches the whole page's language (headings, labels, buttons, hints, the settings
901
+ dialog, the page's own messages), remembered in the browser; its default is the saved choice, else `--lang` /
902
+ `SYS1GREP_LANG` / settings.json's `lang` (below), else the browser's own language, else English. What sys1grep itself
903
+ prints (the results, the `--dry-run` estimate, an error), the command line and option names are never translated.
904
+
905
+ The page is a set of cards under a blue header band: Target and Search conditions in one card with a large Search
906
+ button (Estimate cost and Stop under it) at its right, the examples and Options side by side below it (one column on
907
+ a phone), then the command line and Details. **Target** shows what every search reads: the files and folders given at launch
908
+ (`npm run serve -- src docs/a.md`), or with none what sys1grep falls back to: `.` with `-r`, every file git tracks
909
+ with `git sys1grep`, or, with neither, the bundled sample corpus (`demo corpus (bundled): examples/`, #237); while
910
+ `-g` (in Details) is on, it says git log's commits instead. It is shown only: the page cannot
911
+ change it, and a target in a request is ignored. **Search conditions**
912
+ holds the meaning fields and multi-step, **Options** `rank` and `summarize`,
913
+ and **Details** the other options; the results come below them. What they hold: the meaning fields (`+` adds one; each takes `-e`, `-P`, `-a`, `-v` or `-Q`), `rank`, `summarize`, a **multi-step**
914
+ switch (on, it shows the end fields, `--step-to` and one or more meanings, and the edges, `--hops` and a reverse switch that makes it `--step-reverse-to`; off, they
915
+ leave the command and the search and keep their values), example buttons (searches from this README and
916
+ [docs/use-cases.md](docs/use-cases.md), then multi-step walks; one fills the fields and the controls it needs, puts the rest back to their launch values, and waits for Search), a
917
+ **Details** fold with the other options (`--level`, `-t`/`-T`, `-C`, `-A`/`-B`, `--auto-scope`, `-n`, `-p`, `--dedup`,
918
+ `--unit`, `-o` (shown only with a sentence unit), `-j`, `--max-cost`, `--include`, `--exclude`, `--changed-within`,
919
+ `-g`), and the
920
+ **command line** for the controls as they are now, with a Copy button; pasted in a terminal it runs the
921
+ same search. An option at its launch value is left out of what the controls add. `rank` off shows the matches in file order
922
+ as text, on shows the ranked cards; `summarize` adds a right column: Search runs once, and once its results are on
923
+ screen the page summarizes those results (POSTs the text shown, rank's cards included, to the summarizer; no second
924
+ search, nothing extra sent to Jev, up to 200 KB as `--summarize` takes). Checking `summarize` while results are
925
+ already on screen asks, in the page, whether to summarize those results as they are (#227): OK sends their text back
926
+ the same way; Not now leaves the box checked for the next Search. If a field or option changed since
927
+ those results came, the question says so. No results on screen, or multi-step on: no question. Estimate cost is `--dry-run`. The first field is a question (`-Q`) by default. `--suggest=Q:TEXT` / `--suggest=e:TEXT` (or `--suggest TEXT`, repeatable; a bare TEXT is a question) puts suggested-question buttons first in the examples row; pressing one fills the page, then Search runs it. The options in Details each have a "?" hint saying what they do. While a search runs, Search and Estimate are disabled and **Stop** ends it (the child process is killed). The matches are colored as `--color=always` colors them (file names, line numbers, regex matches), the cards mark them with `<mark>`.
928
+
929
+ Each search runs sys1grep itself (the options given at launch, then the controls'), so everything the command line does
930
+ the page does, and only that. The server listens on `127.0.0.1` only, answers only to that host name, and the page never
931
+ sees the API key. The targets, `--chunk`, `-M`, `--max-filesize`, `-y`, `--edges`, `--template`, the
932
+ API settings and `--summarize`'s TOOL are fixed at launch: the page can narrow what was given but not widen it (`--chunk`,
933
+ `-M` and `--max-filesize` still reach the Settings dialog's default options). A search over `--max-cost` is refused at
934
+ once (`--serve` has no `/dev/tty` to ask `-y`'s question on, or launch with `-y` to let it through unasked); the error
935
+ carries the same estimate `--dry-run` would show, and a button next to it reruns that one search, once, with
936
+ `--max-cost` just above the estimate (not a change to the Details field).
937
+ The meanings come from the page, so `-e -P -a -v -Q --step-to --step-reverse-to` and `-l -c -q -o -z --unit=zero -H -i --format --color --dry-run --verbose` are refused at launch.
938
+
939
+ **Settings** edits `~/.config/sys1grep/settings.json` (see [Install](#install)): `url`, `model`, the key, `opts` (one argument
940
+ per line), the summarizer's three and `lang` (the page's own default language). Save writes the file, and the next search uses it, as does every later `sys1grep`
941
+ run. Next to each field the page says where the value a search uses comes from, and says so when an environment variable or a
942
+ launch option wins over the file, since then saving does not change the search. A key comes back only as saved / not
943
+ saved; type a new one to replace it, or tick remove. A `--sys1-api-key` inside `opts`, and a `url`'s userinfo, come back
944
+ masked as `***`, the same way `--verbose` masks them; `opts` refuses a target, a flag the page itself refuses at launch,
945
+ or `--sys1-url` / `--sys1-api-key` (their own fields), and `url` and a URL summarizer must be `http(s)`. The save is a
946
+ `POST` with the page's token and this request's own Host as its required `Origin`, and the file is written whole (0600,
947
+ its directory 0700 when created) and renamed into place, so a reader never sees half of it.
948
+
780
949
  ### Best first (`--rank`)
781
950
 
782
951
  Matches print in file order, as grep prints them. With many, `--rank` prints the results best first, each under a
@@ -796,18 +965,77 @@ tickets/a.txt-13-We will check it.
796
965
  ```
797
966
 
798
967
  - `--rank` is `--rank=jev`: after the search, Jev is asked of each result, lines and context together, whether it
799
- is relevant to the meanings that are not negated. One more question per result, `--chunk` results to a request;
968
+ is relevant to the meanings that are not negated. One more question per result, `--chunk` results (at most 64) to a request;
800
969
  `--dry-run` / `-i` show an upper bound, since which results there are is known only after the search.
801
970
  - `--rank=match` sorts by each result's highest match probability, with no request. It is the answer to a yes/no
802
971
  question on one line, so clear matches sit close together, and the context is not read.
803
972
  - `-p` puts the score on the header (`1. [0.96] tickets/b.txt`). `-l` lists the files by their best result.
804
973
  - `--format=markdown` writes a `## 1. tickets/b.txt` heading and a fenced block per result; `--format=html` one
805
974
  self-contained HTML document from a template (below), light and dark, with a relevance bar per result. The lines
806
- are as they print, escaped and uncolored.
975
+ are as they print, escaped; what matched is in `<mark>` (the regex matches, else the matching sentences, else the whole
976
+ matching line; context lines are not marked; `--color=never` turns it off).
807
977
  - `--summarize` gets the results in ranked order; with `--dedup` a representative is one result.
808
978
  - It needs a meaning (a regex, `!` or `-v` alone ranks nothing), and cannot be combined with `-c`, `-o` or `-q`.
809
979
  `--no-rank` turns off an earlier one, from `SYS1GREP_OPTS` say.
810
980
 
981
+ ### Finding the cause of a failed command
982
+
983
+ A failed job's last error usually names a symptom (`214 accounts out of balance ... day-close aborted`), and the
984
+ other `ERROR` / `WARN` lines are retries that recovered. The cause is often one plain `INFO` or `DEBUG` line far
985
+ above. Asking for the symptom, or `-Q "why did the job fail"`, finds that noise: on the 30,000-line log of #156 it
986
+ returned only recovered `ERROR` lines. Ask by kind of cause instead, and rank the results:
987
+
988
+ ```sh
989
+ $ sys1grep -n --dedup --level strict --rank \
990
+ -e "a component, writer, job or feature was paused, disabled, skipped or put on hold" \
991
+ -e "a configuration, endpoint, region, credential or data source was changed or switched" \
992
+ -e "an input, file or table was empty, missing, or had fewer records or columns than expected" \
993
+ -e "a fallback or default value was used instead of the real one" \
994
+ job.log
995
+ ```
996
+
997
+ Measured on 24 generated logs of failed jobs, six per kind of cause, 10,008 lines each, with these four meanings
998
+ (`npm run cause-eval`, rows in `tests/cause-results.tsv`; an earlier version of the script wrote its 24 rows, and
999
+ the `top 2 jev` column is filled only in the two rerun rows). Each search returned 366 to 464 results. The
1000
+ `--rank=match` column is not a separate run: it is recomputed from the `-p` columns of the `--rank` run, since
1001
+ `--rank=match` scores a result by the highest of them. That recomputation was checked against a real `--rank=match`
1002
+ run only on config-5, where the cause was not a result. A cause "not matched" may have been judged below `strict` or
1003
+ folded by `--dedup` into a like line; the rows do not tell the two apart.
1004
+
1005
+ | cause | cause is result 1, `--rank` | cause is result 1, `--rank=match` | otherwise |
1006
+ |---|---|---|---|
1007
+ | a configuration, endpoint or credential switched | 5 of 6 | 5 of 6 | 1 not matched |
1008
+ | an input empty or short | 2 of 6 | 2 of 6 | 2 at results 62 (`match`: 343-371) and 81 (`match`: 191-219); 2 not matched |
1009
+ | a default used instead of the real value | 6 of 6 | 5 of 6 | `match`: 2-9 (one result above it, seven tied with it at 0.90) |
1010
+ | a line that never came (a start with no finish) | 0 of 6 | 0 of 6 | the start line never a result |
1011
+
1012
+ A log cost about $0.088 (2.1M input tokens, 643 to 646 requests) and 16 to 17 s. `--rank` (`jev`) adds one question
1013
+ per result, about 3% more input tokens than `--rank=match` (one log run both ways; not recorded in the TSV). It put
1014
+ the cause higher than `--rank=match` on three logs and lower on none.
1015
+
1016
+ When the top results look unrelated (rotations, refreshes, the final error itself), the cause is likely one of these:
1017
+
1018
+ - **A line that states only a value**: `stock snapshot downloaded: 1204 bytes`, `input directory ... holds 2 files`,
1019
+ `resolved db-primary.internal to 10.0.4.22 (replica-2)`. Nothing in the line says it is wrong, so no hypothesis
1020
+ holds, nor did a meaning written from the final error
1021
+ (`-e "the job connected to a read-only replica instead of the primary database"`). Search for the thing the error
1022
+ names instead (`-P '/snapshot|replica/'`) and read the numbers.
1023
+ - **A line that never came**: a worker that claimed a shard and never finished it, a step or a migration that started
1024
+ and never ended. On all six such logs the start line was not among the results. The top result was a routine line,
1025
+ the final error, or once the previous run's `alias products switched to products-20260927`, whose 09-28
1026
+ counterpart never came. A meaning for it
1027
+ (`-e "a worker, step or upload started but never reported that it finished"`) put the final error first, not the
1028
+ start, and `--dedup` can fold one start into a like one, since only the missing finish tells them apart. Compare
1029
+ starts with finishes by eye or with grep.
1030
+ - **The score will not tell you**: where the cause was result 1 it scored 0.93 to 0.97 under `--rank`; on two logs
1031
+ where it never matched, the top result scored 0.88 and 0.91.
1032
+
1033
+ The follow-ups behind the first two points (key lines judged alone in a 31-line window, the meanings written from
1034
+ the error or for a missing finish) are in the description of PR #192; `tests` holds no way to rerun them.
1035
+
1036
+ Every searched line goes to Jev (TypeSafe, or whatever `SYS1GREP_URL` names), and with `--summarize` the matching
1037
+ lines go to its TOOL's provider as well. Build logs and job logs often hold tokens, passwords and customer data.
1038
+
811
1039
  ### HTML templates (`--template`)
812
1040
 
813
1041
  `--rank --format=html` and `--summarize --format=html` fill in a template. Four come with sys1grep: `default`, `print` (black on white, serif,
@@ -875,8 +1103,8 @@ claude plugin marketplace add uehaj/uehaj-marketplace
875
1103
  claude plugin install uehaj@uehaj-marketplace
876
1104
  ```
877
1105
 
878
- No separate install of the command-line tool is needed: the skill uses `sys1grep` from your PATH if present,
879
- otherwise `npx @uehaj/sys1grep`. Only the API key has to be set (see [Install](#install)).
1106
+ No separate install of the command-line tool is needed: the skill runs a version it pins,
1107
+ through `npx -y @uehaj/sys1grep@VERSION`, not the `sys1grep` on your PATH. Only the API key has to be set (see [Install](#install)).
880
1108
 
881
1109
  Then, inside Claude Code:
882
1110
 
@@ -892,22 +1120,27 @@ Claude Code installs plugins, not single skills. If you want just this one skill
892
1120
  npx skills add uehaj/uehaj-marketplace --skill sys1grep -a claude-code -g
893
1121
  ```
894
1122
 
895
- The skill writes the meaning in English, picks `-e` / `-a` / `-v` for AND / OR / NOT, adds `-n`, narrows large
1123
+ The skill writes the meaning in English, picks `-e` / `-a` / `-v` for OR / AND / NOT, adds `-n`, narrows large
896
1124
  directories to files worth paying for, and re-runs with `--level loose` or `strict` when the first result looks off.
897
- The API key and endpoint are read the same way as on the command line (`SYS1GREP_API_KEY`, `~/.config/sys1grep/.env`).
1125
+ The API key and endpoint are read the same way as on the command line (`SYS1GREP_API_KEY`, `~/.config/sys1grep/settings.json`, `~/.config/sys1grep/.env`).
898
1126
 
899
1127
  ## Usage
900
1128
 
901
1129
  ```
902
- usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]... [FILE...]
903
-
904
- -e MEANING lines matching this meaning (several -e are OR'd)
1130
+ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION|-P /RE/FLAGS [-a MEANING] [-v MEANING]... [FILE...]
1131
+ sys1grep [OPTION]... -e START1 [-e START2]... --step-to END1 [-e END2]... [FILE...]
1132
+
1133
+ -e MEANING lines matching this meaning (several -e are OR'd); MEANING is always sent to Jev, never
1134
+ read as a regex — a /RE/flags-shaped value is refused, with a hint naming -P. A meaning
1135
+ that really starts and ends with / gets through with a leading space (-e ' /cat/')
1136
+ -P '/RE/FLAGS' a regex term, OR'd like -e; matched locally, no request; -a/-v after it AND/AND NOT
1137
+ onto it, same as after -e; -P '!/RE/' is not allowed, negate with -v '/RE/'
905
1138
  -Q, --question QUESTION lines that answer QUESTION, not lines asking it; the same as
906
1139
  -e "the line answers: QUESTION" (see "Lines that answer a question" above)
907
- -a MEANING AND onto the preceding -e/-Q term. -e A -a B -e C = (A and B) or C
908
- -v MEANING AND NOT onto the preceding -e/-Q term. -e A -v B = A and not B
1140
+ -a MEANING AND onto the preceding -e/-Q/-P term. -e A -a B -e C = (A and B) or C
1141
+ -v MEANING AND NOT onto the preceding -e/-Q/-P term. -e A -v B = A and not B
909
1142
  At the front it is a bare negation. -v B = not B (like grep -v)
910
- !MEANING a leading ! negates just that meaning, in -e / -Q / -a / -v alike
1143
+ !MEANING a leading ! negates just that meaning, in -e / -Q / -a / -v alike (not -P)
911
1144
  -e A -e '!B' = A or not B. -a '!C' is the same as -v C
912
1145
  --level=LEVEL strictness preset, sets both thresholds (default normal)
913
1146
  loose : -t 0.3 -T 0.7 catch more, accept some noise
@@ -928,6 +1161,8 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
928
1161
  or date-time, today, this-week or this-month
929
1162
  --no-auto-scope do not narrow those files by what a meaning says about them (see Scope from the meaning);
930
1163
  --auto-scope turns it back on
1164
+ -g, --gitlog search git log's commits, one record each, instead of files; FILE arguments are pathspecs
1165
+ (see "Scope from the meaning"). Not with -r or git sys1grep
931
1166
  -l print only the names of files with a match, not the lines
932
1167
  -H, --with-filename prefix file names even for a single file; --no-filename never prefixes them
933
1168
  -A NUM print NUM lines of trailing context after each match (context lines use - as separator)
@@ -935,10 +1170,11 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
935
1170
  -C NUM print NUM lines of context before and after (-A NUM -B NUM)
936
1171
  -c print only a count of matching lines per file (like grep -c)
937
1172
  -q, --quiet print nothing, stop at the first match; exit 0 on a match, even after an error (like grep -q)
938
- --chunk=LINES lines per request (default 30)
1173
+ --chunk=LINES lines per request (default 30; a request also carries at most 64 questions, so fewer lines
1174
+ with 3 or more meanings)
939
1175
  Lines in one request are each other's context, so a small chunk changes verdicts
940
1176
  on ambiguous lines, not just speed
941
- -j N concurrent requests (default 8)
1177
+ -j N concurrent requests (default 8; 1 for a local URL: localhost, 127.0.0.1, [::1])
942
1178
  -n print line numbers
943
1179
  --unit=UNIT the unit of judgement: line (default), zero, sentence-by-jev, sentence-by-rule or function
944
1180
  -z, --null-data --unit=zero: judge NUL-terminated records instead of lines, and print them NUL-terminated (see "Records that span several lines" above);
@@ -947,16 +1183,18 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
947
1183
  --unit=function judge each function and print its lines (see "One function at a time" above)
948
1184
  --step-to EXPRESSION print the paths from the functions the expression before it matches, along the
949
1185
  calls, to the functions the expression after it matches (see "From one function to another" above)
950
- --edges=FILE walk the edges in FILE instead of the calls; --reverse walks them backwards
1186
+ --step-reverse-to EXPRESSION --step-to walking the edges backwards, callee to caller
1187
+ --edges=FILE walk the edges in FILE instead of the calls
951
1188
  --hops=N|M..N|M.. the hops an end may be at (default 0..)
952
- -o with --unit=sentence-by-*, print only the matching sentences
1189
+ -o print only each regex match, one per line, as grep -o (a line only meanings matched prints
1190
+ whole); with --unit=sentence-by-*, only the matching sentences
953
1191
  -p print each meaning's probability at the end of the line
954
1192
  --dry-run send nothing; print the settings the search would run with (and their source, when not the
955
1193
  command line), each file searched and each request with its questions
956
1194
  --verbose print the same to stderr while searching
957
1195
  -i, --interactive show what --dry-run would send, and search only after y on the terminal
958
1196
  -M NUM, --max-columns=NUM send at most the first NUM characters of a line (or record, with -z); still
959
- searched and judged, just not past that cutoff (default 2000, 8000 with -z)
1197
+ searched and judged, just not past that cutoff (default 2000, 8000 with -z or --unit=function)
960
1198
  --max-filesize=SIZE size every target first (K/M/G, default 10M); over it, skip it outright, like rg's own
961
1199
  --max-filesize (-y does not affect it). stdin is sized once read, and skipped the same way if
962
1200
  it is over; a --cached / <tree>: target is a blob, sized from its content; -g's commits stay
@@ -964,12 +1202,24 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
964
1202
  --max-cost=USD price the input about to be sent; over it (default 1), ask to continue on the terminal; -y
965
1203
  answers yes without asking; no terminal and the limit exceeded is exit 2, unchanged by -q
966
1204
  (scripts pass -y); -i already asks unconditionally and earlier, so this does not ask again
1205
+ -y, --yes answer that question yes without asking
967
1206
  --dedup[=auto|always|never] judge one line per template and reuse its answer for the rest (default never,
968
1207
  for now; see "One line per template" above)
1208
+ --cache[=on|off|only] answer a request sent before from ~/.config/sys1grep/cache, free; only: never send,
1209
+ a request not cached is exit 2 unless -q already found a match (default off,
1210
+ SYS1GREP_CACHE; see "Sending less");
1211
+ --cache=clear (alone) empties it
969
1212
  --rank[=jev|match] print the results (a match with its context) best first, under numbered headers; jev
970
1213
  (bare --rank) asks Jev of each result, match sorts by its best match probability (see "Best first"); --no-rank: file order
1214
+ --summarize[=TOOL] pipe what would print to TOOL (claude by default, or SYS1GREP_SUMMARIZER) and print its
1215
+ summary instead; --summarize=ask prints the lines, then asks on the terminal; --no-summarize
1216
+ turns it off (see "A summary instead of the lines")
1217
+ --summarize-prompt=TEXT your own instruction, added after the fixed one; needs --summarize
1218
+ --format=FORMAT plain (default) / markdown / html: --rank's output, or asked of --summarize's TOOL
1219
+ --serve[=PORT] serve a search page on 127.0.0.1 instead of searching; --suggest=Q:TEXT / e:TEXT adds
1220
+ suggested-question buttons (see "A search page (`--serve`)")
971
1221
  --template=NAME the document --rank or --summarize --format=html writes: ~/.config/sys1grep/templates/NAME.html, else the
972
- bundled one (default, print, terminal), or a file; default SYS1GREP_TEMPLATE, else default.
1222
+ bundled one (default, print, search, terminal), or a file; default SYS1GREP_TEMPLATE, else default.
973
1223
  --template=list prints the names (see "HTML templates")
974
1224
  --install-templates copy the bundled templates to ~/.config/sys1grep/templates, keeping existing files
975
1225
  --color[=WHEN] auto (default: color when stdout is a terminal) / always / never; bare --color means auto
@@ -978,17 +1228,25 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
978
1228
  yellow in between. NO_COLOR is honored
979
1229
  --sys1-model=ID, --sys1-url=URL, --sys1-api-key=KEY
980
1230
  the API settings, overriding SYS1GREP_MODEL, SYS1GREP_URL, SYS1GREP_API_KEY
981
- -h, --help this help (Japanese when LANG / LC_ALL / LC_MESSAGES starts with ja)
1231
+ --lang=en|ja this help's language, and --serve's page default; overrides SYS1GREP_LANG
1232
+ -h, --help this help (--lang / SYS1GREP_LANG when set, else Japanese when the first set of LC_ALL /
1233
+ LC_MESSAGES / LANG starts with ja)
982
1234
  -V, --version print the version and exit
983
1235
  ```
984
1236
 
985
1237
  Without FILE, stdin is read. With several files, output is prefixed with `file:`.
986
1238
  Exit codes follow grep: 0 matched, 1 no match, 2 error (bad arguments, unreadable file, API failure).
1239
+ Exit 1 after lines were sent prints one line on stderr: the highest probability, its line, the option that
1240
+ loosens the threshold (unless even that would not match), and `-p -t 0` (`-T 1` too with a negation) to see every
1241
+ probability (`sys1grep: no line reached 0.5 for "…"; the highest was 0.42 (app.log:118). ...`).
1242
+ Not with `-q`, `--step-to` or `--step-reverse-to`, nor when only a negated meaning kept lines out.
987
1243
 
988
1244
  ### Expression grammar
989
1245
 
990
- `-e` starts an OR term. `-a` and `-v` extend the previous term with AND and AND NOT.
991
- A leading `!` on a meaning negates just that meaning (quote it, `!` is history expansion in most shells).
1246
+ `-e` and `-P` each start an OR term, the first always a meaning, the second always a regex. `-a` and
1247
+ `-v` extend the previous term with AND and AND NOT.
1248
+ A leading `!` on a meaning negates just that meaning (quote it, `!` is history expansion in most shells);
1249
+ a leading `!` on a `-P` value is refused instead — negate a regex term with `-v '/RE/'`.
992
1250
 
993
1251
  | command | means |
994
1252
  |---|---|
@@ -1001,10 +1259,10 @@ A leading `!` on a meaning negates just that meaning (quote it, `!` is history e
1001
1259
 
1002
1260
  ## How it works
1003
1261
 
1004
- 1. Non-blank lines are cut into chunks of 30 lines (with a character cap).
1262
+ 1. Non-blank lines are cut into chunks of 30 lines (with a character cap, and at most 64 questions a request).
1005
1263
  2. Each chunk goes into `state` as an object `{"L000": "line 1", "L001": "line 2", ...}`,
1006
1264
  and one `noul` (yes/no probability) question per line × meaning goes into the same request.
1007
- 3. Up to 8 requests run concurrently. Output is printed in file order.
1265
+ 3. Up to 8 requests run concurrently (`-j`; 1 for a local URL). Output is printed in file order.
1008
1266
  4. Per line, each meaning's probability is thresholded to a boolean and the AND / OR / NOT expression is evaluated.
1009
1267
 
1010
1268
  Lines sent together are each other's context: Jev judges a line against what the rest of the chunk shows
@@ -1029,19 +1287,24 @@ the runner evaluates the boolean expression on those verdicts and compares with
1029
1287
  reporting precision and recall. It also sweeps `-t` × `-T` in 0.05 steps and reports the best pair.
1030
1288
 
1031
1289
  ```sh
1032
- node --no-warnings tests/judge.mts [--model sonnet] [--rejudge]
1290
+ npm run judge [-- --model sonnet] [--rejudge] # runs tests/judge.mts; .mts needs Node 23.6+
1033
1291
  ```
1034
1292
 
1035
1293
  Judge verdicts are cached in `tests/verdicts.json`; later runs do not call the judge.
1036
1294
  The result is written to `tests/report.md`. Latest: precision 0.94, recall 0.98.
1037
1295
 
1296
+ The judge is one of several test entry points. `npm run test:offline` checks sys1grep itself against a fake Jev
1297
+ (`tests/fake-jev.mjs`): no key, no network, the same result every run (`OFFLINE_ONLY=PATTERN` runs only the matching
1298
+ sections). `npm test` adds `tests/check.sh` against real Jev. [CLAUDE.md](CLAUDE.md) lists every entry point with what it
1299
+ checks and what it needs.
1300
+
1038
1301
  ## Limits
1039
1302
 
1040
- - Every searched line is sent to api.typesafe.ai. Do not run it over files you would not upload there.
1303
+ - Every searched line is sent to api.typesafe.ai (or wherever `SYS1GREP_URL` points). Do not run it over files you would not upload there.
1041
1304
  - Blank lines are not sent; they count as probability 0 for every meaning, so `-v X` prints them and `-e X` never does.
1042
- - Lines are truncated to 2,000 characters before sending.
1043
- - The maximum number of questions per request is undocumented; 420 worked.
1044
- - 429 / 529 are retried up to 6 times with exponential backoff.
1305
+ - Lines are truncated to 2,000 characters before sending (8,000 with `-z` or `--unit=function`; `-M` sets it).
1306
+ - The maximum number of questions per request is undocumented; 420 worked. sys1grep itself sends at most 64 a request (Clef's cap); only a single unit asked more than 64 meanings goes out whole.
1307
+ - 429, 529 and other 5xx answers, connection errors and timeouts are retried up to 6 times with exponential backoff.
1045
1308
  - Accuracy is best in English. Japanese works but is noisier.
1046
1309
 
1047
1310
  ## FAQ