opencode-diane 0.0.5

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/LICENSE +21 -0
  3. package/README.md +206 -0
  4. package/WIKI.md +1430 -0
  5. package/dist/index.d.ts +28 -0
  6. package/dist/index.js +1632 -0
  7. package/dist/ingest/adaptive.d.ts +47 -0
  8. package/dist/ingest/adaptive.js +182 -0
  9. package/dist/ingest/code-health.d.ts +58 -0
  10. package/dist/ingest/code-health.js +202 -0
  11. package/dist/ingest/code-map.d.ts +71 -0
  12. package/dist/ingest/code-map.js +670 -0
  13. package/dist/ingest/cross-refs.d.ts +59 -0
  14. package/dist/ingest/cross-refs.js +1207 -0
  15. package/dist/ingest/docs.d.ts +49 -0
  16. package/dist/ingest/docs.js +325 -0
  17. package/dist/ingest/git.d.ts +77 -0
  18. package/dist/ingest/git.js +390 -0
  19. package/dist/ingest/live-session.d.ts +101 -0
  20. package/dist/ingest/live-session.js +173 -0
  21. package/dist/ingest/project-notes.d.ts +28 -0
  22. package/dist/ingest/project-notes.js +102 -0
  23. package/dist/ingest/project.d.ts +35 -0
  24. package/dist/ingest/project.js +430 -0
  25. package/dist/ingest/session-snapshot.d.ts +63 -0
  26. package/dist/ingest/session-snapshot.js +94 -0
  27. package/dist/ingest/sessions.d.ts +29 -0
  28. package/dist/ingest/sessions.js +164 -0
  29. package/dist/ingest/tables.d.ts +52 -0
  30. package/dist/ingest/tables.js +360 -0
  31. package/dist/mining/skill-miner.d.ts +53 -0
  32. package/dist/mining/skill-miner.js +234 -0
  33. package/dist/search/bm25.d.ts +81 -0
  34. package/dist/search/bm25.js +334 -0
  35. package/dist/search/e5-embedder.d.ts +30 -0
  36. package/dist/search/e5-embedder.js +91 -0
  37. package/dist/search/embed-pass.d.ts +26 -0
  38. package/dist/search/embed-pass.js +43 -0
  39. package/dist/search/embedder.d.ts +58 -0
  40. package/dist/search/embedder.js +85 -0
  41. package/dist/search/inverted-index.d.ts +51 -0
  42. package/dist/search/inverted-index.js +139 -0
  43. package/dist/search/ppr.d.ts +44 -0
  44. package/dist/search/ppr.js +118 -0
  45. package/dist/search/tokenize.d.ts +26 -0
  46. package/dist/search/tokenize.js +98 -0
  47. package/dist/store/eviction.d.ts +16 -0
  48. package/dist/store/eviction.js +37 -0
  49. package/dist/store/repository.d.ts +222 -0
  50. package/dist/store/repository.js +420 -0
  51. package/dist/store/sqlite-store.d.ts +89 -0
  52. package/dist/store/sqlite-store.js +252 -0
  53. package/dist/store/vector-store.d.ts +66 -0
  54. package/dist/store/vector-store.js +160 -0
  55. package/dist/types.d.ts +385 -0
  56. package/dist/types.js +9 -0
  57. package/dist/utils/file-log.d.ts +87 -0
  58. package/dist/utils/file-log.js +215 -0
  59. package/dist/utils/peer-detection.d.ts +45 -0
  60. package/dist/utils/peer-detection.js +90 -0
  61. package/dist/utils/shell.d.ts +43 -0
  62. package/dist/utils/shell.js +110 -0
  63. package/dist/utils/usage-skill.d.ts +42 -0
  64. package/dist/utils/usage-skill.js +129 -0
  65. package/dist/utils/xlsx.d.ts +36 -0
  66. package/dist/utils/xlsx.js +270 -0
  67. package/grammars/tree-sitter-c.wasm +0 -0
  68. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  69. package/grammars/tree-sitter-cpp.wasm +0 -0
  70. package/grammars/tree-sitter-css.wasm +0 -0
  71. package/grammars/tree-sitter-go.wasm +0 -0
  72. package/grammars/tree-sitter-html.wasm +0 -0
  73. package/grammars/tree-sitter-java.wasm +0 -0
  74. package/grammars/tree-sitter-javascript.wasm +0 -0
  75. package/grammars/tree-sitter-json.wasm +0 -0
  76. package/grammars/tree-sitter-php.wasm +0 -0
  77. package/grammars/tree-sitter-python.wasm +0 -0
  78. package/grammars/tree-sitter-rust.wasm +0 -0
  79. package/grammars/tree-sitter-typescript.wasm +0 -0
  80. package/package.json +80 -0
@@ -0,0 +1,385 @@
1
+ /**
2
+ * opencode-diane — type definitions.
3
+ *
4
+ * The store is hierarchical: top-level `category` partitions the
5
+ * memory set, each entry has a `subject` (mid-level slug for
6
+ * coarse filtering) and a free-form `content` body that's the
7
+ * leaf-level text searched by BM25.
8
+ */
9
+ export type Category = "git-history" | "project-facts" | "code-health" | "code-map" | "session-trace" | "session-snapshot" | "agent-note" | "skill-mined" | "custom";
10
+ /** A single memory entry — a leaf in the hierarchy. */
11
+ export interface Memory {
12
+ /** Stable id, e.g. `mem_000123`. */
13
+ id: string;
14
+ category: Category;
15
+ /** Human-readable slug grouping related entries (file path, task name, etc.). */
16
+ subject: string;
17
+ /** The actual text searched by BM25 and shown to the agent. */
18
+ content: string;
19
+ /** Optional tags for filtering (e.g. "bugfix", "framework:django"). */
20
+ tags: string[];
21
+ /** Where this memory came from (e.g. `git:abc123`, `session:sess_xyz`, `agent`). */
22
+ source: string;
23
+ /** Epoch ms. */
24
+ createdAt: number;
25
+ /** Last time the entry was returned by a recall query. */
26
+ usedAt: number;
27
+ /** Number of recalls that returned this entry. */
28
+ useCount: number;
29
+ /** Approx byte size of content + subject + tags JSON — used for budget. */
30
+ sizeBytes: number;
31
+ /** If true, never evicted regardless of budget. */
32
+ pinned?: boolean;
33
+ }
34
+ /** On-disk JSON representation. */
35
+ export interface MemoryStoreFile {
36
+ version: 1;
37
+ memories: Memory[];
38
+ meta: {
39
+ ingestedAt: Record<string, number>;
40
+ lastEvictionAt: number | null;
41
+ schema: 1;
42
+ };
43
+ }
44
+ /** Result of a recall query. */
45
+ export interface RecallHit {
46
+ memory: Memory;
47
+ score: number;
48
+ }
49
+ /** Plugin-level configuration (everything optional with defaults). */
50
+ export interface UserConfig {
51
+ /** Max disk usage for memory store, in megabytes. Default 50. */
52
+ maxMemoryDiskMB?: number;
53
+ /** Run ingest on plugin startup. Default true. */
54
+ autoIngestOnStartup?: boolean;
55
+ /** Cap on commits walked by the git ingester. Default 500. */
56
+ gitHistoryDepth?: number;
57
+ /** If true, run even when the directory isn't a detected workable repo. Default false. */
58
+ forceActive?: boolean;
59
+ /** Where to write SKILL.md files (relative to project root). Default ".opencode/skills". */
60
+ skillsOutputDir?: string;
61
+ /** Min cluster size to mine a skill. Default 3. */
62
+ skillMiningMinCluster?: number;
63
+ /** Whether to ingest other OpenCode sessions for this project. Default true. */
64
+ ingestSessions?: boolean;
65
+ /**
66
+ * Build a tree-sitter "code map" of file signatures (Aider-style).
67
+ * Default `true` since v0.0.4. Web-tree-sitter plus vendored grammar
68
+ * wasm (~10.3 MB across thirteen languages) ships with the package
69
+ * regardless; this flag controls whether the ingester RUNS at
70
+ * startup. Set `false` to disable if you don't want structural
71
+ * signatures in the store.
72
+ */
73
+ enableCodeMap?: boolean;
74
+ /**
75
+ * On first startup, write `<skillsOutputDir>/<prefix>using-memory/SKILL.md`
76
+ * so OpenCode surfaces a "call memory_recall first" instruction to
77
+ * the agent at session start. The plugin's nudge hook and tool
78
+ * descriptions already point the same way; this is the one-time
79
+ * upfront push.
80
+ *
81
+ * Defaults to `true` since v0.0.4. Set `false` to never install it.
82
+ * The file is written only when it does not already exist, so a
83
+ * user can edit it and the edits survive every subsequent startup
84
+ * — and a user can delete it (with this option still true) to opt
85
+ * out for the lifetime of that checkout.
86
+ */
87
+ installUsageSkill?: boolean;
88
+ /**
89
+ * Walk `<root>/docs/` (and conventional root-level docs like
90
+ * CHANGELOG.md, CONTRIBUTING.md, ARCHITECTURE.md, …) and index
91
+ * each H1/H2/H3 heading as a recallable section pointer
92
+ * (`<path>:<line> # <heading>` + first paragraph). Default `true`.
93
+ */
94
+ ingestDocs?: boolean;
95
+ /**
96
+ * Index root-level agent-instruction files (AGENTS.md, CLAUDE.md,
97
+ * GEMINI.md, COPILOT.md, CONVENTIONS.md, .cursorrules,
98
+ * .windsurfrules, .clinerules) as project facts so the agent sees
99
+ * the repo's house rules within the first recall. Default `true`.
100
+ */
101
+ ingestProjectNotes?: boolean;
102
+ /**
103
+ * Walk for `.csv`, `.tsv`, `.xlsx`, `.xls`, `.xlsm` files and index
104
+ * their column headers (first row only — never row data). XLSX/XLS
105
+ * files are handled via SheetJS, lazy-loaded only when a spreadsheet
106
+ * is actually encountered (projects with no spreadsheets pay no
107
+ * module-load cost). Default `true`.
108
+ */
109
+ ingestTableHeaders?: boolean;
110
+ /**
111
+ * Run the grammar-agnostic cross-reference ingester: detects file-to-
112
+ * file connections in any language (Pascal, Ruby, Perl, Lua, …) and
113
+ * config DSLs (JSON / YAML / TOML) where one file references another
114
+ * by path. Multi-signal corroborated; filesystem-grounded signals
115
+ * (path resolves to existing file) emit edges alone, lexical signals
116
+ * (identifier mention) require an orthogonal corroboration before
117
+ * emitting. Default `true`.
118
+ */
119
+ ingestCrossRefs?: boolean;
120
+ /**
121
+ * Rarity threshold for the grammar-agnostic cross-reference ingester.
122
+ * An identifier that is defined in *more than this many* files is
123
+ * treated as too generic to use as a corroboration signal. Reduce to
124
+ * tighten confidence (fewer, higher-quality edges); raise for large
125
+ * monorepos where the same class name spans many packages. Default 3.
126
+ */
127
+ crossRefsRarityThreshold?: number;
128
+ /**
129
+ * Maximum number of files the cross-reference ingester walks in one
130
+ * prefill pass. Raise for monorepos with hundreds of thousands of
131
+ * files; lower for speed. Default `2000`.
132
+ */
133
+ crossRefsMaxFiles?: number;
134
+ /**
135
+ * Hard cap on total cross-reference edges emitted per prefill pass.
136
+ * Lower values keep the memory store leaner; higher values give more
137
+ * complete coverage on dense codebases. Default `10000`.
138
+ */
139
+ crossRefsMaxEdges?: number;
140
+ /**
141
+ * Maximum number of files the docs ingester walks in one pass
142
+ * (`docs/` tree + conventional root docs). Raise for documentation-
143
+ * heavy repos. Default `200`.
144
+ */
145
+ docsMaxFiles?: number;
146
+ /**
147
+ * Number of characters of body text captured after each heading as
148
+ * context in the recall snippet. A longer value gives richer
149
+ * snippets at the cost of larger memory entries. Default `240`.
150
+ */
151
+ docsBodyChars?: number;
152
+ /**
153
+ * Deepest heading level to index. `3` indexes H1–H3; `2` indexes
154
+ * only H1–H2; `4` or `5` captures deeper structure. Default `3`.
155
+ */
156
+ docsMaxHeadingLevel?: number;
157
+ /**
158
+ * Maximum number of table files (CSV, TSV, XLSX) walked per prefill
159
+ * pass. Default `200`.
160
+ */
161
+ tablesMaxFiles?: number;
162
+ /**
163
+ * Skip XLSX/XLS files larger than this, in megabytes. Very large
164
+ * spreadsheets are usually data dumps whose headers are rarely worth
165
+ * indexing. Set `0` to skip ALL spreadsheets. Default `50`.
166
+ */
167
+ tablesMaxXlsxMB?: number;
168
+ /**
169
+ * Maximum number of column headers to list per table / sheet in the
170
+ * memory content. Wide tables beyond this threshold get a
171
+ * "(N more)" note. Default `40`.
172
+ */
173
+ tablesMaxColumns?: number;
174
+ /**
175
+ * Maximum bytes of content read from each agent-instruction file
176
+ * (AGENTS.md, CLAUDE.md, .cursorrules, …). Teams with detailed
177
+ * instructions should raise this. Default `6144` (6 KB).
178
+ */
179
+ notesMaxBytes?: number;
180
+ /**
181
+ * Minimum number of times two files must appear in the same commit
182
+ * before a co-change edge is recorded. Lower values add more
183
+ * connections on new or small repos; higher values keep the graph
184
+ * tight. Default `3`.
185
+ */
186
+ coChangeMinOccurrences?: number;
187
+ /**
188
+ * Maximum number of source files the code-map ingester parses per
189
+ * pass. By default this is set adaptively (1500 small / 4000 medium
190
+ * / 10000 large), based on a one-shot measurement of the repo at
191
+ * startup. Setting this explicitly *overrides the adaptive
192
+ * decision* — useful when you know your repo's right size and want
193
+ * deterministic behaviour. Set to a small value to speed up
194
+ * startup at the cost of code-map completeness; raise for very
195
+ * large monorepos. Default `4000` (medium tier).
196
+ */
197
+ codeMapMaxFiles?: number;
198
+ /**
199
+ * Maximum number of git commits the co-change graph builder
200
+ * scans per pass. By default adaptive (1000 small / 5000 medium /
201
+ * 20000 large). Setting this explicitly overrides the adaptive
202
+ * choice. Lower for faster startup at the cost of co-change graph
203
+ * density; raise for repos where you want deeper history coverage.
204
+ * Default `5000` (medium tier).
205
+ */
206
+ coChangeMaxCommits?: number;
207
+ /**
208
+ * The recall-first nudge: a tool.execute.before/after hook pair that
209
+ * appends ONE reminder to a discovery tool's output if the agent
210
+ * does raw discovery without checking memory. Default TRUE.
211
+ *
212
+ * Set FALSE if another plugin (e.g. oh-my-opencode, which registers
213
+ * many lifecycle hooks) also post-processes tool output and you'd
214
+ * rather avoid two plugins touching `output.output`. The directive
215
+ * tool descriptions still encourage recall-first; only the
216
+ * output-mutating hook is disabled.
217
+ */
218
+ enableNudgeHook?: boolean;
219
+ /**
220
+ * Adapt size-derived settings to the repository. Default TRUE.
221
+ *
222
+ * When on, prefill measures one cheap signal — commit count (or
223
+ * file count when there's no git) — classifies the repo as small /
224
+ * medium / large, and scales `gitHistoryDepth`, the code-map file
225
+ * cap, and the co-change cutoff accordingly. (The disk budget is
226
+ * deliberately not tier-scaled — the 50 MB default is generous
227
+ * enough for every realistic repo.) The chosen tier is logged
228
+ * every run.
229
+ *
230
+ * Adaptation only fills knobs the user did NOT set explicitly — any
231
+ * value you pass in config always wins. Set FALSE to pin every
232
+ * setting to its fixed default regardless of repo size.
233
+ */
234
+ adaptive?: boolean;
235
+ /**
236
+ * Opt-in cross-lingual semantic search. Default FALSE.
237
+ *
238
+ * When on, the plugin also embeds each memory with a small
239
+ * multilingual e5 model (via the optional `@huggingface/transformers`
240
+ * dependency, downloaded on demand) and fuses vector similarity with
241
+ * the BM25 lexical ranking. This lets a query in one language
242
+ * (e.g. Chinese, Russian) retrieve code and comments written in
243
+ * another (e.g. English) — something pure lexical search cannot do.
244
+ *
245
+ * It is fully additive: when off, no model is downloaded, no runtime
246
+ * is loaded, and retrieval is byte-for-byte the lexical path. When
247
+ * on but the optional dependency is absent, the plugin logs that and
248
+ * falls back to lexical search rather than failing.
249
+ */
250
+ enableSemanticSearch?: boolean;
251
+ /**
252
+ * The embedding model id (a transformers.js-compatible model).
253
+ * Default "Xenova/multilingual-e5-small". Only consulted when
254
+ * `enableSemanticSearch` is true.
255
+ */
256
+ embeddingModel?: string;
257
+ /**
258
+ * Use Personalized PageRank for the co-change boost in recall,
259
+ * instead of the default single-hop propagation. Default FALSE.
260
+ *
261
+ * The co-change graph (files historically changed together) feeds a
262
+ * boost that surfaces structurally-related context a query alone
263
+ * would miss. By default that boost is one hop — the direct
264
+ * neighbours of the textual hits. With this on, it is instead a
265
+ * restart-biased random walk over the whole graph: relevance
266
+ * reaches multi-hop files, graded by graph distance.
267
+ *
268
+ * The tradeoff: PPR is a per-recall iterative computation (a few ms
269
+ * on a large graph) and is less trivially inspectable than the one
270
+ * hop. It is opt-in for that reason; the default stays cheap and
271
+ * fully traceable. See WIKI: "How it compares".
272
+ */
273
+ personalizedPageRank?: boolean;
274
+ /**
275
+ * Record the current session's activity (file edits + bash commands)
276
+ * as a single rolling memory under `session-trace` →
277
+ * `live:${sessionId}`. Lets the current session recall what it has
278
+ * already touched without scanning the OpenCode SDK, and pre-seeds
279
+ * the trace for the moment this session becomes "past" to a
280
+ * successor session. Default TRUE.
281
+ *
282
+ * Set FALSE to opt out of recording — the JSONL log still captures
283
+ * the same events for offline inspection. Disabling this is mostly
284
+ * useful in tests or when running against very locked-down stores
285
+ * where any extra write is unwelcome.
286
+ */
287
+ recordSessionActivity?: boolean;
288
+ /**
289
+ * Maximum number of files re-indexed after a single `bash` tool
290
+ * call. After a `bash` call the plugin runs `git status` to find
291
+ * what changed in the working tree and refreshes the code-map for
292
+ * each touched file. A bash command that triggers a branch switch
293
+ * or large reformat can change thousands of files at once; this cap
294
+ * keeps post-hook latency bounded. Default `20` — enough for typical
295
+ * commit/format/codegen workflows, low enough that even a mass
296
+ * checkout doesn't stall the next tool call.
297
+ */
298
+ bashFileTrackingMaxFiles?: number;
299
+ /**
300
+ * Auto-detect git ref movement (HEAD changing as a side effect of a
301
+ * bash command like `git pull`, `git merge`, `git rebase`,
302
+ * `git checkout`, or `git reset`) and re-run the git-history ingester
303
+ * in the background when it happens. Re-ingestion is idempotent —
304
+ * already-known commits are skipped via `insertIfMissing` — so the
305
+ * cost is roughly linear in the number of NEW commits.
306
+ *
307
+ * Without this, new commits arriving mid-session are invisible to
308
+ * `memory_recall` until the next session start. With it on, a HEAD
309
+ * change is detected after every `bash` call and a re-ingest is
310
+ * queued (de-duplicated: only one re-ingest runs at a time; further
311
+ * detections coalesce into the next pass). Default TRUE.
312
+ *
313
+ * The `memory_ingest_git` tool exposes the same behaviour as an
314
+ * explicit, on-demand call for cases where HEAD doesn't move
315
+ * (e.g. fetch without merge, or running outside a bash tool).
316
+ */
317
+ autoReingestGitOnHeadChange?: boolean;
318
+ }
319
+ export interface ResolvedConfig {
320
+ maxMemoryBytes: number;
321
+ autoIngestOnStartup: boolean;
322
+ gitHistoryDepth: number;
323
+ forceActive: boolean;
324
+ skillsOutputDir: string;
325
+ /**
326
+ * Prefix applied to mined skill subdirectory names AND to the
327
+ * subjects of the memories that point at them. Default `""` — the
328
+ * standalone behaviour, paths unchanged. Set to `"diane-"` at
329
+ * startup when a coexisting plugin (caveman, oh-my-opencode) writes
330
+ * into the same `.opencode/skills/` directory; this namespaces our
331
+ * subdirs so they don't collide with the peer's slugs (`caveman`,
332
+ * `caveman-commit`, …) and ensures `memory_skill` surfaces only
333
+ * ours, not the peer's.
334
+ */
335
+ minedSkillPrefix: string;
336
+ skillMiningMinCluster: number;
337
+ ingestSessions: boolean;
338
+ enableCodeMap: boolean;
339
+ installUsageSkill: boolean;
340
+ ingestDocs: boolean;
341
+ ingestProjectNotes: boolean;
342
+ ingestTableHeaders: boolean;
343
+ ingestCrossRefs: boolean;
344
+ /** See `UserConfig.crossRefsRarityThreshold`. */
345
+ crossRefsRarityThreshold: number;
346
+ crossRefsMaxFiles: number;
347
+ crossRefsMaxEdges: number;
348
+ docsMaxFiles: number;
349
+ docsBodyChars: number;
350
+ docsMaxHeadingLevel: number;
351
+ tablesMaxFiles: number;
352
+ tablesMaxXlsxMB: number;
353
+ tablesMaxColumns: number;
354
+ notesMaxBytes: number;
355
+ coChangeMinOccurrences: number;
356
+ enableNudgeHook: boolean;
357
+ adaptive: boolean;
358
+ enableSemanticSearch: boolean;
359
+ embeddingModel: string;
360
+ personalizedPageRank: boolean;
361
+ recordSessionActivity: boolean;
362
+ bashFileTrackingMaxFiles: number;
363
+ autoReingestGitOnHeadChange: boolean;
364
+ /**
365
+ * Names of the keys the user set explicitly in opencode.json.
366
+ * Adaptive tuning consults this so it never overrides a value the
367
+ * user chose deliberately — it only fills in size-derived defaults.
368
+ */
369
+ explicitKeys: ReadonlySet<keyof UserConfig>;
370
+ /**
371
+ * Size adaptation knobs not exposed as user config — set by
372
+ * `applyAdaptiveTuning` from the measured repo signal, read by the
373
+ * ingesters. When `adaptive` is false these stay at fixed defaults.
374
+ */
375
+ codeMapMaxFiles: number;
376
+ /** Skip the O(commits × files²) co-change pass above this commit count. */
377
+ coChangeMaxCommits: number;
378
+ }
379
+ /** Returned by tools that perform background operations. */
380
+ export interface BackgroundJobHandle {
381
+ job: string;
382
+ startedAt: number;
383
+ /** Caller is told the job is running async; this is for the agent's reference. */
384
+ note: string;
385
+ }
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * opencode-diane — type definitions.
3
+ *
4
+ * The store is hierarchical: top-level `category` partitions the
5
+ * memory set, each entry has a `subject` (mid-level slug for
6
+ * coarse filtering) and a free-form `content` body that's the
7
+ * leaf-level text searched by BM25.
8
+ */
9
+ export {};
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Rich, structured logging to disk. A second log sink alongside
3
+ * OpenCode's own session log channel — the existing `log()` callback
4
+ * keeps piping human-readable lines into OpenCode's UI, and this
5
+ * module mirrors them (plus structured events) to a JSONL file under
6
+ * `os.tmpdir()/opencode-diane/` by default, or `$OPENCODE_DIANE_LOG_DIR`
7
+ * if set (use the env var when running inside Docker — point it at a
8
+ * mounted volume and your logs survive the container). The file is
9
+ * per-session and per-PID, so parallel OpenCode sessions never
10
+ * interleave; each line is a standalone JSON object so the whole file
11
+ * is greppable AND `jq`-able.
12
+ *
13
+ * Why not into OpenCode's log alone:
14
+ * - OpenCode's log is human-oriented (single-line strings) and is
15
+ * scoped to a session's UI panel — easy to lose between turns.
16
+ * - For debugging the plugin itself (a flush that took 200ms, an
17
+ * ingester that skipped half its commits, an eviction that fired
18
+ * under budget) you want timestamped, machine-readable, persistent
19
+ * records you can keep across sessions and diff.
20
+ *
21
+ * Why JSONL not a text log:
22
+ * - One line per record, never multi-line, so `tail -f` works.
23
+ * - Each line is valid JSON, so `jq '.event == "ingest.git"'` works.
24
+ * - Streams append cleanly even from multiple writers (each line is
25
+ * atomic on POSIX up to PIPE_BUF, and our records are well under).
26
+ *
27
+ * Failure model: every disk operation can fail (full disk, permission
28
+ * lost, tmpdir on a flaky volume). A failure HERE must never propagate
29
+ * to the host plugin — the file logger is a debugging aid, not a
30
+ * correctness dependency. We try once, drop the stream on any error,
31
+ * and go silent. The OpenCode log channel is unaffected.
32
+ *
33
+ * Retention is the user's problem: we never delete; files accumulate
34
+ * in `os.tmpdir()/opencode-diane/` until the OS clears tmp. On Linux
35
+ * that's typically at reboot or via systemd-tmpfiles; on macOS every
36
+ * few days. Documented in WIKI.
37
+ */
38
+ export type LogLevel = "debug" | "info" | "warn" | "error";
39
+ /**
40
+ * Public surface. `log()` writes a line message (mirrors what
41
+ * OpenCode's own log channel takes); `event()` writes a structured
42
+ * record with a name and a typed payload. `path()` returns the file
43
+ * the logger is writing to — useful at startup to print "rich logs at
44
+ * <path>" so the user can find them. `close()` drains the stream.
45
+ */
46
+ export interface FileLogger {
47
+ log(level: LogLevel, message: string): void;
48
+ event(name: string, data?: Record<string, unknown>): void;
49
+ path(): string;
50
+ close(): void;
51
+ }
52
+ export interface CreateFileLoggerOptions {
53
+ /** Service name — used in the filename and on every record. */
54
+ service: string;
55
+ /**
56
+ * Fields included on EVERY record (e.g. `{ root: "/path/to/repo" }`).
57
+ * Kept small — they multiply across every line of the file.
58
+ */
59
+ base?: Record<string, unknown>;
60
+ }
61
+ /**
62
+ * Directory the logger writes into.
63
+ *
64
+ * Honours `OPENCODE_DIANE_LOG_DIR` if set — point it at a mounted
65
+ * volume when running under Docker (`-e OPENCODE_DIANE_LOG_DIR=/logs
66
+ * -v $PWD/logs:/logs`) so logs survive the container and can be read
67
+ * with `analyze-logs.py --dir /logs` from outside. Falls back to
68
+ * `os.tmpdir()/opencode-diane/` everywhere else; on a fresh host that
69
+ * is `/tmp/opencode-diane/` on Linux and a per-user temp folder on
70
+ * macOS / Windows. Exported for tests + docs.
71
+ */
72
+ export declare function richLogsDir(): string;
73
+ /**
74
+ * Best-effort sanitiser for values put on a structured event payload —
75
+ * trims long strings to keep the log file manageable and caps arrays
76
+ * at a sensible length. Used to shape the `args` field of a
77
+ * `tool.call` event before it lands on disk: a tool's free-form
78
+ * `query` or `content` field could in principle be many KB, and we
79
+ * don't want a single log line to dominate the file. The marker
80
+ * `…(+N chars)` / `…(+N items)` is preserved so a reader knows the
81
+ * truncation happened. Returns the trimmed value; doesn't mutate the
82
+ * input. Shallow-recurses through plain objects and arrays — there's
83
+ * no cycle protection because tool args are flat data, but a try
84
+ * around the caller's `JSON.stringify` still catches anything weird.
85
+ */
86
+ export declare function truncateForLog(value: unknown, maxStringLength?: number): unknown;
87
+ export declare function createFileLogger(opts: CreateFileLoggerOptions): FileLogger;