@uehaj/sys1grep 0.5.0-next.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/LICENSE +20 -0
- package/README.ja.md +775 -0
- package/README.md +799 -0
- package/docs/color.svg +15 -0
- package/docs/demo.svg +83 -0
- package/docs/sentence.svg +14 -0
- package/git-sys1grep.mjs +4 -0
- package/package.json +44 -0
- package/sys1grep.mjs +1347 -0
package/README.md
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
# sys1grep — System 1 for your files; uses TypeSafe AI's Jev
|
|
2
|
+
|
|
3
|
+
[日本語版はこちら](README.ja.md)
|
|
4
|
+
|
|
5
|
+
Background and design notes (Japanese): [Jevのキラーアプリ、「意味で探す grep」を作った](https://zenn.dev/uehaj/articles/jev-semgrep-grep-by-meaning) on Zenn.
|
|
6
|
+
|
|
7
|
+
A grep that finds lines by **what they mean**, not by regular expressions.
|
|
8
|
+
Matching is done by **Jev**, the System One model from [TypeSafe AI](https://typesafe.ai/).
|
|
9
|
+
Jev does not generate text. It answers typed questions with probabilities, so for every line
|
|
10
|
+
sys1grep asks "does this line match the meaning *network failure*?", gets a probability back,
|
|
11
|
+
and applies a threshold.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
./sys1grep -n -e "customer is angry or frustrated" tickets.txt
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
[](https://uehaj.github.io/sys1grep/)
|
|
18
|
+
|
|
19
|
+
<sub>▶ Click the demo, or open <a href="https://uehaj.github.io/sys1grep/">uehaj.github.io/sys1grep</a>, for the full demo on the landing page.</sub>
|
|
20
|
+
|
|
21
|
+
- Zero dependencies. One file, Node.js 20.16+ and `fetch`.
|
|
22
|
+
- Fast. 30 lines go into one request, requests run 8 at a time. A 210-line file finishes in under a second.
|
|
23
|
+
- Meanings combine with AND / OR / NOT.
|
|
24
|
+
- **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.
|
|
25
|
+
|
|
26
|
+
## Search across languages
|
|
27
|
+
|
|
28
|
+
The meaning and the text do not have to share a language. Jev compares concepts, not words,
|
|
29
|
+
so one query finds matching lines in every language the file contains.
|
|
30
|
+
|
|
31
|
+
An **English** meaning finds **Japanese** lines. None of the hits contain "angry" or "frustrated", and two of them are in Japanese:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
$ ./sys1grep -n -e "customer is angry or frustrated" tests/corpus.txt
|
|
35
|
+
14:ユーザー山田さんからの問い合わせ: 返金してほしい、商品が壊れていた
|
|
36
|
+
16:ユーザー佐藤さんからの問い合わせ: 注文した覚えのない請求が来ています。至急確認してください
|
|
37
|
+
18:I want my money back. The item arrived broken and customer service ignored me.
|
|
38
|
+
21:Your product ruined my weekend. Never buying from you again.
|
|
39
|
+
23:This is the third time I'm writing. Nobody has replied to my previous emails.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A **Japanese** meaning finds **English** lines, with the same confidence as the Japanese ones:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
$ ./sys1grep -n -p -e "返金の要求" tests/corpus.txt
|
|
46
|
+
14:ユーザー山田さんからの問い合わせ: 返金してほしい、商品が壊れていた [0.97]
|
|
47
|
+
18:I want my money back. The item arrived broken and customer service ignored me. [0.95]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
It is not a Japanese/English feature. `tests/multi.txt` holds refund requests and thank-you notes in French,
|
|
51
|
+
Russian, German, Spanish, Chinese and Korean. One Japanese meaning finds all six refund requests; a Russian
|
|
52
|
+
meaning does the same:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
$ ./sys1grep -n -p -e "顧客が返金を求めている" tests/multi.txt
|
|
56
|
+
1:Je veux être remboursé, le produit est arrivé cassé. [0.98]
|
|
57
|
+
3:Я требую вернуть деньги, товар не работает. [0.97]
|
|
58
|
+
5:Ich möchte mein Geld zurück, das Gerät ist defekt. [0.97]
|
|
59
|
+
7:Quiero un reembolso, el paquete llegó vacío. [0.97]
|
|
60
|
+
9:我要求退款,商品坏了。 [0.97]
|
|
61
|
+
11:환불해 주세요. 제품이 고장났어요. [0.97]
|
|
62
|
+
|
|
63
|
+
$ ./sys1grep -n -p -e "клиент требует возврат денег" tests/multi.txt
|
|
64
|
+
1:Je veux être remboursé, le produit est arrivé cassé. [0.94]
|
|
65
|
+
3:Я требую вернуть деньги, товар не работает. [0.97]
|
|
66
|
+
5:Ich möchte mein Geld zurück, das Gerät ist defekt. [0.92]
|
|
67
|
+
7:Quiero un reembolso, el paquete llegó vacío. [0.89]
|
|
68
|
+
9:我要求退款,商品坏了。 [0.92]
|
|
69
|
+
11:환불해 주세요. 제품이 고장났어요. [0.89]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This makes sys1grep useful for mixed-language logs and ticket dumps, and for teams whose members
|
|
73
|
+
query in different languages. One caveat: TypeSafe documents English as the most accurate language,
|
|
74
|
+
and in our tests Japanese meanings wobble a little more near the threshold. When a query is borderline,
|
|
75
|
+
phrasing the meaning in English is the safer choice.
|
|
76
|
+
|
|
77
|
+
## How this differs from vector search
|
|
78
|
+
|
|
79
|
+
If all you want is "lines about X", embedding similarity gives you the same lines. sys1grep differs in
|
|
80
|
+
*what* it judges: not how close a line is to a topic, but whether a **proposition** holds for that line.
|
|
81
|
+
Jev reads the line and the question together (a cross-encoder shape), so who did what, negation, and
|
|
82
|
+
"asked for" versus "already done" all change the answer. An embedding of the line is fixed before it
|
|
83
|
+
ever sees your query, so it can only measure topical closeness.
|
|
84
|
+
|
|
85
|
+
All eight lines below are "about a refund" (the same contrast as `tests/contrast.txt`, translated to
|
|
86
|
+
English as `tests/contrast.en.txt` — see [Search across languages](#search-across-languages) above for
|
|
87
|
+
the case where the meaning and the text differ in language). Only two are a customer asking for one:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
$ ./sys1grep -n -p -t 0 -e "customer is asking for a refund" tests/contrast.en.txt
|
|
91
|
+
1:I want a refund. The item was broken. [0.99]
|
|
92
|
+
2:The refund has been processed. Please check your account. [0.11]
|
|
93
|
+
3:Our refund policy is within 30 days of purchase. [0.09]
|
|
94
|
+
4:The manager denied the refund request yesterday [0.17]
|
|
95
|
+
5:I demand a full refund immediately [0.93]
|
|
96
|
+
6:Refunds are processed within 5 business days [0.07]
|
|
97
|
+
7:The support agent got angry and hung up the phone. [0.05]
|
|
98
|
+
8:The customer got angry and hung up the phone. [0.06]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Because each meaning yields an independent probability, **logical AND and NOT are plain boolean
|
|
102
|
+
operations**, not a trick with set differences or "negative queries":
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
# about a refund, but NOT a customer asking for one → completed, policy, denied, timelines
|
|
106
|
+
$ ./sys1grep -n -e "about a refund" -v "the customer is asking for a refund" tests/contrast.en.txt
|
|
107
|
+
2:The refund has been processed. Please check your account.
|
|
108
|
+
3:Our refund policy is within 30 days of purchase.
|
|
109
|
+
4:The manager denied the refund request yesterday
|
|
110
|
+
6:Refunds are processed within 5 business days
|
|
111
|
+
|
|
112
|
+
# angry AND it is the customer, not the staff
|
|
113
|
+
$ ./sys1grep -n -e "someone is angry" -a "the customer, not the staff, is the one acting" tests/contrast.en.txt
|
|
114
|
+
5:I demand a full refund immediately
|
|
115
|
+
8:The customer got angry and hung up the phone.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Line 7, `The support agent got angry and hung up the phone.`, scores 0.05 on the second meaning and is
|
|
119
|
+
excluded, even though it differs from line 8 by one word (*support agent* vs. *customer*) — wording an
|
|
120
|
+
embedding would place right next to line 8's. (We could not reproduce a cosine-similarity number for
|
|
121
|
+
this pair: no embedding-model access was available in this environment, and no prior measurement script
|
|
122
|
+
exists in the repository or its history to rerun. The argument stands on the wording alone: an embedding
|
|
123
|
+
of line 7 has no way to see that "support agent" changes who the sentence is about.)
|
|
124
|
+
|
|
125
|
+
Two more practical consequences. The probabilities are calibrated, so one threshold (0.5) works across
|
|
126
|
+
queries, where cosine scores need top-k or per-query tuning. And there is no index to build: sys1grep reads
|
|
127
|
+
the files in front of you. The flip side is that every query pays for the whole corpus again, so for
|
|
128
|
+
repeated queries over a large, fixed corpus a vector index is cheaper and faster.
|
|
129
|
+
|
|
130
|
+
## Regex terms
|
|
131
|
+
|
|
132
|
+
`-e`/`-a`/`-v '/pattern/flags'` (first and last character `/`, JavaScript flags) is matched locally,
|
|
133
|
+
as a plain regex, with no request at all. It prefilters its AND term: only the lines it holds for
|
|
134
|
+
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
|
|
135
|
+
only if some term's regexes all hold for it (a term with no regex holds for every line), so with
|
|
136
|
+
`-e '/re/' -a A -e B` a line without `re` is still sent, asked `B` only. A query of regex terms alone
|
|
137
|
+
sends nothing, except that `--sentence` (`=jev`, the default) still asks Jev where wrapped lines
|
|
138
|
+
break; use `--sentence=rules` to stay offline. Anything that isn't shaped like `/…/flags` is still a meaning, so
|
|
139
|
+
`-e '/etc 以下のファイルを変更している'` (no closing `/`) is unaffected; a meaning that really starts
|
|
140
|
+
and ends with `/` can be written with a leading space to dodge the regex reading.
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
$ ./sys1grep -e '/ERROR|FATAL/' app.log # no requests at all
|
|
144
|
+
$ ./sys1grep -e '/timeout/i' -a '顧客に影響が出ている' app.log # only lines with "timeout" go to Jev
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A regex's named and numbered groups pass to the other meanings of the *same* AND term as
|
|
148
|
+
`$<name>`, `$1`-`$99`, `$&`, `$$` — ECMAScript's replacement-pattern syntax
|
|
149
|
+
(`String.prototype.replace`'s `GetSubstitution`), with one deviation: `$<name>` naming no group is
|
|
150
|
+
an error rather than an empty string (so is a reference to a negated regex's group). A `$n` naming
|
|
151
|
+
no group stays literal, as in ECMAScript, so `$100 以上の請求` is unaffected.
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
$ ./sys1grep -e '/(?<date>\d{4}-\d\d-\d\d) (?<time>\d\d:\d\d)/' \
|
|
155
|
+
-a '$<time> が深夜(0時〜5時)であり、$<date> が週末である' app.log
|
|
156
|
+
# 2026-09-19 03:12 ... → asks "03:12 が深夜(0時〜5時)であり、2026-09-19 が週末である"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Prefer `$<name>` and single quotes: `$<name>` survives double quotes in sh/bash/zsh; `$1`, `$time`
|
|
160
|
+
and `${time}` don't (the shell expands them itself). `-p` prints `1.00`/`0.00` for a regex term.
|
|
161
|
+
|
|
162
|
+
On a terminal every match of a regex is in grep's match color (bold red), for the terms that held; a negated regex
|
|
163
|
+
is never colored. `-o` prints each match on a line of its own, as `grep -o`; a line only meanings matched prints
|
|
164
|
+
whole, since a meaning has no matching part.
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
$ ./sys1grep -o -n -e '/[A-Z]+-\d+/' -a 'the ticket is still open' notes.txt # the ticket ids, one per line
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Sending less
|
|
171
|
+
|
|
172
|
+
Every line sent to Jev costs money and time, so the cheapest line is the one never sent. From the widest cut
|
|
173
|
+
to the narrowest:
|
|
174
|
+
|
|
175
|
+
- **Which files.** `-r` skips `.git`, `node_modules`, binary files, likely secrets and what git ignores;
|
|
176
|
+
[`git sys1grep`](#as-a-git-subcommand-git-sys1grep) searches tracked files only. `--include` / `--exclude`
|
|
177
|
+
(file-name globs) and `--changed-within` (`30m`, `7d`, `today`, `this-week`, a date) narrow them further.
|
|
178
|
+
A meaning that restricts itself to a language or a time of change narrows them by itself: see
|
|
179
|
+
[Scope from the meaning](#scope-from-the-meaning).
|
|
180
|
+
- **Which lines.** A [regex term](#regex-terms) is matched locally, and only the lines it holds for are asked
|
|
181
|
+
its AND term's meanings. Blank lines are never sent.
|
|
182
|
+
- **How many times.** [`--dedup`](#one-line-per-template---dedup) judges one line per template: lines that
|
|
183
|
+
differ only in ids, numbers, times or paths share one answer.
|
|
184
|
+
- **Check before paying.** `--dry-run` sends nothing and prints the files, how many lines each would send and
|
|
185
|
+
every request with its questions. Its last line estimates the input tokens and, for TypeSafe itself, the price
|
|
186
|
+
(`~3178 input tokens, ~$0.000133`; within about 10%). `-i` shows the same totals on the terminal and sends only
|
|
187
|
+
after `y`.
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
$ sys1grep --dry-run -r --include='*.log' --changed-within=today -e '/ERROR|FATAL/' -a 'a customer is affected' logs/
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Install
|
|
194
|
+
|
|
195
|
+
Two ways to use it: as a command-line tool (this section), or as a Claude Code skill
|
|
196
|
+
(see [Use it from Claude Code](#use-it-from-claude-code) below). The skill falls back to `npx @uehaj/sys1grep`,
|
|
197
|
+
so if you only use it through Claude Code you can skip the install here entirely and just set the API key.
|
|
198
|
+
|
|
199
|
+
Requires Node.js 20.16 or later. No other dependencies.
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
npm install -g @uehaj/sys1grep
|
|
203
|
+
sys1grep --help
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
To try it without installing, run it through `npx` (the first run downloads the package, later runs use the cache):
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
npx @uehaj/sys1grep -n -e "customer is angry or frustrated" tickets.txt
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Then give it an API key from the [TypeSafe console](https://console.typesafe.ai/). Any one of these works:
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
export SYS1GREP_API_KEY=your-key # environment variable
|
|
216
|
+
echo 'SYS1GREP_API_KEY=your-key' > ~/.config/sys1grep/.env # per user (mkdir -p first)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Variables already in the environment win; otherwise `~/.config/sys1grep/.env` fills them in. A `.env` in the current
|
|
220
|
+
directory is never read: it may belong to a repository you just cloned, and could send your key elsewhere through
|
|
221
|
+
`SYS1GREP_URL`. For per-project settings, load a file yourself: `node --env-file=.env "$(command -v sys1grep)" ...`.
|
|
222
|
+
`TYPESAFE_API_KEY` is accepted too when `SYS1GREP_API_KEY` is not set.
|
|
223
|
+
|
|
224
|
+
### Default options
|
|
225
|
+
|
|
226
|
+
`SYS1GREP_OPTS` holds options to apply on every call, read like the settings above. It is split on spaces and put in
|
|
227
|
+
front of the command line, so the command line wins: a later value counts, and `--no-X` turns a default flag off.
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
export SYS1GREP_OPTS='--level strict -j 8 -n'
|
|
231
|
+
sys1grep -e "payment failed" app.log # strict, 8 at once, line numbers
|
|
232
|
+
sys1grep --level loose --no-n -e "payment failed" app.log
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
A script calling sys1grep would pick these up too (grep dropped `GREP_OPTIONS` for that reason). Call it as
|
|
236
|
+
`SYS1GREP_OPTS= sys1grep ...` in scripts.
|
|
237
|
+
|
|
238
|
+
### Other endpoints
|
|
239
|
+
|
|
240
|
+
The API is configured by exactly three settings: `SYS1GREP_API_KEY` (or `TYPESAFE_API_KEY`), `SYS1GREP_URL` and `SYS1GREP_MODEL`.
|
|
241
|
+
Any endpoint that speaks TypeSafe's `POST /v1/systemone` works. The key is sent to `SYS1GREP_URL` as is, so set the
|
|
242
|
+
two together. On the command line, `--sys1-model=ID`, `--sys1-url=URL` and `--sys1-api-key=KEY` override
|
|
243
|
+
the three. A key given this way shows up in `ps` and shell history, so prefer `~/.config/sys1grep/.env` for it.
|
|
244
|
+
|
|
245
|
+
```sh
|
|
246
|
+
# OpenRouter
|
|
247
|
+
SYS1GREP_URL=https://openrouter.ai/api/v1/systemone SYS1GREP_API_KEY=sk-or-... sys1grep -e ...
|
|
248
|
+
# Vercel AI Gateway
|
|
249
|
+
SYS1GREP_URL=https://ai-gateway.vercel.sh/typesafe/v1/systemone SYS1GREP_MODEL=typesafe-ai/jev SYS1GREP_API_KEY=vck_... sys1grep -e ...
|
|
250
|
+
# A compatible server that needs no key: no Authorization header is sent
|
|
251
|
+
SYS1GREP_URL=http://localhost:8000/v1/systemone sys1grep -e ...
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The summary line shows the cost the endpoint reports (`usage.cost`), or for TypeSafe itself an estimate at list
|
|
255
|
+
price marked `~`.
|
|
256
|
+
|
|
257
|
+
From source: `git clone https://github.com/uehaj/sys1grep.git && cd sys1grep && npm install -g .`,
|
|
258
|
+
or run it in place with `node sys1grep.mjs ...`.
|
|
259
|
+
|
|
260
|
+
## Examples
|
|
261
|
+
|
|
262
|
+
All examples run against [`tests/corpus.txt`](tests/corpus.txt), a 51-line mix of server logs,
|
|
263
|
+
support tickets in English and Japanese, source code, SQL and small talk. Where a Japanese line would
|
|
264
|
+
otherwise show up in the output below, this section instead uses [`tests/corpus.en.txt`](tests/corpus.en.txt),
|
|
265
|
+
the same 51 lines with the Japanese ones translated to English — the cross-language behaviour itself is
|
|
266
|
+
shown once, in [Search across languages](#search-across-languages) above.
|
|
267
|
+
|
|
268
|
+
### Find lines by a concept, in any language
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
$ ./sys1grep -n -e "customer is angry or frustrated" tests/corpus.en.txt
|
|
272
|
+
14:Inquiry from user Yamada: I want a refund, the item was broken
|
|
273
|
+
16:Inquiry from user Sato: I'm being charged for an order I never placed, it looks like fraud, please check urgently
|
|
274
|
+
18:I want my money back. The item arrived broken and customer service ignored me.
|
|
275
|
+
21:Your product ruined my weekend. Never buying from you again.
|
|
276
|
+
23:This is the third time I'm writing. Nobody has replied to my previous emails.
|
|
277
|
+
5 of 51 lines matched; 51 sent to Jev in 2 requests, 3054 input tokens, ~$0.000128
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
None of these lines contain the words "angry" or "frustrated".
|
|
281
|
+
|
|
282
|
+
### Lines that answer a question (`-Q`)
|
|
283
|
+
|
|
284
|
+
`-e` asks whether a line states a meaning. `-Q QUESTION` (`--question`) finds lines that answer it instead,
|
|
285
|
+
so you can write the question as you would ask it:
|
|
286
|
+
|
|
287
|
+
```sh
|
|
288
|
+
$ echo "The job failed." | ./sys1grep -e "Did the job succeed?"
|
|
289
|
+
$ echo "The job failed." | ./sys1grep -Q "Did the job succeed?"
|
|
290
|
+
The job failed.
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
As a meaning, "Did the job succeed?" is not what the line says (0.08), so `-e` finds nothing. As a question,
|
|
294
|
+
the line answers it: no, it failed (0.83). A line that *asks* the question is close to it in meaning but
|
|
295
|
+
answers nothing, and for a yes/no question a line that *denies* it still answers it:
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
$ ./sys1grep -n -Q "whether the server is down" tests/intent.txt
|
|
299
|
+
5:The server is down.
|
|
300
|
+
6:The server is healthy and responding normally.
|
|
301
|
+
2 of 17 lines matched; 17 sent to Jev in 1 request, 1087 input tokens, ~$0.000046
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Both the confirming line and the denying line match: each settles whether the server is down. `Is the
|
|
305
|
+
server down?` does not match, because asking is not answering; `-e "asking whether the server is down"`
|
|
306
|
+
would match it instead. `-Q X` is shorthand for `-e "the line answers: X"`, so it combines with `-a` / `-v`
|
|
307
|
+
/ `!` and OR's with other terms exactly like `-e`.
|
|
308
|
+
|
|
309
|
+
### OR: two meanings, and see the probabilities with `-p`
|
|
310
|
+
|
|
311
|
+
```sh
|
|
312
|
+
$ ./sys1grep -n -p -e "customer is asking for a refund" -e "delivery address change request" tests/corpus.en.txt
|
|
313
|
+
14:Inquiry from user Yamada: I want a refund, the item was broken [0.99 0.01]
|
|
314
|
+
17:Inquiry from user Takahashi: I'd like to change the delivery address [0.01 0.99]
|
|
315
|
+
18:I want my money back. The item arrived broken and customer service ignored me. [0.96 0.01]
|
|
316
|
+
22:Can I change the delivery address for order #8821? [0.02 0.99]
|
|
317
|
+
4 of 51 lines matched; 51 sent to Jev in 2 requests, 4275 input tokens, ~$0.000180
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The bracket shows one probability per meaning, in the order given. Use it to pick a threshold.
|
|
321
|
+
|
|
322
|
+
With `--color` (on by default in a terminal) the probabilities are colored against the thresholds:
|
|
323
|
+
green at or above `-t`, red below `-T`, yellow in between. Line numbers and file names use grep's colors.
|
|
324
|
+
|
|
325
|
+

|
|
326
|
+
|
|
327
|
+
### AND NOT: network errors, excluding retries
|
|
328
|
+
|
|
329
|
+
```sh
|
|
330
|
+
$ ./sys1grep -n -e "network or remote connection failure" -v "a retry is happening or was attempted" tests/corpus.txt
|
|
331
|
+
4:2026-09-19 08:02:30 ERROR connection reset by peer while calling payment-gateway
|
|
332
|
+
6:2026-09-19 08:02:35 ERROR timeout after 5000ms waiting for payment-gateway
|
|
333
|
+
9:2026-09-19 08:10:44 ERROR DNS lookup failed for api.example.com
|
|
334
|
+
11:2026-09-19 09:00:00 ERROR SSL handshake failed: certificate expired
|
|
335
|
+
13:network unreachable: no route to host 10.0.0.5
|
|
336
|
+
30:except ConnectionError as e:
|
|
337
|
+
31: logger.error("upstream unreachable: %s", e)
|
|
338
|
+
7 of 51 lines matched; 51 sent to Jev in 2 requests, 5112 input tokens
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Line 5, `retrying payment-gateway request (attempt 2/3)`, is a network failure but is dropped by `-v`.
|
|
342
|
+
|
|
343
|
+
### Mixed: (finance AND negative) OR weather
|
|
344
|
+
|
|
345
|
+
```sh
|
|
346
|
+
$ ./sys1grep -n -e "about economy, finance or markets" -a "the news is negative or a decline" -e "about weather" tests/corpus.en.txt
|
|
347
|
+
36:Today's weather is sunny, high of 28 degrees
|
|
348
|
+
43:Stock prices fell 3% after the earnings report missed expectations.
|
|
349
|
+
48:Tomorrow's forecast is rain so I'll bring an umbrella
|
|
350
|
+
3 of 51 lines matched; 51 sent to Jev in 2 requests, 5499 input tokens, ~$0.000231
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`The central bank raised interest rates` is about finance but not a decline, so it is out.
|
|
354
|
+
|
|
355
|
+
### Strictness presets
|
|
356
|
+
|
|
357
|
+
```sh
|
|
358
|
+
$ ./sys1grep --level strict -n -e "a security risk or dangerous destructive operation" tests/corpus.en.txt
|
|
359
|
+
33:DROP TABLE sessions;
|
|
360
|
+
49:API keys must never be committed to the repository.
|
|
361
|
+
|
|
362
|
+
$ ./sys1grep --level loose -n -e "a security risk or dangerous destructive operation" tests/corpus.en.txt
|
|
363
|
+
11:2026-09-19 09:00:00 ERROR SSL handshake failed: certificate expired
|
|
364
|
+
16:Inquiry from user Sato: I'm being charged for an order I never placed, it looks like fraud, please check urgently
|
|
365
|
+
33:DROP TABLE sessions;
|
|
366
|
+
49:API keys must never be committed to the repository.
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`strict` keeps only what the model is sure about. `loose` also pulls in the expired certificate and the suspicious-billing ticket.
|
|
370
|
+
|
|
371
|
+
### Recursive search and file names only
|
|
372
|
+
|
|
373
|
+
```sh
|
|
374
|
+
$ ./sys1grep -r -n -e "customer is asking for a refund" tests/tickets/
|
|
375
|
+
tests/tickets/a.txt:7:Ticket #16: I want a refund, the item was broken.
|
|
376
|
+
tests/tickets/sub/b.txt:1:The customer wants a refund for the broken lamp.
|
|
377
|
+
|
|
378
|
+
$ ./sys1grep -rl -e "customer is asking for a refund" tests/tickets/
|
|
379
|
+
tests/tickets/a.txt
|
|
380
|
+
tests/tickets/sub/b.txt
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
`-r` walks directories in sorted order and skips `.git`, `node_modules`, `.ssh`, `.aws`, `.gnupg`, `.kube`,
|
|
384
|
+
`.docker`, binary files (a NUL byte in the first 8 KB, or a PDF; UTF-16 with a BOM is read as text) and files
|
|
385
|
+
that usually hold secrets (`.env*`, `.netrc`, `.npmrc`, `.pypirc`, `.pgpass`, `.git-credentials`, `*.pem`,
|
|
386
|
+
`*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `id_rsa*` and friends; names compared without case). **Every line that is searched is sent to the TypeSafe API**, so point `-r` at a directory
|
|
387
|
+
you mean to scan. Inside a git repository, `-r` also skips what git ignores (`.gitignore`, `.git/info/exclude`, the
|
|
388
|
+
global excludes file), so build output and local files stay home; a tracked file is searched even if it matches.
|
|
389
|
+
A file named explicitly on the command line is always searched, even if it matches the skip list or is ignored;
|
|
390
|
+
so is a directory that git ignores, when you name it (`sys1grep -r -e ... dist`). `-l` prints each
|
|
391
|
+
matching file once, in the order matches are found, and works with or without `-r`. `-c` prints the number of
|
|
392
|
+
matching lines per file instead.
|
|
393
|
+
|
|
394
|
+
### Scope from the meaning
|
|
395
|
+
|
|
396
|
+
A meaning that restricts its matches to some kind of file can only match in such files. With `-r` and
|
|
397
|
+
`git sys1grep`, each meaning first asks Jev, in one small request, a yes / no per candidate, and a yes at 0.6 or
|
|
398
|
+
more narrows the files before anything else is sent. The rest is never read or sent. Each scope is reported on
|
|
399
|
+
stderr with Jev's answer, so a wrong one is visible:
|
|
400
|
+
|
|
401
|
+
```sh
|
|
402
|
+
$ sys1grep -r -e 'Python でリトライ処理を書いている箇所' .
|
|
403
|
+
sys1grep: scope: *.py *.pyi *.pyw (from "Python files: 0.94")
|
|
404
|
+
sys1grep: scope: 12 of 340 files
|
|
405
|
+
$ sys1grep -r -e '昨日変えた箇所で認証を扱っている' src/
|
|
406
|
+
sys1grep: scope: modified since 2026-09-25 00:00 (from "what was changed yesterday: 0.91")
|
|
407
|
+
sys1grep: scope: 3 of 120 files
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
- **Language or format**: 26 candidates (Python, JavaScript, TypeScript, Go, Rust, Java, Kotlin, Ruby, PHP, C, C++,
|
|
411
|
+
C#, Swift, Scala, R, shell script, SQL, HTML, CSS, Markdown, YAML, JSON, TOML, XML, Dockerfile, Makefile), with
|
|
412
|
+
GitHub Linguist's extensions and file names. Several yes answers are alternatives ("JavaScript か TypeScript").
|
|
413
|
+
- **Time of change**: 14 spans: the last minute, the last hour, today, yesterday, the last 1 / 2 / 3 / 7 / 30 days,
|
|
414
|
+
this month, last week, last month, the last year, this fiscal year (from April 1). The narrowest span answered
|
|
415
|
+
yes is taken, by its start only: a file changed yesterday may have been modified again today.
|
|
416
|
+
- **Place**: test code (`tests/ test/ __tests__/ spec/ e2e/`, `test_*.py *_test.* *.test.* *.spec.* *Test.java
|
|
417
|
+
*_spec.rb`, and every `*.rs`: Rust keeps unit tests inside the file), database migrations (`*migrat*`, Flyway's
|
|
418
|
+
`V1__*.sql`), the README, the changelog (`CHANGELOG* CHANGES* HISTORY* NEWS*`), documentation (`*.md *.rst *.adoc
|
|
419
|
+
*.txt`, `docs/`), source code (whatever is not documentation, so languages missing from the dictionary still
|
|
420
|
+
count) and logs (`*.log *.log.N *.out *.err`, `logs/`), by the path conventions of JS, Python, Go, Java, Ruby, Rust
|
|
421
|
+
and PHP. Several places are alternatives ("README か CHANGELOG に").
|
|
422
|
+
- **git**: in a repository, a time goes by commits: a committed file needs a commit at or after the start (the
|
|
423
|
+
committer date; a checkout sets every mtime to now), an uncommitted one its mtime. States: uncommitted (worktree
|
|
424
|
+
and index against HEAD, and untracked files), staged, untracked, this branch (since it left `origin/HEAD`, `main`
|
|
425
|
+
or `master`), not pushed (`@{upstream}..HEAD`, else commits on no remote branch), and mine (`user.email`'s
|
|
426
|
+
commits and the uncommitted files). Authors: the 30 with the most commits (`git shortlog`), each a candidate by
|
|
427
|
+
name and e-mail; a yes narrows to every file a commit of theirs touched. Outside a repository none of these is
|
|
428
|
+
asked, and a time goes by the mtime.
|
|
429
|
+
- Jev reads the whole meaning, in any language: "案A、B、Cで比較" is not about C files, and a date quoted in a
|
|
430
|
+
comment is not when the file changed. Nothing is extracted from the text; the candidates are fixed.
|
|
431
|
+
- **Per term.** `-e A -e B` still searches B in the files A's scope leaves out; within an AND term, and across
|
|
432
|
+
categories ("Python のテストコード"), the scopes intersect. Negated meanings (`-v`, `!`) are not asked. The
|
|
433
|
+
meaning is sent unchanged.
|
|
434
|
+
- The question costs one small request per meaning, sent only when `-r` / `git sys1grep` found something to
|
|
435
|
+
narrow, and after `-i`'s answer. `--dry-run` shows it as `[scope]`. As with `--include`, with `-r` a file named on
|
|
436
|
+
the command line and stdin are never narrowed, and `git sys1grep`'s pathspecs are narrowed like the rest. `--no-auto-scope` turns it off (`--auto-scope` turns it back on).
|
|
437
|
+
|
|
438
|
+
### As a git subcommand (`git sys1grep`)
|
|
439
|
+
|
|
440
|
+
`npm install -g` also installs `git-sys1grep`, so git runs it as `git sys1grep`. Like `git grep`, it searches only
|
|
441
|
+
the files git tracks (ignored files and build output are never sent), and FILE arguments are pathspecs relative
|
|
442
|
+
to the current directory.
|
|
443
|
+
|
|
444
|
+
```sh
|
|
445
|
+
$ cd tests && git sys1grep -l -e "customer is asking for a refund" fixture.txt tickets
|
|
446
|
+
fixture.txt
|
|
447
|
+
tickets/a.txt
|
|
448
|
+
tickets/sub/b.txt
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Without FILE it searches every tracked file under the current directory. The `-r` skip list (`.env*`, keys, ...)
|
|
452
|
+
applies even to tracked files. `--include`, `--exclude` and `--changed-within` filter the tracked files, those named
|
|
453
|
+
by a pathspec included. `--changed-within` reads the working tree's modification times, not git history: right after
|
|
454
|
+
a clone or a checkout, every file it wrote counts as just changed. For help use `git sys1grep -h`: git takes `--help` itself and looks for a man page.
|
|
455
|
+
|
|
456
|
+
### Everything that is *not* something
|
|
457
|
+
|
|
458
|
+
```sh
|
|
459
|
+
./sys1grep -v "a timestamped server log line" mixed.txt # like grep -v
|
|
460
|
+
./sys1grep -e "source code or SQL" -v "SQL" src.txt # code, but not SQL
|
|
461
|
+
cat app.log | ./sys1grep -e "the deploy failed or was rolled back"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Records that span several lines (`-z`)
|
|
465
|
+
|
|
466
|
+
The unit of judgement is a line. That is right for logs and source, and wrong when one record spans
|
|
467
|
+
several lines. `-z` makes the unit a NUL-terminated record instead, exactly as in `grep -z`, so it pairs
|
|
468
|
+
with the tools that already emit records: `git log -z`, `find -print0`, `xargs -0`.
|
|
469
|
+
|
|
470
|
+
A proposition like "this commit changes user-visible behaviour" is true of a whole commit, not of any one
|
|
471
|
+
line in it:
|
|
472
|
+
|
|
473
|
+
```sh
|
|
474
|
+
$ git log -z --format='%h %s %b' | ./sys1grep -z -n -e "the change alters user-visible behaviour" -v "documentation only"
|
|
475
|
+
7:21120e9 Revert "feat: ship the /sys1grep Claude Code skill" ...
|
|
476
|
+
8:51ae333 feat: ship the /sys1grep Claude Code skill
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Matching records are printed NUL-terminated too, so pipe them through `tr '\0' '\n'` to read them.
|
|
480
|
+
File names (`-l`) and counts (`-c`) stay on newlines, as they do in grep. With `-z`, `-n` numbers records,
|
|
481
|
+
`-A` / `-B` / `-C` count neighbouring records, and `--chunk` counts records per request.
|
|
482
|
+
|
|
483
|
+
### One sentence at a time (`--sentence`)
|
|
484
|
+
|
|
485
|
+
`--sentence` judges each sentence instead of each line. The output is still lines, as in grep: every line a
|
|
486
|
+
matching sentence touches is printed, and on a terminal the sentence itself is in bold yellow (a regex match inside it, in grep's bold red).
|
|
487
|
+
Wrapped lines are joined before splitting, so a sentence that runs over several lines is judged as one.
|
|
488
|
+
[`tests/prose.txt`](tests/prose.txt) wraps an English paragraph and a Japanese one:
|
|
489
|
+
|
|
490
|
+
```sh
|
|
491
|
+
$ ./sys1grep -n --sentence -e "the author admits they made a mistake" tests/prose.txt
|
|
492
|
+
1:I should have checked the input
|
|
493
|
+
2:before shipping, and that was my
|
|
494
|
+
3:mistake. Next time I will add a test
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The sentence starts on line 1 and ends at `mistake.` on line 3; only that part is colored, not
|
|
498
|
+
`Next time I will add a test`, which is judged separately and does not match.
|
|
499
|
+
|
|
500
|
+
On a terminal, with both meanings and `-C 3` for context, the colors show where each sentence starts and ends
|
|
501
|
+
inside a line: lines 3 and 9 are colored only up to the end of the matching sentence, and lines 4-7 are context (`-`):
|
|
502
|
+
|
|
503
|
+

|
|
504
|
+
|
|
505
|
+
`-o` prints only the matching sentences, one per line, as `grep -o` prints only the matching part.
|
|
506
|
+
`-n` then gives the line where the sentence starts. Japanese is joined without a space, as are Chinese,
|
|
507
|
+
Thai, Lao, Khmer, Myanmar and Tibetan, which do not put spaces between words:
|
|
508
|
+
|
|
509
|
+
```sh
|
|
510
|
+
$ ./sys1grep -n -o --sentence -e "the author admits they made a mistake" -e "customer is asking for a refund" tests/prose.txt
|
|
511
|
+
1:I should have checked the input before shipping, and that was my mistake.
|
|
512
|
+
8:先週買った掃除機が初日から動かないので返金してほしいです。
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Without `-o`, `-c` counts lines and `-A` / `-B` / `-C` count lines, as usual. With `-o` they count sentences.
|
|
516
|
+
With `-z`, each record is split on its own and matching records are printed whole.
|
|
517
|
+
|
|
518
|
+
Jev finds a matching sentence inside a long line on its own, so `--sentence` is not needed for accuracy.
|
|
519
|
+
Use it to see which sentence matched, to get the sentences with `-o`, and when AND should hold within one
|
|
520
|
+
sentence: the expression is evaluated per sentence. For the same reason `-v X` alone prints every line
|
|
521
|
+
with at least one sentence that is not X; to find lines that are not X as a whole, leave `--sentence` off.
|
|
522
|
+
|
|
523
|
+
Japanese and Chinese entries often end without `。`: a chat message, a support ticket, a memo line. Joining
|
|
524
|
+
them would glue separate entries into one "sentence". So by default (`--sentence`, the same as
|
|
525
|
+
`--sentence=jev`) sys1grep asks Jev about each unpunctuated break next to a script written without word
|
|
526
|
+
spaces: "does this line break end a sentence or entry, or is it a wrap inside a sentence?" It sends 30 lines
|
|
527
|
+
per request with one yes/no per break, and keeps the lines apart when the answer is 0.7 or more. On
|
|
528
|
+
[`tests/corpus.txt`](tests/corpus.txt) this keeps the four one-line Japanese tickets apart, so
|
|
529
|
+
`--sentence` finds the same refund requests (lines 14 and 18) as a line-by-line search, where the rules alone
|
|
530
|
+
merged the tickets and missed line 18. The extra requests cost about as much as one more meaning; use
|
|
531
|
+
`--sentence=rules` to skip them. Breaks between English lines are never asked: joining them keeps a space,
|
|
532
|
+
and the full stop still ends the sentence.
|
|
533
|
+
|
|
534
|
+
Where a newline cannot be inside a sentence, lines are not joined: at a blank line, next to brackets or
|
|
535
|
+
`;` (JSON, code), and before a line starting with `-` `*` `+` `#` `>` `"` or a digit (list items,
|
|
536
|
+
headings, quotes, numbers, timestamps). So JSONL keeps one line per record and each line is split on its
|
|
537
|
+
own. Sentences are cut by `Intl.Segmenter` ([Unicode UAX #29](https://unicode.org/reports/tr29/)),
|
|
538
|
+
which splits at `.` `!` `?` `。` `!` `?` but also after abbreviations such as `Mr.`. Logs are not prose:
|
|
539
|
+
consecutive log lines that start with a letter, such as `WARN ...` after `ERROR ...`, get joined.
|
|
540
|
+
|
|
541
|
+
### One line per template (`--dedup`)
|
|
542
|
+
|
|
543
|
+
Cost is proportional to the text sent, and machine-generated logs are mostly one skeleton with a different
|
|
544
|
+
id or number in it. `--dedup` masks ids, hashes, numbers, dates and times, paths and URLs, groups lines by
|
|
545
|
+
the result, sends one line per group and reuses its answer for the rest. What is sent is that line's
|
|
546
|
+
original text, and every line is printed as itself:
|
|
547
|
+
|
|
548
|
+
```sh
|
|
549
|
+
$ ./sys1grep --dedup -n -e "a request failed" app.log
|
|
550
|
+
1:worker request 3fa9c1e27b failed: connection reset
|
|
551
|
+
2:worker request 88d0e41a5c failed: connection reset
|
|
552
|
+
4:worker request 0b7f2a9e13 failed: connection reset
|
|
553
|
+
3 of 6 lines matched; 4 sent to Jev (2 folded by --dedup, ~235 input tokens / ~$0.000010 saved, 21%) in 2 requests, 907 input tokens, ~$0.000038
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Whether a value may be folded depends on the meaning: a number decides "disk usage is above 90%", a time
|
|
557
|
+
decides "happened at night". So Jev is first asked, one small request per meaning, which kinds of value
|
|
558
|
+
could change a match, and those kinds are kept apart (one of the two requests above). With
|
|
559
|
+
`-e "disk usage is above 90%"` the same file keeps `95%` and `12%` apart and prints only `5:disk usage 95%`.
|
|
560
|
+
|
|
561
|
+
Measured on real logs, with nothing kept: a 43,071-line system log folds into 550 templates (1.2% of the
|
|
562
|
+
bytes), `install.log` to 21.1%, a Claude Code transcript (jsonl) only to 56.8%. It is for machine-generated
|
|
563
|
+
logs; prose has no shared skeleton, and a meaning that reads a timestamp folds almost nothing. With `-z` or
|
|
564
|
+
`--sentence` the records or sentences fold instead of lines.
|
|
565
|
+
|
|
566
|
+
After a search, the summary line says what the folding saved: `(2 folded by --dedup, ~235 input tokens /
|
|
567
|
+
~$0.000010 saved, 21%)` above. It estimates what the folded lines would have cost as requests of their own and subtracts
|
|
568
|
+
what the value questions cost. On a small file or on prose, the result can be negative.
|
|
569
|
+
|
|
570
|
+
To see how far your own logs fold before paying for a search, `node scripts/dedup-measure.mjs FILE...`
|
|
571
|
+
counts lines, templates and the share of bytes sent, offline, with the same masks; `--keep=num,time` shows
|
|
572
|
+
a meaning that keeps those kinds apart.
|
|
573
|
+
|
|
574
|
+
The request's other lines are each line's context (#9), and `--dedup` changes them, so a line near the
|
|
575
|
+
threshold can be judged differently than in a full pass.
|
|
576
|
+
|
|
577
|
+
### A summary instead of the lines (`--summarize`)
|
|
578
|
+
|
|
579
|
+
When many lines match, what you want is often the gist: what they say about the meaning you searched for.
|
|
580
|
+
`--summarize` pipes what sys1grep would print to `claude -p --model haiku` (no tools, no settings, no CLAUDE.md),
|
|
581
|
+
asks it to summarize the lines as they bear on the meanings, and prints its answer instead of the lines.
|
|
582
|
+
|
|
583
|
+
```sh
|
|
584
|
+
$ sys1grep -r -n --summarize -e "the API key is read from a file" .
|
|
585
|
+
The key is read in sys1grep.mjs:22 from ~/.config/sys1grep/.env, never from ./.env (sys1grep.mjs:18), ...
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
The expensive model reads only what Jev kept. Asking why `./.env` is no longer read of this repository's
|
|
589
|
+
`git log` (168 commits), Claude's input fell from 22,059 tokens to 1,356, the total cost with Jev's from
|
|
590
|
+
$0.094 to $0.013, with the same answer (#69). It pays when the answer sits in a few lines.
|
|
591
|
+
|
|
592
|
+
- The matching lines leave the machine a second time, to Anthropic.
|
|
593
|
+
- `-n`, `-A/-B/-C`, `-p` and file names go in as they would print; colors never do. No match runs nothing (exit 1).
|
|
594
|
+
- `SYS1GREP_SUMMARIZER` picks the TOOL of a bare `--summarize` (only `claude` so far), `SYS1GREP_SUMMARIZER_MODEL` its model.
|
|
595
|
+
- `-q`, `-l` and `-c` print no lines, so they cannot be combined with it.
|
|
596
|
+
- With `--dedup`, the TOOL gets what Jev got: each template's representative once, marked `(×N like it)`,
|
|
597
|
+
not every line its answer was reused for.
|
|
598
|
+
- More than 200 KB (about 50k tokens) is not sent at all: exit 2, with the size, before the TOOL is paid.
|
|
599
|
+
It is never cut short, since a summary of the first part would read as a summary of all of it.
|
|
600
|
+
|
|
601
|
+
## Use it from Claude Code
|
|
602
|
+
|
|
603
|
+
There is a Claude Code skill that runs sys1grep for you: describe what you are looking for in plain words
|
|
604
|
+
and it builds the expression, runs the search and reports `file:line` hits. It is published in the
|
|
605
|
+
[`uehaj/uehaj-marketplace`](https://github.com/uehaj/uehaj-marketplace) marketplace as the `uehaj` plugin.
|
|
606
|
+
|
|
607
|
+
```sh
|
|
608
|
+
claude plugin marketplace add uehaj/uehaj-marketplace
|
|
609
|
+
claude plugin install uehaj@uehaj-marketplace
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
No separate install of the command-line tool is needed: the skill uses `sys1grep` from your PATH if present,
|
|
613
|
+
otherwise `npx @uehaj/sys1grep`. Only the API key has to be set (see [Install](#install)).
|
|
614
|
+
|
|
615
|
+
Then, inside Claude Code:
|
|
616
|
+
|
|
617
|
+
```
|
|
618
|
+
/uehaj:sys1grep customer is asking for a refund tickets/*.txt
|
|
619
|
+
/uehaj:sys1grep a fix that shipped without a test git log --oneline -200
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Claude Code installs plugins, not single skills. If you want just this one skill, the
|
|
623
|
+
[skills CLI](https://skills.sh/) copies it into `~/.claude/skills/` and it is invoked as `/sys1grep`:
|
|
624
|
+
|
|
625
|
+
```sh
|
|
626
|
+
npx skills add uehaj/uehaj-marketplace --skill sys1grep -a claude-code -g
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
The skill writes the meaning in English, picks `-e` / `-a` / `-v` for AND / OR / NOT, adds `-n`, narrows large
|
|
630
|
+
directories to files worth paying for, and re-runs with `--level loose` or `strict` when the first result looks off.
|
|
631
|
+
The API key and endpoint are read the same way as on the command line (`SYS1GREP_API_KEY`, `~/.config/sys1grep/.env`).
|
|
632
|
+
|
|
633
|
+
## Usage
|
|
634
|
+
|
|
635
|
+
```
|
|
636
|
+
usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]... [FILE...]
|
|
637
|
+
|
|
638
|
+
-e MEANING lines matching this meaning (several -e are OR'd)
|
|
639
|
+
-Q, --question QUESTION lines that answer QUESTION, not lines asking it; the same as
|
|
640
|
+
-e "the line answers: QUESTION" (see "Lines that answer a question" above)
|
|
641
|
+
-a MEANING AND onto the preceding -e/-Q term. -e A -a B -e C = (A and B) or C
|
|
642
|
+
-v MEANING AND NOT onto the preceding -e/-Q term. -e A -v B = A and not B
|
|
643
|
+
At the front it is a bare negation. -v B = not B (like grep -v)
|
|
644
|
+
!MEANING a leading ! negates just that meaning, in -e / -Q / -a / -v alike
|
|
645
|
+
-e A -e '!B' = A or not B. -a '!C' is the same as -v C
|
|
646
|
+
--level=LEVEL strictness preset, sets both thresholds (default normal)
|
|
647
|
+
loose : -t 0.3 -T 0.7 catch more, accept some noise
|
|
648
|
+
normal : -t 0.5 -T 0.5
|
|
649
|
+
strict : -t 0.7 -T 0.3 only confident matches
|
|
650
|
+
-t THRESH positive threshold: match when probability >= THRESH (overrides --level)
|
|
651
|
+
-T THRESH negative threshold: "not X" when probability < THRESH (overrides --level)
|
|
652
|
+
with -t 0.6 -T 0.3 a line at 0.3..0.6 matches neither X nor not-X
|
|
653
|
+
-r recurse into directories (current directory when FILE is omitted);
|
|
654
|
+
skips .git, node_modules, binary files, likely secrets and what git ignores
|
|
655
|
+
--include=GLOB, --exclude=GLOB with -r and git sys1grep, only files whose name matches GLOB, or not
|
|
656
|
+
(with -r a file named on the command line is always searched; git sys1grep's pathspecs are filtered)
|
|
657
|
+
--changed-within=WHEN with -r and git sys1grep, only files modified within 30m / 2h / 7d / 2w, since a date
|
|
658
|
+
or date-time, today, this-week or this-month
|
|
659
|
+
--no-auto-scope do not narrow those files by what a meaning says about them (see Scope from the meaning);
|
|
660
|
+
--auto-scope turns it back on
|
|
661
|
+
-l print only the names of files with a match, not the lines
|
|
662
|
+
-H, --with-filename prefix file names even for a single file; --no-filename never prefixes them
|
|
663
|
+
-A NUM print NUM lines of trailing context after each match (context lines use - as separator)
|
|
664
|
+
-B NUM print NUM lines of leading context before each match
|
|
665
|
+
-C NUM print NUM lines of context before and after (-A NUM -B NUM)
|
|
666
|
+
-c print only a count of matching lines per file (like grep -c)
|
|
667
|
+
-q, --quiet print nothing, stop at the first match; exit 0 on a match, even after an error (like grep -q)
|
|
668
|
+
--chunk=LINES lines per request (default 30)
|
|
669
|
+
Lines in one request are each other's context, so a small chunk changes verdicts
|
|
670
|
+
on ambiguous lines, not just speed
|
|
671
|
+
-j N concurrent requests (default 8)
|
|
672
|
+
-n print line numbers
|
|
673
|
+
-z, --null-data judge NUL-terminated records instead of lines, and print them NUL-terminated (see "Records that span several lines" above)
|
|
674
|
+
--sentence[=HOW] judge each sentence instead of each line; HOW is jev (default) or rules (see "One sentence at a time" above)
|
|
675
|
+
-o with --sentence, print only the matching sentences
|
|
676
|
+
-p print each meaning's probability at the end of the line
|
|
677
|
+
--dry-run send nothing; print the endpoint, each file searched and each request with its questions
|
|
678
|
+
--verbose print the same to stderr while searching
|
|
679
|
+
-i, --interactive show what --dry-run would send, and search only after y on the terminal
|
|
680
|
+
--dedup judge one line per template and reuse its answer for the rest (see "One line per template" above)
|
|
681
|
+
--color[=WHEN] auto (default: color when stdout is a terminal) / always / never; bare --color means auto
|
|
682
|
+
file and line number use grep's colors; with -p, probabilities are
|
|
683
|
+
green at or above the positive threshold, red below the negative one,
|
|
684
|
+
yellow in between. NO_COLOR is honored
|
|
685
|
+
--sys1-model=ID, --sys1-url=URL, --sys1-api-key=KEY
|
|
686
|
+
the API settings, overriding SYS1GREP_MODEL, SYS1GREP_URL, SYS1GREP_API_KEY
|
|
687
|
+
-h, --help this help (Japanese when LANG / LC_ALL / LC_MESSAGES starts with ja)
|
|
688
|
+
-V, --version print the version and exit
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
Without FILE, stdin is read. With several files, output is prefixed with `file:`.
|
|
692
|
+
Exit codes follow grep: 0 matched, 1 no match, 2 error (bad arguments, unreadable file, API failure).
|
|
693
|
+
|
|
694
|
+
### Expression grammar
|
|
695
|
+
|
|
696
|
+
`-e` starts an OR term. `-a` and `-v` extend the previous term with AND and AND NOT.
|
|
697
|
+
A leading `!` on a meaning negates just that meaning (quote it, `!` is history expansion in most shells).
|
|
698
|
+
|
|
699
|
+
| command | means |
|
|
700
|
+
|---|---|
|
|
701
|
+
| `-e A -e B` | A or B |
|
|
702
|
+
| `-e A -a B` | A and B |
|
|
703
|
+
| `-e A -v B` | A and not B |
|
|
704
|
+
| `-e A -a B -v C -e D` | (A and B and not C) or D |
|
|
705
|
+
| `-e A -e '!B'` | A or not B |
|
|
706
|
+
| `-v B` | not B |
|
|
707
|
+
|
|
708
|
+
## How it works
|
|
709
|
+
|
|
710
|
+
1. Non-blank lines are cut into chunks of 30 lines (with a character cap).
|
|
711
|
+
2. Each chunk goes into `state` as an object `{"L000": "line 1", "L001": "line 2", ...}`,
|
|
712
|
+
and one `noul` (yes/no probability) question per line × meaning goes into the same request.
|
|
713
|
+
3. Up to 8 requests run concurrently. Output is printed in file order.
|
|
714
|
+
4. Per line, each meaning's probability is thresholded to a boolean and the AND / OR / NOT expression is evaluated.
|
|
715
|
+
|
|
716
|
+
Lines sent together are each other's context: Jev judges a line against what the rest of the chunk shows
|
|
717
|
+
is normal in the file. Clear matches and non-matches hold, but ambiguous lines can move. Where the chunk
|
|
718
|
+
boundaries fall barely matters (moving them by 15 lines at `--chunk 30` flipped 1 line in 200 of a log), but
|
|
719
|
+
judging a line with little of its file around it does: `--chunk 1` flipped 18 of the same 200, and those
|
|
720
|
+
solitary verdicts were the less reliable ones (#9). So `--chunk` changes results, not just speed, and a
|
|
721
|
+
very short input is judged with little context whatever `--chunk` says. One line at a time is also slow
|
|
722
|
+
(30 lines in one request take about 0.2 s, one line at a time about 7 s).
|
|
723
|
+
Very large chunks start losing lines near the threshold, hence the default of 30.
|
|
724
|
+
Probabilities drift by about ±0.05 between runs. Use `-p` when tuning thresholds.
|
|
725
|
+
|
|
726
|
+
Pricing is $0.042 per million input tokens (September 2026). A 30-line chunk with two meanings is about
|
|
727
|
+
3,000 tokens. Throughput is bounded by the rate limit of 1,200 requests per minute, roughly 36,000 lines
|
|
728
|
+
per minute at the defaults.
|
|
729
|
+
|
|
730
|
+
## Tests
|
|
731
|
+
|
|
732
|
+
`tests/` holds an LLM-as-judge test. Each of the 10 cases in `tests/cases.json` runs against
|
|
733
|
+
`tests/corpus.txt` (51 lines). Claude (`claude -p`) decides which lines truly match each meaning;
|
|
734
|
+
the runner evaluates the boolean expression on those verdicts and compares with sys1grep's output,
|
|
735
|
+
reporting precision and recall. It also sweeps `-t` × `-T` in 0.05 steps and reports the best pair.
|
|
736
|
+
|
|
737
|
+
```sh
|
|
738
|
+
node --no-warnings tests/judge.mts [--model sonnet] [--rejudge]
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Judge verdicts are cached in `tests/verdicts.json`; later runs do not call the judge.
|
|
742
|
+
The result is written to `tests/report.md`. Latest: precision 0.94, recall 0.98.
|
|
743
|
+
|
|
744
|
+
## Limits
|
|
745
|
+
|
|
746
|
+
- Every searched line is sent to api.typesafe.ai. Do not run it over files you would not upload there.
|
|
747
|
+
- Blank lines are not sent; they count as probability 0 for every meaning, so `-v X` prints them and `-e X` never does.
|
|
748
|
+
- Lines are truncated to 2,000 characters before sending.
|
|
749
|
+
- The maximum number of questions per request is undocumented; 420 worked.
|
|
750
|
+
- 429 / 529 are retried up to 6 times with exponential backoff.
|
|
751
|
+
- Accuracy is best in English. Japanese works but is noisier.
|
|
752
|
+
|
|
753
|
+
## FAQ
|
|
754
|
+
|
|
755
|
+
Some options were left out because a standard tool in front of sys1grep already does the job. These are the
|
|
756
|
+
combinations.
|
|
757
|
+
|
|
758
|
+
### How do I judge a whole paragraph as one unit? Why is there no `--paragraph`?
|
|
759
|
+
|
|
760
|
+
Join each paragraph into one line first. `fmt` joins the lines of a paragraph and keeps the blank lines
|
|
761
|
+
between paragraphs:
|
|
762
|
+
|
|
763
|
+
```sh
|
|
764
|
+
fmt -w 100000 essay.txt | sys1grep -n -e "the author admits they made a mistake"
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
Each output line is then a whole paragraph, and `-n` counts the lines of `fmt`'s output, not of the file.
|
|
768
|
+
`fmt` puts a space where it joins two Japanese lines; Jev reads through it. To judge sentences across
|
|
769
|
+
wrapped lines you need neither: `--sentence` already joins them.
|
|
770
|
+
|
|
771
|
+
### How do I search a JSONL chat log one message at a time?
|
|
772
|
+
|
|
773
|
+
Take the text out with `jq` and end each message with NUL, then judge records with `-z`:
|
|
774
|
+
|
|
775
|
+
```sh
|
|
776
|
+
jq -j '.content + "\u0000"' chat.jsonl | sys1grep -z -n --sentence -e "the customer is asking for a refund" | tr '\0' '\n'
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
Adjust `.content` to where your log keeps the text. Each message becomes one record, so a sentence never
|
|
780
|
+
runs into the next speaker's message, and the JSON punctuation is not sent. `-n` numbers messages. The file
|
|
781
|
+
also works as is, one JSON object per line; `jq` just gives cleaner units.
|
|
782
|
+
|
|
783
|
+
### My records are separated by something other than NUL. Why is there no `--record-separator`?
|
|
784
|
+
|
|
785
|
+
Turn the separator into NUL and use `-z`. For records separated by a `----` line:
|
|
786
|
+
|
|
787
|
+
```sh
|
|
788
|
+
perl -0777 -pe 's/\n----\n/\0/g' notes.txt | sys1grep -z -e "a decision was made" | tr '\0' '\n'
|
|
789
|
+
awk '/^----$/ { printf "%c", 0; next } { print }' notes.txt | sys1grep -z -e "a decision was made" | tr '\0' '\n'
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
NUL is what `git log -z`, `find -print0` and `xargs -0` already emit, so `-z` covers them directly (#6).
|
|
793
|
+
A general separator would bring escaping, multi-byte and regex-or-literal questions for a case one line of
|
|
794
|
+
`perl` or `awk` handles. The `awk` form works with the BSD awk on macOS, which does not take a
|
|
795
|
+
multi-character `RS`.
|
|
796
|
+
|
|
797
|
+
## License
|
|
798
|
+
|
|
799
|
+
MIT
|