issuary 0.1.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 ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Lucas Merencia
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,519 @@
1
+ # issuary
2
+
3
+ CLI to monitor and AI-compact GitHub issues across multiple repositories.
4
+
5
+ `issuary` keeps a local, incremental mirror of the issues in the repos you watch,
6
+ tells you what changed since the last sync (new issues, closed issues, new
7
+ comments), and offers a compaction layer: structured summaries written and
8
+ consumed by AIs so an agent can understand a whole project's issues without
9
+ re-fetching from GitHub or blowing its context window.
10
+
11
+ The name is "issuary" (issue + -ary): an archive of a project's issues, distilled
12
+ into something an agent can read at a glance.
13
+
14
+ ## Core idea
15
+
16
+ - **Local incremental mirror.** `issuary` mirrors issues from many repos into a
17
+ local SQLite database and only fetches what changed since the last sync.
18
+ - **Change detection.** Each sync records events (opened, closed, reopened, new
19
+ comments) so you can see what moved across every watched repo at a glance.
20
+ - **AI compaction layer.** `issuary` never calls an LLM. It stores raw issue
21
+ content, exposes which issues need a summary, and accepts the summary back. The
22
+ agent that consumes the tool is the one that writes the summaries. A compact
23
+ saves context tokens for that agent, not disk space: the raw is never deleted.
24
+
25
+ The core (mirror, change detection, digests) works on its own. Compaction is an
26
+ optional layer on top.
27
+
28
+ ## Install
29
+
30
+ ```sh
31
+ npm install -g issuary
32
+ ```
33
+
34
+ Requirements:
35
+
36
+ - **Node.js >= 20.**
37
+ - **A GitHub token.** Either export `GITHUB_TOKEN` (a personal access token with
38
+ read access to the repos you watch, the `repo` scope or `public_repo` for
39
+ public repos only) or run `issuary login` to authenticate via the browser. See
40
+ [Authentication](#authentication). Commands that hit the GitHub API (`add`,
41
+ `sync`, and `show --raw`) require a token; purely local commands do not.
42
+
43
+ ### Environment variables
44
+
45
+ | Variable | Purpose | Default |
46
+ |---|---|---|
47
+ | `GITHUB_TOKEN` | GitHub personal access token used to reach the API. Takes precedence over a token stored by `issuary login`. | (required for API commands unless `issuary login` was run) |
48
+ | `GITHUB_API_URL` | REST API base URL. Set this for GitHub Enterprise, e.g. `https://github.example.com/api/v3`. Trailing slashes are trimmed. | `https://api.github.com` |
49
+ | `ISSUARY_HOME` | Directory holding local state (the SQLite database and `issuary login` credentials). | `~/.issuary` |
50
+ | `ISSUARY_GITHUB_CLIENT_ID` | OAuth App client id used by `issuary login` (device flow). Overrides the baked-in default. | (build default) |
51
+ | `ISSUARY_GITHUB_SCOPE` | OAuth scope requested by `issuary login`. | `repo` |
52
+
53
+ The database lives at `$ISSUARY_HOME/db.sqlite` (so `~/.issuary/db.sqlite` by default).
54
+
55
+ ## Authentication
56
+
57
+ Commands that hit the GitHub API (`add`, `sync`, `show --raw`) need a token.
58
+ There are two ways to provide one:
59
+
60
+ 1. **Export a token.** Set `GITHUB_TOKEN` to a GitHub personal access token with
61
+ read access to the repos you watch (the `repo` scope, or `public_repo` for
62
+ public repos only):
63
+
64
+ ```sh
65
+ export GITHUB_TOKEN=ghp_...
66
+ ```
67
+
68
+ 2. **`issuary login` (device flow).** Authenticate in the browser, no manual token
69
+ handling:
70
+
71
+ ```sh
72
+ issuary login
73
+ ```
74
+
75
+ It prints a short code and a URL. Open the URL, enter the code, and approve.
76
+ `issuary` then stores the resulting token and confirms with `Logged in as <you>.`
77
+ The default scope requested is `repo` so private repos work; override it with
78
+ `ISSUARY_GITHUB_SCOPE` if you only need public access. `issuary login --json` emits
79
+ `{ "ok": true, "login": "<you>", "scopes": [...] }`. The token itself is never
80
+ printed.
81
+
82
+ **Precedence.** When both are present, the `GITHUB_TOKEN` environment variable
83
+ wins over the stored token. So an explicitly exported token always takes effect,
84
+ and `issuary login` is the fallback when no env token is set.
85
+
86
+ **Where the token is stored.** `issuary login` writes the token to
87
+ `~/.issuary/credentials.json` (under `$ISSUARY_HOME`), created with file mode `0600`
88
+ (owner read/write only). The token is never logged.
89
+
90
+ **Log out.** `issuary logout` removes the stored token locally:
91
+
92
+ ```sh
93
+ issuary logout
94
+ ```
95
+
96
+ This only deletes the local credentials file; it does not revoke the token on
97
+ GitHub. `issuary logout --json` emits `{ "ok": true, "removed": boolean }`.
98
+
99
+ ### Maintainer setup (device login)
100
+
101
+ `issuary login` uses the GitHub OAuth **device flow**, which requires a registered
102
+ GitHub OAuth App with "Device Flow" enabled. The app's **public** client id must
103
+ be available to the CLI: either baked into `DEFAULT_GITHUB_CLIENT_ID` in
104
+ `src/auth/client-id.ts` (a device-flow client id is not a secret, so it is safe
105
+ to commit) or supplied at runtime via the `ISSUARY_GITHUB_CLIENT_ID` environment
106
+ variable. Until a client id is configured, `issuary login` exits with a clear error;
107
+ the `GITHUB_TOKEN` path keeps working regardless.
108
+
109
+ ## Quickstart
110
+
111
+ ```sh
112
+ # 1. Watch a couple of repos (each is validated against the API).
113
+ issuary add octocat/hello-world
114
+ issuary add facebook/react
115
+
116
+ # 2. Mirror their issues locally (incremental: only what changed is fetched).
117
+ issuary sync
118
+
119
+ # 3. See what changed everywhere, as an aggregated inbox.
120
+ issuary digest
121
+
122
+ # 4. List what is open right now, across all repos (read-only, no API calls).
123
+ issuary issues
124
+
125
+ # 5. Get the full project-wide view of one repo's issues.
126
+ issuary repo-digest facebook/react
127
+
128
+ # 6. Read a single issue (compact if present, otherwise raw body).
129
+ issuary show facebook/react#123
130
+
131
+ # Read the same issue's full raw body and comments.
132
+ issuary show facebook/react#123 --raw
133
+ ```
134
+
135
+ Every command also supports `--json` for machine and AI consumption.
136
+
137
+ ## Command reference
138
+
139
+ All commands accept `--json`, which prints a single JSON document to stdout and
140
+ suppresses the human formatting. Expected, user-facing errors (a malformed
141
+ argument, an unwatched repo, a missing issue) print a message to stderr and exit
142
+ with a non-zero status.
143
+
144
+ Four commands answer four different questions, so it helps to keep them apart:
145
+
146
+ - `issuary list` lists the **repos** you watch.
147
+ - `issuary issues` is the **filterable issue list**: "what issues match these
148
+ filters right now?" (state, repo, label, author, since, search, compaction).
149
+ - `issuary digest` is the **inbox**: "what changed since I last looked?"
150
+ - `issuary repo-digest` is **one project's full memory**: every issue of a single
151
+ repo, compacted where possible.
152
+
153
+ ### `issuary add <owner/repo>`
154
+
155
+ Start watching a repo. Validates that the repo exists and is accessible via the
156
+ GitHub API before recording it. Re-adding a previously removed repo reactivates
157
+ it. Requires `GITHUB_TOKEN`.
158
+
159
+ - Argument: `<owner/repo>`, e.g. `octocat/hello-world`.
160
+ - `--json` emits `{ "ok": true, "repo": "<owner/repo>", "status": "added" | "already-watched" | "reactivated" }`.
161
+
162
+ ### `issuary remove <owner/repo>`
163
+
164
+ Stop watching a repo. This deactivates it; it never deletes, so the repo's
165
+ issues and compacts are kept. Local only, no token required.
166
+
167
+ - Argument: `<owner/repo>`.
168
+ - `--json` emits `{ "ok": true, "repo": "<owner/repo>", "status": "removed" | "already-inactive" }`.
169
+
170
+ ### `issuary list`
171
+
172
+ List watched repos with their state and last sync time. Active repos first, then
173
+ inactive. Local only.
174
+
175
+ - `--json` emits an array of `{ "repo": "<owner/repo>", "active": boolean, "lastSyncedAt": string | null }`.
176
+
177
+ ### `issuary sync [repo]`
178
+
179
+ Fetch issue updates for watched repos and record what changed. With no argument
180
+ it syncs every active repo; with a `[repo]` argument it limits the sync to that
181
+ single watched repo. The fetch is incremental (see [How it works](#how-it-works)).
182
+ Requires `GITHUB_TOKEN`.
183
+
184
+ - Argument (optional): `[repo]` as `owner/repo`.
185
+ - `--quiet`: print nothing when there was no activity across all repos (no
186
+ events and no errors), so a scheduled/cron run stays silent on a no-op cycle.
187
+ A concise summary is still printed when something changed, and failed repos
188
+ are always printed. Has no effect on `--json`. See [Scheduling](#scheduling).
189
+ - `--json` emits `{ "repos": [ { "repo", "notModified", "opened", "closed", "reopened", "commented", "processed" } ] }`,
190
+ one entry per synced repo. `notModified` is `true` when the repo returned a 304
191
+ (nothing changed); the counts are then all zero.
192
+
193
+ The command exits `0` on success (even when nothing changed) and non-zero when
194
+ any repo failed to sync, so a scheduler or monitor can detect failures.
195
+
196
+ ### `issuary digest`
197
+
198
+ Show an aggregated inbox of issue changes across all watched repos, grouped by
199
+ repo and then by change type (new issues, closed, new comments, closed with new
200
+ comment, reopened).
201
+
202
+ Three modes:
203
+
204
+ - **Default (inbox):** shows unseen events, then marks them seen so each change
205
+ appears only once.
206
+ - `--since <when>`: a read-only time window showing events at or after `<when>`.
207
+ Accepts an ISO-8601 date or a simple relative duration: `<n>d` (days) or
208
+ `<n>h` (hours), e.g. `7d` or `24h`. Does not mark anything seen.
209
+ - `--all`: every event, seen and unseen. Does not mark anything seen.
210
+
211
+ Options:
212
+
213
+ - `--since <when>`: ISO date or `Nd` / `Nh` duration.
214
+ - `--all`: show all events without marking any seen.
215
+ - `--repo <owner/repo>`: narrow any mode to a single watched repo.
216
+ - `--json` emits `{ "mode": "inbox" | "since" | "all", "total": number, "repos": [ { "repo", "groups": [ { "type", "events": [...] } ] } ] }`.
217
+
218
+ Local only, no token required.
219
+
220
+ ### `issuary issues`
221
+
222
+ List issues across watched repos, with filters. Read-only: it never calls the
223
+ GitHub API and never changes local state (it does not mark anything seen). With
224
+ no flags it shows OPEN issues across all watched repos, sorted by most recently
225
+ updated, grouped by repo, with a count header. Local only, no token required.
226
+
227
+ Options:
228
+
229
+ - `--state <open|closed|all>`: which issues to include (default `open`).
230
+ - `--repo <owner/repo>`: scope to a watched repo. Repeatable to pass several.
231
+ - `--label <name>`: match issues carrying any of these labels. Repeatable; the
232
+ labels are OR-ed (an issue matches if it has at least one).
233
+ - `--author <login>`: restrict to issues opened by this user.
234
+ - `--state-reason <completed|not_planned>`: restrict by GitHub's close reason.
235
+ - `--since <when>`: only issues with `updated_at >=` an ISO date or a relative
236
+ duration (`Nd` / `Nh`, e.g. `7d`, `24h`).
237
+ - `--search <text>`: case-insensitive substring match on the issue title.
238
+ - `--uncompacted` | `--stale` | `--compacted`: filter by compaction state.
239
+ Mutually exclusive (passing more than one is an error).
240
+ - `--sort <updated|created|number>` (default `updated`) and
241
+ `--order <asc|desc>` (default `desc`).
242
+ - `--limit <n>`: cap the number of issues returned.
243
+ - `--json` (see shape below).
244
+
245
+ Examples:
246
+
247
+ ```sh
248
+ # What is open right now, everywhere.
249
+ issuary issues
250
+
251
+ # Everything, including closed.
252
+ issuary issues --state all
253
+
254
+ # One project, only bugs.
255
+ issuary issues --repo facebook/react --label bug
256
+
257
+ # Issues touched in the last week.
258
+ issuary issues --since 7d
259
+
260
+ # Issues whose memory still needs writing.
261
+ issuary issues --uncompacted
262
+
263
+ # Find by title, as JSON for an agent.
264
+ issuary issues --search "timezone" --json
265
+ ```
266
+
267
+ Sample human output:
268
+
269
+ ```
270
+ 3 open issues across 2 repos (filter: labels=bug)
271
+
272
+ facebook/react:
273
+ #321 [open] Hooks break with timezones {bug, timezone} (4c) (uncompacted)
274
+ #204 [open] Crash on hydrate {bug} (2c)
275
+
276
+ octocat/hello-world:
277
+ #12 [open] Typo in error message {bug}
278
+ ```
279
+
280
+ The `{...}` are labels, `(Nc)` is the comment count, and a trailing `(stale)` or
281
+ `(uncompacted)` marks issues whose compact is missing or out of date (nothing is
282
+ shown when the compact is fresh).
283
+
284
+ `--json` emits:
285
+
286
+ ```json
287
+ {
288
+ "filters": {
289
+ "state": "open", "repos": null, "labels": ["bug"], "author": null,
290
+ "stateReason": null, "since": null, "search": null, "compaction": null,
291
+ "sort": "updated", "order": "desc", "limit": null
292
+ },
293
+ "summary": { "total": 3, "open": 3, "closed": 0, "repos": 2 },
294
+ "issues": [
295
+ {
296
+ "repo": "facebook/react", "number": 321, "title": "Hooks break with timezones",
297
+ "state": "open", "stateReason": null, "author": "ann",
298
+ "labels": ["bug", "timezone"], "commentCount": 4,
299
+ "createdAt": "...", "updatedAt": "...",
300
+ "compact": null, "compactTldr": null, "compacted": false, "stale": false,
301
+ "refs": ["#204"]
302
+ }
303
+ ]
304
+ }
305
+ ```
306
+
307
+ The `compact` field carries the full canonical compact when one exists,
308
+ `compactTldr` its one-line headline, and `compacted` / `stale` say whether it is
309
+ fresh. Raw bodies and comments are intentionally not included here; use
310
+ `issuary show <repo>#<n> --raw` for those.
311
+
312
+ ### `issuary repo-digest <repo>`
313
+
314
+ Consume all issues of one watched repo as a project-wide, AI-optimized view. For
315
+ each issue it prefers a fresh compact and falls back to the raw body, flagging
316
+ which issues an AI may want to (re)compact. The header summarizes totals (open,
317
+ closed, compacted, stale or uncompacted). Local only.
318
+
319
+ - Argument: `<repo>` as `owner/repo`.
320
+ - `--headlines`: list every issue using only its cheap `tldr` headline (roughly
321
+ 20 tokens per issue), falling back to the issue title when there is no `tldr`.
322
+ - `--json` (full) emits `{ "repo", "summary": { "total", "open", "closed", "compacted", "staleOrUncompacted" }, "issues": [ { "number", "state", "stateReason", "title", "representation", "compacted", "stale", "refs" } ] }`.
323
+ - `--headlines --json` emits `{ "repo", "summary": {...}, "headlines": [ { "number", "state", "headline", "fromTldr" } ] }`.
324
+
325
+ ### `issuary show <target>`
326
+
327
+ Display a single issue from the local store. By default it shows the compact if a
328
+ fresh one exists, otherwise the raw body. Local only by default.
329
+
330
+ - Argument: `<target>` as `owner/repo#number`, e.g. `facebook/react#123`.
331
+ - `--raw`: include the full raw body and the comment thread. Comments are fetched
332
+ on demand the first time and then cached, so `--raw` requires `GITHUB_TOKEN`.
333
+ - `--json` emits the issue's fields: `{ "repo", "number", "title", "state", "stateReason", "author", "labels", "commentCount", "createdAt", "updatedAt", "closedAt", "compact", "compactStale", "rawBody", "refs" }`, plus `"comments"` when `--raw` is set.
334
+
335
+ ### `issuary compact list`
336
+
337
+ List issues with their compaction status (`compacted`, `stale`, or
338
+ `uncompacted`), grouped by repo. Local only.
339
+
340
+ - `--pending`: narrow to the actionable set, only issues that are uncompacted or
341
+ stale (the work an AI needs to do). Each pending item carries a `reason`.
342
+ - `--repo <owner/repo>`: restrict to a single watched repo.
343
+ - `--json` emits an array of `{ "repo", "number", "title", "state", "status", "reason", "rawBody", "commentsNeedFetch" }`.
344
+ `reason` is `"uncompacted"` or `"stale"` for pending issues and `null` for
345
+ fresh ones. `commentsNeedFetch` is `true` when the issue has comments that have
346
+ not been pulled yet, a hint to run `issuary show <repo>#<n> --raw` before
347
+ compacting.
348
+
349
+ ### `issuary compact set <target> --from-file <file>`
350
+
351
+ Persist a compact for an issue from a file in the canonical format. The file is
352
+ parsed and validated; an invalid compact is rejected. Saving a compact clears the
353
+ issue's stale flag. Local only.
354
+
355
+ - Argument: `<target>` as `owner/repo#number`.
356
+ - `--from-file <file>` (required): path to the compact file to read.
357
+ - `--json` emits `{ "ok": true, "repo", "number", "tldr" }`.
358
+
359
+ ### `issuary protocol`
360
+
361
+ Print the AI compaction protocol, the contract AI consumers follow. This is the
362
+ self-describing usage that an agent can read to discover how compaction works.
363
+
364
+ - `--json` emits `{ "protocol": string, "compactFormat": { "doc", "frontmatterFields", "bodyFields", "persistCommand" } }`.
365
+
366
+ ### `issuary skill`
367
+
368
+ Emit issuary's neutral agent skill, or install it for an AI agent. The content is
369
+ vendor-neutral: it teaches an agent what issuary is, when to reach for it, and where
370
+ to find the exact contract (`issuary protocol`, `issuary --help`).
371
+
372
+ - No flags: print the skill to stdout. This is the universal path: paste it into
373
+ any agent's system prompt or rules file.
374
+ - `--install --format claude` (the default format): write
375
+ `~/.claude/skills/issuary/SKILL.md` (override the skills root with `--dir` or
376
+ `CLAUDE_SKILLS_DIR`).
377
+ - `--install --format agents`: insert or replace a delimited, idempotent issuary
378
+ section in an `AGENTS.md` at the project root (override the directory with
379
+ `--dir`). Running it twice yields exactly one section; existing unrelated
380
+ content is preserved.
381
+ - `--json` emits `{ "name", "description", "path", "content", "format" }`.
382
+
383
+ ### `issuary login`
384
+
385
+ Authenticate with GitHub via the OAuth device flow and store the token at
386
+ `~/.issuary/credentials.json` (mode `0600`). Prints a user code and a verification
387
+ URL to open in the browser, polls until you authorize, then confirms with
388
+ `Logged in as <you>.` See [Authentication](#authentication).
389
+
390
+ - `--json` emits `{ "ok": true, "login": "<you>", "scopes": [...] }`.
391
+
392
+ ### `issuary logout`
393
+
394
+ Remove the locally stored token. Local only; it does not revoke the token on
395
+ GitHub.
396
+
397
+ - `--json` emits `{ "ok": true, "removed": boolean }`.
398
+
399
+ ## For AI agents
400
+
401
+ `issuary` does not call any LLM itself. It stores raw issue content, exposes which
402
+ issues need a summary, and accepts the summary back. The agent that consumes the
403
+ tool is the compaction CPU; `issuary` only stores and serves.
404
+
405
+ ### issuary vs GitHub's MCP
406
+
407
+ They are complementary, not competing. GitHub's MCP server gives live, raw access
408
+ to issues, use it when you need the current, unfiltered state of an issue or its
409
+ comment thread. `issuary` is not another raw-issue reader: its value is the
410
+ persistent, compacted memory of issues plus the cross-repo digest of what changed
411
+ since you last looked. Use GitHub's MCP for live, raw access, and `issuary` for the
412
+ distilled memory and the "what changed" digest.
413
+
414
+ ### Reading the memory with filters
415
+
416
+ `issuary issues --json` is the filtered entry point into the memory. Pass any of
417
+ the filters (`--state`, `--repo`, `--label`, `--author`, `--since`, `--search`,
418
+ `--uncompacted` / `--stale` / `--compacted`) and you get back the matching issues
419
+ with their `compact`, `compactTldr`, `refs`, and the `compacted` / `stale` flags,
420
+ without raw bodies. It complements the other two read paths:
421
+ `issuary repo-digest <repo> --json` for one project's full dump, and
422
+ `issuary show <repo>#<n> --json` for a single issue (add `--raw` for the body and
423
+ comments). Reach for `issues --json` when you want a slice of the memory ("open
424
+ bugs across all repos", "anything touched this week", "what still needs
425
+ compacting") rather than a whole project or a single issue.
426
+
427
+ ### Teaching an agent to use issuary
428
+
429
+ `issuary skill` emits a neutral skill document that explains all of this. Print it
430
+ (`issuary skill`) and paste it into any agent's system prompt or rules file, or
431
+ install it: `issuary skill --install --format claude` writes
432
+ `~/.claude/skills/issuary/SKILL.md` for Claude Code, and
433
+ `issuary skill --install --format agents` inserts an idempotent issuary section into a
434
+ project `AGENTS.md`. See the [`issuary skill`](#issuary-skill) command reference.
435
+
436
+ Each issue carries two fields that drive the workflow:
437
+
438
+ - `compact`: the AI-written structured summary, or `null` if none exists.
439
+ - `compact_stale`: `true` when the compact no longer reflects the issue (set by
440
+ `sync` when a new comment lands on an already-compacted issue).
441
+
442
+ The protocol:
443
+
444
+ 1. **If `compact != null` and `compact_stale == false`, use the compact.** Do not
445
+ read the raw, do not recompact. It is trusted and current.
446
+ 2. **If `compact == null` or `compact_stale == true`, recompact.** Read the raw,
447
+ write a fresh compact in the canonical format, and persist it.
448
+
449
+ A typical agent loop:
450
+
451
+ ```sh
452
+ # 1. Find the work: issues that are uncompacted or stale.
453
+ issuary compact list --pending --json
454
+
455
+ # 2. Read the raw body and comments for one of them
456
+ # (comments are fetched on demand).
457
+ issuary show owner/repo#123 --raw --json
458
+
459
+ # 3. Write a compact in the canonical format to a file, then persist it.
460
+ # Persisting clears the stale flag.
461
+ issuary compact set owner/repo#123 --from-file compact.md
462
+
463
+ # 4. Re-compact whenever an issue goes stale again after a future sync.
464
+ ```
465
+
466
+ Run `issuary protocol` to get the contract as text (or `issuary protocol --json` for
467
+ the structured form). The full, authoritative, field-by-field compact format,
468
+ with rules and worked examples, is in
469
+ [docs/compact-format.md](./docs/compact-format.md).
470
+
471
+ To automate this loop, see the optional auto-compaction worker in
472
+ [examples/auto-compact/](./examples/auto-compact/): a small companion script that
473
+ batches the pending set (`compact list --pending --limit`), calls an LLM, and
474
+ writes the compacts back. It lives outside the CLI on purpose: `issuary` itself
475
+ never calls an LLM, so the worker keeps that dependency in the example, not the
476
+ core.
477
+
478
+ ## How it works
479
+
480
+ - **Local SQLite mirror.** State lives in a single SQLite database at
481
+ `~/.issuary/db.sqlite` (override the directory with `ISSUARY_HOME`).
482
+ - **Incremental sync.** `sync` fetches issues with the GitHub `since` parameter
483
+ and an `ETag`. When nothing changed the API returns `304 Not Modified`, which
484
+ does not spend your rate limit and is reported as `unchanged`.
485
+ - **Comments on demand.** Comment threads are not pulled on every sync. They are
486
+ fetched the first time you need them (via `show --raw`) and then cached.
487
+ - **Raw is never deleted.** Compacting adds a summary layer on top of the raw
488
+ body and comments; it never removes them. You can always re-read the raw and
489
+ re-compact. The win from compaction is context tokens for the consuming agent,
490
+ not disk space.
491
+ - **Removal is deactivation.** `remove` deactivates a repo rather than deleting
492
+ it, so its issues and compacts are preserved as history.
493
+
494
+ ## Scheduling
495
+
496
+ `issuary` is not a daemon. To keep the mirror fresh, let your OS scheduler run
497
+ `issuary sync --quiet` on an interval. Quiet mode stays silent on a no-op cycle (no
498
+ events, no errors), prints a summary when something changed, always prints
499
+ failed repos, and exits non-zero when any repo failed so a monitor can react.
500
+
501
+ See [docs/scheduling.md](./docs/scheduling.md) for ready-to-use crontab and
502
+ macOS launchd recipes, plus notes on rate limits and how failures surface.
503
+
504
+ ## Development
505
+
506
+ ```sh
507
+ npm install
508
+ npm run check # lint + format:check + typecheck + test
509
+ npm run build # bundle to dist/cli.js
510
+ ```
511
+
512
+ `npm run check` is the quality gate: ESLint, Prettier (`format:check`),
513
+ `tsc --noEmit`, and the Vitest suite. CI (`.github/workflows/ci.yml`) runs the
514
+ same gate plus the build across the Node 20, 22, and 24 matrix; a PR is only
515
+ mergeable with CI green.
516
+
517
+ ## License
518
+
519
+ ISC, Lucas Merencia.