opencode-harness-kit 0.0.0-stage → 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.
@@ -0,0 +1,98 @@
1
+ // Local telemetry.
2
+ //
3
+ // Purpose is comparison, not surveillance: `oc-harness baseline` reads the
4
+ // opencode database for history, this records what the kit itself changes so
5
+ // the two can be diffed. It is a JSONL file in the project, never a network
6
+ // call, and it is gitignored.
7
+ //
8
+ // What is worth recording: context size per request (to see pruning working),
9
+ // latency by role and effort (to see tiering paying off), tool denials (to see
10
+ // the guard being useful rather than annoying), and provider rate limits.
11
+
12
+ import type { Plugin } from "@opencode-ai/plugin"
13
+ import { appendFileSync, mkdirSync, existsSync, statSync, renameSync } from "node:fs"
14
+ import { join } from "node:path"
15
+
16
+ const MAX_BYTES = 4 * 1024 * 1024
17
+
18
+ function logPath(dir: string): string {
19
+ return join(dir, "telemetry.jsonl")
20
+ }
21
+
22
+ /** Rotate by size so a long-running project cannot fill its disk. */
23
+ function rotate(file: string) {
24
+ try {
25
+ if (existsSync(file) && statSync(file).size > MAX_BYTES) {
26
+ renameSync(file, `${file}.1`)
27
+ }
28
+ } catch {
29
+ // Best effort.
30
+ }
31
+ }
32
+
33
+ function write(dir: string, record: Record<string, unknown>) {
34
+ try {
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
36
+ const file = logPath(dir)
37
+ rotate(file)
38
+ appendFileSync(file, JSON.stringify({ t: new Date().toISOString(), ...record }) + "\n")
39
+ } catch {
40
+ // Telemetry must never be load-bearing.
41
+ }
42
+ }
43
+
44
+ export const TelemetryPlugin: Plugin = async ({ worktree }) => {
45
+ const dir = join(worktree, ".space")
46
+ const started = Date.now()
47
+
48
+ return {
49
+ event: async ({ event }) => {
50
+ // Events are provider-specific and opencode may add shapes we have never
51
+ // seen. Anything unrecognised is ignored rather than trusted.
52
+ const type = (event as { type?: unknown } | undefined)?.type
53
+ if (typeof type !== "string") return
54
+
55
+ // Provider rate limits are the one signal that changes behaviour, so
56
+ // they get logged even though the event shape is provider-specific.
57
+ if (/error|rate|limit|quota/i.test(type)) {
58
+ write(dir, { kind: "provider-event", type, detail: truncate(safeStringify(event)) })
59
+ }
60
+ },
61
+
62
+ "tool.execute.after": async (input, output) => {
63
+ const text = typeof output?.output === "string" ? output.output : ""
64
+ if (text.includes("[opencode-harness-kit] blocked")) {
65
+ write(dir, { kind: "guard", tool: input.tool, rule: extractRule(text) })
66
+ return
67
+ }
68
+ const command = String((input.args as { command?: unknown })?.command ?? "")
69
+ if (input.tool === "bash" && /git push|rm -rf|chmod 777/.test(command)) {
70
+ write(dir, { kind: "guard-attempt", tool: input.tool, command: truncate(command) })
71
+ }
72
+ },
73
+
74
+ dispose: async () => {
75
+ write(dir, { kind: "session", uptimeMs: Date.now() - started })
76
+ },
77
+ }
78
+ }
79
+
80
+ function extractRule(output: string): string {
81
+ const m = output.match(/blocked:\s*([^\n]+)/)
82
+ return m ? m[1].trim() : "unknown"
83
+ }
84
+
85
+ function truncate(s: string, n = 300): string {
86
+ return s.length > n ? `${s.slice(0, n)}...` : s
87
+ }
88
+
89
+ /** Stringify without throwing on a cycle or a BigInt. */
90
+ function safeStringify(value: unknown): string {
91
+ try {
92
+ return JSON.stringify(value) ?? String(value)
93
+ } catch {
94
+ return String(value)
95
+ }
96
+ }
97
+
98
+ export default TelemetryPlugin