@rejacky/opencode-insights 0.1.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -93,12 +93,13 @@ The `uninstall` command removes plugin config entries and local Insights data; i
93
93
 
94
94
  - Configurable live metrics in the OpenCode session prompt zone.
95
95
  - A collapsible session-wide `Token Usage` sidebar showing total tokens, response count, input/output/reasoning usage, cache read/write usage, and aggregate cache rate. It loads completed responses already present in the session and continues updating live.
96
+ - An opt-in `Go Usage` sidebar showing OpenCode Go rolling/weekly/monthly usage limits for sessions that use the `opencode-go` provider.
96
97
  - Subagent status (running, done, failed, elapsed time, and token/context usage) in the sidebar.
97
98
  - Local capture of OpenCode hook/event data without redaction.
98
99
  - A local web viewer for reconstructed sessions, user turns, hidden request context, system/messages transforms, and assistant thinking/response sequences.
99
100
  - Native OpenCode footer components (project directory and version) remain visible — the plugin does not override `sidebar_footer` or `home_prompt_right` slots.
100
101
 
101
- The right sidebar contains two independent plugin sections: `Token Usage` and `Subagents`. Click either section header to collapse or expand it. Token usage is aggregated across the full session; prompt-right `used` and `cache` values continue to represent the latest completed assistant response.
102
+ The right sidebar contains the plugin sections: `Token Usage`, `Go Usage` (when enabled and the session uses `opencode-go`), and `Subagents`. Click any section header to collapse or expand it. Token usage is aggregated across the full session; prompt-right `used` and `cache` values continue to represent the latest completed assistant response.
102
103
 
103
104
  ## TUI Metrics Configuration
104
105
 
@@ -118,6 +119,28 @@ With a custom database path, the configuration file is created in that database'
118
119
 
119
120
  `promptRightMetrics` controls both the fields and their order. Supported values are `tps`, `avg`, `ttft`, `used`, `cache`, `input`, `output`, and `reasoning`. Values that are not recognized are ignored; an empty or invalid configuration uses the default. Restart OpenCode after editing this file.
120
121
 
122
+ ## Go Usage Configuration
123
+
124
+ The `Go Usage` sidebar shows the rolling (5 hour), weekly, and monthly usage limits of your OpenCode Go subscription for sessions that use the `opencode-go` provider. It is disabled by default and opt-in:
125
+
126
+ ```json
127
+ {
128
+ "goUsage": {
129
+ "enabled": true,
130
+ "cookie": "Fe26.2**...",
131
+ "workspaceID": "wrk_...",
132
+ "refreshMs": 300000
133
+ }
134
+ }
135
+ ```
136
+
137
+ - `enabled` — set to `true` to activate the section. Defaults to `false`.
138
+ - `cookie` — the `auth` session cookie for `opencode.ai` (see below).
139
+ - `workspaceID` — your workspace id, visible in the console URL (`/workspace/<workspaceID>/go`).
140
+ - `refreshMs` — how often to re-fetch usage from the console. Defaults to `300000` (5 minutes); values below 60000 are clamped.
141
+
142
+ To get the cookie, log in to `https://opencode.ai`, open the workspace `/go` page, then copy the `auth` cookie value from your browser's DevTools (Application → Cookies → `https://opencode.ai`). The cookie lasts up to a year; if the section shows an error, copy it again. Restart OpenCode after editing this file.
143
+
121
144
  ## Open The Viewer
122
145
 
123
146
  Start the local web viewer and open it in your browser:
@@ -216,24 +239,30 @@ If SQLite is unavailable in the plugin runtime, the fallback path is:
216
239
  ~/.opencode-insights/insights.sqlite.jsonl
217
240
  ```
218
241
 
219
- The database keeps one day of captures by default and auto-cleans older rows on startup and after new captures. Set `retentionDays` to another number of days, or `0` to disable auto-cleaning.
242
+ The database keeps one day of captures by default and auto-cleans older rows on
243
+ startup and after new captures; `retentionDays` sets how many days to keep (`0`
244
+ disables auto-cleaning).
220
245
 
221
- You can override storage and retention in `opencode.json` or `opencode.jsonc`:
246
+ Storage settings live in the config file `~/.opencode-insights/config.jsonc` (JSONC
247
+ comments allowed). The plugin creates it on first run with `dbPath` commented out
248
+ (uncomment to relocate the database) and `retentionDays` defaulting to 1:
222
249
 
223
- ```json
250
+ ```jsonc
224
251
  {
225
- "plugin": [
226
- [
227
- "@rejacky/opencode-insights",
228
- {
229
- "dbPath": "/absolute/path/to/insights.sqlite",
230
- "retentionDays": 1
231
- }
232
- ]
233
- ]
252
+ // Database file. Default: ~/.opencode-insights/insights.sqlite
253
+ // "dbPath": "/absolute/path/to/insights.sqlite",
254
+ "retentionDays": 1,
255
+ "promptRightMetrics": ["tps", "avg", "used", "cache"],
256
+ "goUsage": { "enabled": false, "cookie": "", "workspaceID": "", "refreshMs": 300000 }
234
257
  }
235
258
  ```
236
259
 
260
+ A legacy `config.json` is still honored when `config.jsonc` does not exist. Plugin
261
+ params such as `{ "dbPath": … }` in `opencode.json` are ignored after the upgrade —
262
+ if you previously set `dbPath` or `retentionDays` there, copy those values into
263
+ `~/.opencode-insights/config.jsonc`. CLI commands read the configured database path
264
+ from this file; the former `--db`/`--data-dir`/`--retention-days` flags are removed.
265
+
237
266
  ## Privacy Model
238
267
 
239
268
  This plugin intentionally does not redact anything. It stores data locally exactly as OpenCode exposes it to plugin hooks and events.
@@ -101,13 +101,24 @@ type InsightsOptions = {
101
101
  dbPath?: unknown;
102
102
  retentionDays?: unknown;
103
103
  };
104
+ type GoUsageConfig = {
105
+ enabled: boolean;
106
+ cookie: string;
107
+ workspaceID: string;
108
+ refreshMs: number;
109
+ };
104
110
  type InsightsConfig = {
105
111
  promptRightMetrics: PromptRightMetric[];
112
+ goUsage: GoUsageConfig;
113
+ dbPath?: string | undefined;
114
+ retentionDays?: number | undefined;
106
115
  };
107
116
  declare function defaultDataDir(): string;
108
117
  declare function resolveCapturePath(options?: InsightsOptions): string;
109
118
  declare function resolveInsightsConfigPath(options?: InsightsOptions): string;
119
+ declare function resolveLegacyInsightsConfigPath(options?: InsightsOptions): string;
110
120
  declare function readInsightsConfig(options?: InsightsOptions): Promise<InsightsConfig>;
121
+ declare function insightsOptionsFromConfig(config: InsightsConfig, dataDir?: string): InsightsOptions;
111
122
  declare function normalizeChatMessageCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
112
123
  declare function normalizeChatParamsCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
113
124
  declare function normalizeChatHeadersCapture(input: unknown, output: unknown, timestamp?: number): CaptureRecord;
@@ -145,4 +156,4 @@ declare class SqliteCaptureStore implements CaptureStore {
145
156
  declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
146
157
  declare function resolveRetentionDays(value: unknown): number;
147
158
 
148
- export { type AssistantResponseUsage as A, renderMetricsText as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, renderPromptRightMetricsText as E, renderResponseMetricsText as F, renderSessionTokenUsage as G, resolveCapturePath as H, type InsightsConfig as I, JsonlCaptureStore as J, resolveInsightsConfigPath as K, resolveRetentionDays as L, type MessageTiming as M, type PromptRightMetric as P, type SessionAverage as S, type CaptureKind as a, type CaptureStore as b, type InsightsOptions as c, type MetricsState as d, type SessionTokenUsage as e, SqliteCaptureStore as f, type SqliteDb as g, type StreamSample as h, createCaptureStore as i, createMetricsState as j, defaultDataDir as k, estimateStreamTokens as l, extractEventType as m, getSessionTokenUsage as n, normalizeChatHeadersCapture as o, normalizeChatMessageCapture as p, normalizeChatParamsCapture as q, normalizeEventCapture as r, normalizeExperimentalChatMessagesTransformCapture as s, normalizeExperimentalChatSystemTransformCapture as t, normalizeToolCapture as u, openDatabase as v, readInsightsConfig as w, recordAssistantDelta as x, recordAssistantMessage as y, recordToolActivity as z };
159
+ export { type AssistantResponseUsage as A, recordToolActivity as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, renderMetricsText as E, renderPromptRightMetricsText as F, type GoUsageConfig as G, renderResponseMetricsText as H, type InsightsConfig as I, JsonlCaptureStore as J, renderSessionTokenUsage as K, resolveCapturePath as L, type MessageTiming as M, resolveInsightsConfigPath as N, resolveLegacyInsightsConfigPath as O, type PromptRightMetric as P, resolveRetentionDays as Q, type SessionAverage as S, type CaptureKind as a, type CaptureStore as b, type InsightsOptions as c, type MetricsState as d, type SessionTokenUsage as e, SqliteCaptureStore as f, type SqliteDb as g, type StreamSample as h, createCaptureStore as i, createMetricsState as j, defaultDataDir as k, estimateStreamTokens as l, extractEventType as m, getSessionTokenUsage as n, insightsOptionsFromConfig as o, normalizeChatHeadersCapture as p, normalizeChatMessageCapture as q, normalizeChatParamsCapture as r, normalizeEventCapture as s, normalizeExperimentalChatMessagesTransformCapture as t, normalizeExperimentalChatSystemTransformCapture as u, normalizeToolCapture as v, openDatabase as w, readInsightsConfig as x, recordAssistantDelta as y, recordAssistantMessage as z };
@@ -266,8 +266,11 @@ import { mkdir, appendFile, readFile, writeFile } from "fs/promises";
266
266
  import { existsSync, readFileSync } from "fs";
267
267
  import { dirname, join } from "path";
268
268
  import { homedir } from "os";
269
+ import { parse } from "jsonc-parser";
269
270
  var DEFAULT_RETENTION_DAYS = 1;
270
271
  var DAY_MS = 24 * 60 * 60 * 1e3;
272
+ var DEFAULT_GO_USAGE_REFRESH_MS = 3e5;
273
+ var MIN_GO_USAGE_REFRESH_MS = 6e4;
271
274
  var sequence = 0;
272
275
  function nextID(timestamp) {
273
276
  sequence += 1;
@@ -326,32 +329,81 @@ function resolveCapturePath(options = {}) {
326
329
  return join(dataDir, "insights.sqlite");
327
330
  }
328
331
  function resolveInsightsConfigPath(options = {}) {
329
- return join(dirname(resolveCapturePath(options)), "config.json");
332
+ return join(resolveConfigDataDir(options), "config.jsonc");
333
+ }
334
+ function resolveLegacyInsightsConfigPath(options = {}) {
335
+ return join(resolveConfigDataDir(options), "config.json");
336
+ }
337
+ function resolveConfigDataDir(options = {}) {
338
+ return typeof options.dataDir === "string" && options.dataDir.length > 0 ? options.dataDir : defaultDataDir();
330
339
  }
331
340
  async function readInsightsConfig(options = {}) {
332
- const path = resolveInsightsConfigPath(options);
333
- if (!existsSync(path)) {
334
- try {
335
- await mkdir(dirname(path), { recursive: true });
336
- await writeFile(path, `${JSON.stringify(defaultInsightsConfig(), null, 2)}
337
- `, "utf8");
338
- } catch {
339
- return defaultInsightsConfig();
340
- }
341
+ const jsoncPath = resolveInsightsConfigPath(options);
342
+ if (existsSync(jsoncPath)) {
343
+ return parseInsightsConfigFile(jsoncPath);
344
+ }
345
+ const legacyPath = resolveLegacyInsightsConfigPath(options);
346
+ if (existsSync(legacyPath)) {
347
+ return parseInsightsConfigFile(legacyPath);
341
348
  }
342
349
  try {
343
- return insightsConfigFrom(JSON.parse(await readFile(path, "utf8")));
350
+ await mkdir(dirname(jsoncPath), { recursive: true });
351
+ await writeFile(jsoncPath, defaultInsightsConfigJsonc(), "utf8");
352
+ } catch {
353
+ }
354
+ return defaultInsightsConfig();
355
+ }
356
+ async function parseInsightsConfigFile(path) {
357
+ try {
358
+ const parseErrors = [];
359
+ const parsed = parse(await readFile(path, "utf8"), parseErrors, { allowTrailingComma: true });
360
+ if (parseErrors.length > 0) return defaultInsightsConfig();
361
+ return insightsConfigFrom(parsed);
344
362
  } catch {
345
363
  return defaultInsightsConfig();
346
364
  }
347
365
  }
366
+ function defaultInsightsConfigJsonc() {
367
+ return [
368
+ "{",
369
+ " // Database file. Default: ~/.opencode-insights/insights.sqlite",
370
+ ' // "dbPath": "/absolute/path/to/insights.sqlite",',
371
+ ` "retentionDays": ${DEFAULT_RETENTION_DAYS},`,
372
+ ` "promptRightMetrics": ${JSON.stringify(DEFAULT_PROMPT_RIGHT_METRICS)},`,
373
+ ` "goUsage": ${JSON.stringify(defaultGoUsageConfig())}`,
374
+ "}",
375
+ ""
376
+ ].join("\n");
377
+ }
378
+ function insightsOptionsFromConfig(config, dataDir) {
379
+ return compactUndefined({ dataDir, dbPath: config.dbPath, retentionDays: config.retentionDays });
380
+ }
348
381
  function defaultInsightsConfig() {
349
- return { promptRightMetrics: [...DEFAULT_PROMPT_RIGHT_METRICS] };
382
+ return { promptRightMetrics: [...DEFAULT_PROMPT_RIGHT_METRICS], goUsage: defaultGoUsageConfig() };
383
+ }
384
+ function defaultGoUsageConfig() {
385
+ return { enabled: false, cookie: "", workspaceID: "", refreshMs: DEFAULT_GO_USAGE_REFRESH_MS };
350
386
  }
351
387
  function insightsConfigFrom(value) {
352
- if (!isRecord(value) || !Array.isArray(value.promptRightMetrics)) return defaultInsightsConfig();
353
- const metrics = value.promptRightMetrics.filter(isPromptRightMetric);
354
- return metrics.length ? { promptRightMetrics: metrics } : defaultInsightsConfig();
388
+ const record = isRecord(value) ? value : {};
389
+ const metrics = Array.isArray(record.promptRightMetrics) ? record.promptRightMetrics.filter(isPromptRightMetric) : [];
390
+ const dbPath = typeof record.dbPath === "string" && record.dbPath.trim().length > 0 ? record.dbPath.trim() : void 0;
391
+ const rawRetention = record.retentionDays;
392
+ const retentionDays = rawRetention === void 0 || rawRetention === null || rawRetention === "" ? void 0 : resolveRetentionDays(rawRetention);
393
+ return {
394
+ promptRightMetrics: metrics.length ? metrics : [...DEFAULT_PROMPT_RIGHT_METRICS],
395
+ goUsage: goUsageConfigFrom(record.goUsage),
396
+ dbPath,
397
+ retentionDays
398
+ };
399
+ }
400
+ function goUsageConfigFrom(value) {
401
+ const record = isRecord(value) ? value : {};
402
+ const enabled = record.enabled === true;
403
+ const cookie = typeof record.cookie === "string" && record.cookie.length > 0 ? record.cookie : "";
404
+ const workspaceID = typeof record.workspaceID === "string" && record.workspaceID.length > 0 ? record.workspaceID : "";
405
+ const refreshMs = typeof record.refreshMs === "number" && Number.isFinite(record.refreshMs) ? Math.max(MIN_GO_USAGE_REFRESH_MS, Math.trunc(record.refreshMs)) : DEFAULT_GO_USAGE_REFRESH_MS;
406
+ return { enabled, cookie, workspaceID, refreshMs };
355
407
  }
356
408
  function isPromptRightMetric(value) {
357
409
  return value === "tps" || value === "avg" || value === "ttft" || value === "used" || value === "cache" || value === "input" || value === "output" || value === "reasoning";
@@ -692,7 +744,9 @@ export {
692
744
  defaultDataDir,
693
745
  resolveCapturePath,
694
746
  resolveInsightsConfigPath,
747
+ resolveLegacyInsightsConfigPath,
695
748
  readInsightsConfig,
749
+ insightsOptionsFromConfig,
696
750
  normalizeChatMessageCapture,
697
751
  normalizeChatParamsCapture,
698
752
  normalizeChatHeadersCapture,
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as CaptureRecord } from './capture-BIiGg2nW.js';
2
+ import { C as CaptureRecord } from './capture-CwesXqmX.js';
3
3
 
4
4
  type HistoryMessage = {
5
5
  id: string;
@@ -72,10 +72,10 @@ type CliOptions = {
72
72
  port?: number | undefined;
73
73
  output?: string | undefined;
74
74
  configDir?: string | undefined;
75
- retentionDays?: number | undefined;
76
75
  dryRun: boolean;
77
76
  keepData: boolean;
78
77
  };
78
+ declare function unsupportedFlagWarning(arg: string): string | undefined;
79
79
  declare function parseOptions(args: string[]): CliOptions;
80
80
  declare function summarizeSessions(sessions: ReturnType<typeof buildRequestHistory>["sessions"]): {
81
81
  id: string;
@@ -95,4 +95,4 @@ declare function addUniquePlugin(config: JsonObject, plugin: string): boolean;
95
95
  declare function removePlugin(config: JsonObject, plugin: string): boolean;
96
96
  declare function uninstallOpenCode(options: CliOptions): Promise<string>;
97
97
 
98
- export { addUniquePlugin, configureOpenCodeDebug, defaultOpenCodeConfigDir, formatSessionSummary, parseOptions, removePlugin, resolveOpenCodeConfigPath, stripJsonCommentsAndTrailingCommas, summarizeSessions, uninstallOpenCode };
98
+ export { addUniquePlugin, configureOpenCodeDebug, defaultOpenCodeConfigDir, formatSessionSummary, parseOptions, removePlugin, resolveOpenCodeConfigPath, stripJsonCommentsAndTrailingCommas, summarizeSessions, uninstallOpenCode, unsupportedFlagWarning };
package/dist/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  openDatabase,
4
- resolveCapturePath
5
- } from "./chunk-3YLLHABZ.js";
4
+ readInsightsConfig,
5
+ resolveCapturePath,
6
+ resolveInsightsConfigPath
7
+ } from "./chunk-RZGCLQ2M.js";
6
8
 
7
9
  // src/cli.ts
8
10
  import { execFile as execFile2 } from "child_process";
@@ -1446,10 +1448,25 @@ var DEFAULT_HISTORY_LIMIT = 5e3;
1446
1448
  var SERVER_PLUGIN_SPEC = "@rejacky/opencode-insights";
1447
1449
  var TUI_PLUGIN_SPEC = SERVER_PLUGIN_SPEC;
1448
1450
  var SUBPATH_TUI_PLUGIN_SPEC = "@rejacky/opencode-insights/tui";
1451
+ function unsupportedFlagWarning(arg) {
1452
+ if (arg === "--data-dir") {
1453
+ return "warning: --data-dir is no longer supported; the CLI reads the configured database path from ~/.opencode-insights/config.jsonc";
1454
+ }
1455
+ const setting = arg === "--db" ? "dbPath" : arg === "--retention-days" ? "retentionDays" : void 0;
1456
+ if (!setting) return void 0;
1457
+ return `warning: ${arg} is no longer supported; set ${setting} in ~/.opencode-insights/config.jsonc`;
1458
+ }
1449
1459
  async function main(argv) {
1450
1460
  const command = argv[2] ?? "recent";
1461
+ for (const arg of argv.slice(3)) {
1462
+ const warning = unsupportedFlagWarning(arg);
1463
+ if (warning) process.stderr.write(`${warning}
1464
+ `);
1465
+ }
1451
1466
  const options = parseOptions(argv.slice(3));
1452
1467
  const positionals = parsePositionals(argv.slice(3));
1468
+ const config = await readInsightsConfig();
1469
+ options.dbPath = config.dbPath;
1453
1470
  if (command === "help" || command === "--help" || command === "-h") {
1454
1471
  process.stdout.write(`${usage()}
1455
1472
  `);
@@ -1553,14 +1570,6 @@ function parseOptions(args) {
1553
1570
  options.dryRun = true;
1554
1571
  } else if (arg === "--keep-data") {
1555
1572
  options.keepData = true;
1556
- } else if (arg === "--db") {
1557
- const value = args[index + 1];
1558
- if (value) options.dbPath = value;
1559
- index += 1;
1560
- } else if (arg === "--data-dir") {
1561
- const value = args[index + 1];
1562
- if (value) options.dataDir = value;
1563
- index += 1;
1564
1573
  } else if (arg === "--limit") {
1565
1574
  options.limit = Number.parseInt(args[index + 1] ?? "20", 10);
1566
1575
  options.limitProvided = true;
@@ -1580,16 +1589,10 @@ function parseOptions(args) {
1580
1589
  const value = args[index + 1];
1581
1590
  if (value) options.configDir = value;
1582
1591
  index += 1;
1583
- } else if (arg === "--retention-days") {
1584
- options.retentionDays = Number.parseFloat(args[index + 1] ?? "");
1585
- index += 1;
1586
1592
  }
1587
1593
  }
1588
1594
  if (!Number.isFinite(options.limit) || options.limit < 1) options.limit = DEFAULT_RECENT_LIMIT;
1589
1595
  if (options.port !== void 0 && (!Number.isFinite(options.port) || options.port < 1)) options.port = 8765;
1590
- if (options.retentionDays !== void 0 && (!Number.isFinite(options.retentionDays) || options.retentionDays < 0)) {
1591
- options.retentionDays = void 0;
1592
- }
1593
1596
  return options;
1594
1597
  }
1595
1598
  function parsePositionals(args) {
@@ -1732,7 +1735,7 @@ async function configureOpenCodeDebug(options) {
1732
1735
  const tuiSource = await readJsonConfigSource(tuiPath);
1733
1736
  const opencodeConfig = await readJsonConfig(opencodePath, { plugin: [] }, opencodeSource);
1734
1737
  const tuiConfig = await readJsonConfig(tuiPath, { plugin: [] }, tuiSource);
1735
- setSinglePluginSpec(opencodeConfig, SERVER_PLUGIN_SPEC, [localServerEntry, debugServerOptions(options)], localServerEntry);
1738
+ setSinglePluginSpec(opencodeConfig, SERVER_PLUGIN_SPEC, localServerEntry);
1736
1739
  setSinglePluginSpec(tuiConfig, TUI_PLUGIN_SPEC, localTuiEntry);
1737
1740
  removePlugin(tuiConfig, SUBPATH_TUI_PLUGIN_SPEC);
1738
1741
  const lines = [
@@ -1740,6 +1743,7 @@ async function configureOpenCodeDebug(options) {
1740
1743
  `TUI config: ${tuiPath}`,
1741
1744
  `Local server plugin: ${localServerEntry}`,
1742
1745
  `Local TUI plugin: ${localTuiEntry}`,
1746
+ `Insights config: ${resolveInsightsConfigPath({ dataDir: options.dataDir })}`,
1743
1747
  `Server plugin: set local build output`,
1744
1748
  `TUI plugin: set local build output`
1745
1749
  ];
@@ -1747,6 +1751,7 @@ async function configureOpenCodeDebug(options) {
1747
1751
  lines.push("Dry run: no files written.");
1748
1752
  return lines.join("\n");
1749
1753
  }
1754
+ await readInsightsConfig({ dataDir: options.dataDir });
1750
1755
  await mkdir(configDir, { recursive: true });
1751
1756
  await writeJsonConfig(opencodePath, opencodeConfig, opencodeSource);
1752
1757
  await writeJsonConfig(tuiPath, tuiConfig, tuiSource);
@@ -1826,11 +1831,6 @@ function stripJsonCommentsAndTrailingCommas(input) {
1826
1831
  function isJsonObject(value) {
1827
1832
  return !!value && typeof value === "object" && !Array.isArray(value);
1828
1833
  }
1829
- function debugServerOptions(options) {
1830
- const serverOptions = {};
1831
- if (options.retentionDays !== void 0) serverOptions.retentionDays = options.retentionDays;
1832
- return serverOptions;
1833
- }
1834
1834
  function addUniquePlugin(config, plugin) {
1835
1835
  const current = Array.isArray(config.plugin) ? config.plugin : [];
1836
1836
  if (current.includes(plugin)) {
@@ -1928,17 +1928,17 @@ async function writeJsonConfig(path, config, source) {
1928
1928
  function usage() {
1929
1929
  return [
1930
1930
  "Usage:",
1931
- " opencode-insights debug [--config-dir DIR] [--retention-days DAYS] [--dry-run]",
1932
- " opencode-insights uninstall [--config-dir DIR] [--db PATH] [--data-dir DIR] [--keep-data] [--dry-run]",
1933
- " opencode-insights recent [--db PATH] [--data-dir DIR] [--limit N] [--json]",
1934
- " opencode-insights sessions [--db PATH] [--data-dir DIR] [--limit N] [--json]",
1935
- " opencode-insights history [--db PATH] [--data-dir DIR] [--limit N]",
1936
- " opencode-insights show <session-id> [--db PATH] [--data-dir DIR] [--limit N]",
1937
- " opencode-insights export <session-id> [--output PATH] [--db PATH] [--data-dir DIR] [--limit N]",
1938
- " opencode-insights serve [--db PATH] [--data-dir DIR] [--limit N] [--host HOST] [--port PORT]",
1939
- " opencode-insights open [--db PATH] [--data-dir DIR] [--limit N] [--host HOST] [--port PORT]",
1940
- " opencode-insights doctor [--db PATH] [--data-dir DIR]",
1941
- " opencode-insights vacuum [--db PATH] [--data-dir DIR]"
1931
+ " opencode-insights debug [--config-dir DIR] [--dry-run]",
1932
+ " opencode-insights uninstall [--config-dir DIR] [--keep-data] [--dry-run]",
1933
+ " opencode-insights recent [--limit N] [--json]",
1934
+ " opencode-insights sessions [--limit N] [--json]",
1935
+ " opencode-insights history [--limit N]",
1936
+ " opencode-insights show <session-id> [--limit N]",
1937
+ " opencode-insights export <session-id> [--output PATH] [--limit N]",
1938
+ " opencode-insights serve [--limit N] [--host HOST] [--port PORT]",
1939
+ " opencode-insights open [--limit N] [--host HOST] [--port PORT]",
1940
+ " opencode-insights doctor",
1941
+ " opencode-insights vacuum"
1942
1942
  ].join("\n");
1943
1943
  }
1944
1944
  function isDirectRun() {
@@ -1967,5 +1967,6 @@ export {
1967
1967
  resolveOpenCodeConfigPath,
1968
1968
  stripJsonCommentsAndTrailingCommas,
1969
1969
  summarizeSessions,
1970
- uninstallOpenCode
1970
+ uninstallOpenCode,
1971
+ unsupportedFlagWarning
1971
1972
  };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from '@opencode-ai/plugin';
2
2
  import { TuiPlugin } from '@opencode-ai/plugin/tui';
3
- export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, D as DEFAULT_PROMPT_RIGHT_METRICS, I as InsightsConfig, c as InsightsOptions, J as JsonlCaptureStore, M as MessageTiming, d as MetricsState, P as PromptRightMetric, S as SessionAverage, e as SessionTokenUsage, f as SqliteCaptureStore, g as SqliteDb, h as StreamSample, i as createCaptureStore, j as createMetricsState, k as defaultDataDir, l as estimateStreamTokens, m as extractEventType, n as getSessionTokenUsage, o as normalizeChatHeadersCapture, p as normalizeChatMessageCapture, q as normalizeChatParamsCapture, r as normalizeEventCapture, s as normalizeExperimentalChatMessagesTransformCapture, t as normalizeExperimentalChatSystemTransformCapture, u as normalizeToolCapture, v as openDatabase, w as readInsightsConfig, x as recordAssistantDelta, y as recordAssistantMessage, z as recordToolActivity, B as renderMetricsText, E as renderPromptRightMetricsText, F as renderResponseMetricsText, G as renderSessionTokenUsage, H as resolveCapturePath, K as resolveInsightsConfigPath, L as resolveRetentionDays } from './capture-BIiGg2nW.js';
3
+ export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig, c as InsightsOptions, J as JsonlCaptureStore, M as MessageTiming, d as MetricsState, P as PromptRightMetric, S as SessionAverage, e as SessionTokenUsage, f as SqliteCaptureStore, g as SqliteDb, h as StreamSample, i as createCaptureStore, j as createMetricsState, k as defaultDataDir, l as estimateStreamTokens, m as extractEventType, n as getSessionTokenUsage, o as insightsOptionsFromConfig, p as normalizeChatHeadersCapture, q as normalizeChatMessageCapture, r as normalizeChatParamsCapture, s as normalizeEventCapture, t as normalizeExperimentalChatMessagesTransformCapture, u as normalizeExperimentalChatSystemTransformCapture, v as normalizeToolCapture, w as openDatabase, x as readInsightsConfig, y as recordAssistantDelta, z as recordAssistantMessage, B as recordToolActivity, E as renderMetricsText, F as renderPromptRightMetricsText, H as renderResponseMetricsText, K as renderSessionTokenUsage, L as resolveCapturePath, N as resolveInsightsConfigPath, O as resolveLegacyInsightsConfigPath, Q as resolveRetentionDays } from './capture-CwesXqmX.js';
4
4
 
5
5
  type SubagentStatus = "running" | "done" | "error";
6
6
  type SubagentInfo = {
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  estimateStreamTokens,
20
20
  extractEventType,
21
21
  getSessionTokenUsage,
22
+ insightsOptionsFromConfig,
22
23
  normalizeChatHeadersCapture,
23
24
  normalizeChatMessageCapture,
24
25
  normalizeChatParamsCapture,
@@ -37,8 +38,9 @@ import {
37
38
  renderSessionTokenUsage,
38
39
  resolveCapturePath,
39
40
  resolveInsightsConfigPath,
41
+ resolveLegacyInsightsConfigPath,
40
42
  resolveRetentionDays
41
- } from "./chunk-3YLLHABZ.js";
43
+ } from "./chunk-RZGCLQ2M.js";
42
44
 
43
45
  // src/cli-shim.ts
44
46
  import { existsSync } from "fs";
@@ -91,7 +93,8 @@ var OpenCodeInsights = async (_input, options) => {
91
93
  if (options?.cliShim !== false) {
92
94
  void ensureCliShim().catch(() => void 0);
93
95
  }
94
- const store = createCaptureStore(options);
96
+ const config = await readInsightsConfig(options);
97
+ const store = createCaptureStore(insightsOptionsFromConfig(config, options?.dataDir));
95
98
  try {
96
99
  await store.initialize?.();
97
100
  } catch {
@@ -157,6 +160,7 @@ export {
157
160
  getSubagentSidebarModel,
158
161
  getSubagentSidebarRowAtLine,
159
162
  id,
163
+ insightsOptionsFromConfig,
160
164
  normalizeChatHeadersCapture,
161
165
  normalizeChatMessageCapture,
162
166
  normalizeChatParamsCapture,
@@ -179,6 +183,7 @@ export {
179
183
  renderSubagentStatus,
180
184
  resolveCapturePath,
181
185
  resolveInsightsConfigPath,
186
+ resolveLegacyInsightsConfigPath,
182
187
  resolveRetentionDays,
183
188
  server,
184
189
  rootTui as tui
package/dist/tui.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  recordToolActivity,
13
13
  renderPromptRightMetricsText,
14
14
  renderSessionTokenUsage
15
- } from "./chunk-3YLLHABZ.js";
15
+ } from "./chunk-RZGCLQ2M.js";
16
16
 
17
17
  // src/tui.tsx
18
18
  import { createTextAttributes, StyledText } from "@opentui/core";
@@ -37,6 +37,130 @@ function hasRenderStateChanged(previous, next) {
37
37
  return !previous || previous.content !== next.content || previous.visible !== next.visible || previous.height !== next.height;
38
38
  }
39
39
 
40
+ // src/go-usage.ts
41
+ var GoUsageError = class extends Error {
42
+ };
43
+ var USAGE_PATTERN = /(rollingUsage|weeklyUsage|monthlyUsage):\$R\[(\d+)\]=\{([^{}]*)\}/g;
44
+ var KEY_PATTERN = /([a-zA-Z]+):/g;
45
+ function parseGoUsageHtml(html) {
46
+ const usage = {};
47
+ USAGE_PATTERN.lastIndex = 0;
48
+ let match;
49
+ while (match = USAGE_PATTERN.exec(html)) {
50
+ const key = match[1];
51
+ const literal = match[3] ?? "";
52
+ try {
53
+ usage[key] = JSON.parse(`{${literal.replace(KEY_PATTERN, '"$1":')}}`);
54
+ } catch {
55
+ return void 0;
56
+ }
57
+ }
58
+ if (!usage.rollingUsage || !usage.weeklyUsage || !usage.monthlyUsage) return void 0;
59
+ return usage;
60
+ }
61
+ async function fetchGoUsage(input, fetchImpl = fetch) {
62
+ const response = await fetchImpl(`https://opencode.ai/workspace/${input.workspaceID}/go`, {
63
+ headers: {
64
+ "user-agent": "Mozilla/5.0",
65
+ cookie: `auth=${input.cookie}`
66
+ },
67
+ redirect: "manual"
68
+ });
69
+ if (response.status >= 300 && response.status < 400) {
70
+ throw new GoUsageError("console redirected to login; the auth cookie may be expired");
71
+ }
72
+ if (!response.ok) {
73
+ throw new GoUsageError(`console request failed with status ${response.status}`);
74
+ }
75
+ const usage = parseGoUsageHtml(await response.text());
76
+ if (!usage) throw new GoUsageError("could not parse go usage from the console page");
77
+ return usage;
78
+ }
79
+ function formatReset(seconds) {
80
+ const total = Math.max(0, Math.floor(seconds));
81
+ const days = Math.floor(total / 86400);
82
+ const hours = Math.floor(total % 86400 / 3600);
83
+ const minutes = Math.floor(total % 3600 / 60);
84
+ if (days > 0) return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
85
+ if (hours > 0) return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
86
+ return `${minutes}m`;
87
+ }
88
+ var BLOCK_PARTIALS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
89
+ function formatUsageBar(usagePercent, width = 10) {
90
+ const percent = Math.max(0, Math.min(100, usagePercent));
91
+ const steps = width * 8;
92
+ const filled = Math.min(steps, Math.max(0, Math.round(percent / 100 * steps)));
93
+ const full = Math.floor(filled / 8);
94
+ const remainder = filled % 8;
95
+ const partial = BLOCK_PARTIALS[remainder] ?? "";
96
+ const empty = Math.max(0, width - full - (partial.length > 0 ? 1 : 0));
97
+ return "\u2588".repeat(full) + partial + "\u2591".repeat(empty);
98
+ }
99
+ function formatGoUsageRow(row) {
100
+ const bar = formatUsageBar(row.usagePercent);
101
+ return `${row.label.padEnd(9)}${`${row.usagePercent}%`.padEnd(3)} ${bar} ${row.reset}`;
102
+ }
103
+ function goUsageSectionVisible(config, usesGoProvider) {
104
+ return usesGoProvider && config.goUsage.enabled && config.goUsage.cookie.length > 0 && config.goUsage.workspaceID.length > 0;
105
+ }
106
+ function createGoProviderTracker() {
107
+ const providers = /* @__PURE__ */ new Map();
108
+ return {
109
+ record(sessionID, providerID) {
110
+ if (providerID) providers.set(sessionID, providerID);
111
+ },
112
+ usesOpenCodeGo(sessionID) {
113
+ return providers.get(sessionID) === "opencode-go";
114
+ }
115
+ };
116
+ }
117
+ function goUsageRows(state, now) {
118
+ if (!state.data) return void 0;
119
+ const elapsedSeconds = state.lastFetchAt === void 0 ? 0 : Math.max(0, (now - state.lastFetchAt) / 1e3);
120
+ return [
121
+ {
122
+ label: "Rolling",
123
+ usagePercent: state.data.rollingUsage.usagePercent,
124
+ reset: formatReset(state.data.rollingUsage.resetInSec - elapsedSeconds)
125
+ },
126
+ {
127
+ label: "Weekly",
128
+ usagePercent: state.data.weeklyUsage.usagePercent,
129
+ reset: formatReset(state.data.weeklyUsage.resetInSec - elapsedSeconds)
130
+ },
131
+ {
132
+ label: "Monthly",
133
+ usagePercent: state.data.monthlyUsage.usagePercent,
134
+ reset: formatReset(state.data.monthlyUsage.resetInSec - elapsedSeconds)
135
+ }
136
+ ];
137
+ }
138
+ function createGoUsageRefresher(config, fetchImpl = fetch) {
139
+ const state = {};
140
+ let inflight;
141
+ async function refresh(now = Date.now()) {
142
+ if (inflight) {
143
+ await inflight;
144
+ return false;
145
+ }
146
+ if (state.lastFetchAt !== void 0 && now - state.lastFetchAt < config.refreshMs) return false;
147
+ inflight = (async () => {
148
+ try {
149
+ state.data = await fetchGoUsage({ cookie: config.cookie, workspaceID: config.workspaceID }, fetchImpl);
150
+ state.error = void 0;
151
+ } catch (error) {
152
+ state.error = error instanceof Error ? error.message : String(error);
153
+ } finally {
154
+ state.lastFetchAt = now;
155
+ inflight = void 0;
156
+ }
157
+ })();
158
+ await inflight;
159
+ return true;
160
+ }
161
+ return { state, refresh };
162
+ }
163
+
40
164
  // src/tui.tsx
41
165
  import { Fragment, jsx, jsxs } from "@opentui/solid/jsx-runtime";
42
166
  function isSessionID(value) {
@@ -119,6 +243,74 @@ function TokenUsageSidebar(props) {
119
243
  }
120
244
  );
121
245
  }
246
+ function GoUsageSidebar(props) {
247
+ let text;
248
+ const [collapsed, setCollapsed] = createSignal(false);
249
+ const titleAttributes = createTextAttributes({ bold: true });
250
+ let previous;
251
+ const toggle = (event) => {
252
+ if (!text || event.y !== text.y) return;
253
+ setCollapsed((prev) => !prev);
254
+ sync();
255
+ };
256
+ const sync = () => {
257
+ if (!text) return;
258
+ const visible = goUsageSectionVisible(props.config, props.tracker.usesOpenCodeGo(props.sessionID));
259
+ if (visible) props.refresh();
260
+ const rows = visible ? goUsageRows(props.state, Date.now()) : void 0;
261
+ const error = props.state.error;
262
+ const showContent = visible && (rows || error);
263
+ const signature = showContent ? JSON.stringify({ collapsed: collapsed(), rows, error }) : "";
264
+ const next = {
265
+ content: signature,
266
+ visible: signature.length > 0,
267
+ height: signature.length > 0 ? "auto" : 0
268
+ };
269
+ if (!hasRenderStateChanged(previous, next)) return;
270
+ previous = next;
271
+ text.visible = next.visible;
272
+ text.height = next.height;
273
+ text.content = showContent ? renderGoUsageSidebar(rows, error, props.api, titleAttributes, collapsed()) : "";
274
+ props.api.renderer.requestRender();
275
+ };
276
+ const unsubscribe = props.subscribe(sync);
277
+ const unsubscribeGoUsage = props.goUsageSubscribe(sync);
278
+ const timer = setInterval(sync, 1e3);
279
+ onCleanup(() => {
280
+ unsubscribe();
281
+ unsubscribeGoUsage();
282
+ clearInterval(timer);
283
+ });
284
+ return /* @__PURE__ */ jsx(
285
+ "text",
286
+ {
287
+ ref: (ref) => {
288
+ text = ref;
289
+ sync();
290
+ },
291
+ onMouseDown: toggle,
292
+ fg: props.api.theme.current.textMuted,
293
+ children: ""
294
+ }
295
+ );
296
+ }
297
+ function renderGoUsageSidebar(rows, error, api, titleAttributes, collapsed) {
298
+ const chunks = [
299
+ textChunk(`${collapsed ? "\u25B6" : "\u25BC"} Go Usage
300
+ `, api.theme.current.text, titleAttributes)
301
+ ];
302
+ if (!collapsed) {
303
+ if (rows && rows.length > 0) {
304
+ for (const [index, row] of rows.entries()) {
305
+ if (index > 0) chunks.push(textChunk("\n"));
306
+ chunks.push(textChunk(formatGoUsageRow(row), api.theme.current.textMuted));
307
+ }
308
+ } else if (error) {
309
+ chunks.push(textChunk(`Go usage: ${error}`, api.theme.current.error));
310
+ }
311
+ }
312
+ return new StyledText(chunks);
313
+ }
122
314
  function SubagentSidebar(props) {
123
315
  let text;
124
316
  const [collapsed, setCollapsed] = createSignal(false);
@@ -238,7 +430,13 @@ var tui = async (api, options) => {
238
430
  const subagents = createSubagentState();
239
431
  const metricListeners = createListenerRegistry();
240
432
  const subagentListeners = createListenerRegistry();
433
+ const goUsageListeners = createListenerRegistry();
241
434
  const hydratedSessions = /* @__PURE__ */ new Set();
435
+ const goProviderTracker = createGoProviderTracker();
436
+ const goUsage = createGoUsageRefresher(config.goUsage);
437
+ const refreshGoUsage = async () => {
438
+ if (await goUsage.refresh()) goUsageListeners.notify();
439
+ };
242
440
  const hydrateSessionMetrics = async (sessionID) => {
243
441
  if (!isSessionID(sessionID) || hydratedSessions.has(sessionID)) return;
244
442
  hydratedSessions.add(sessionID);
@@ -247,6 +445,8 @@ var tui = async (api, options) => {
247
445
  const messages = response.data ?? [];
248
446
  for (const message of messages) {
249
447
  const info = message.info;
448
+ const providerID = info.providerID;
449
+ goProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
250
450
  if (info.role !== "assistant" || typeof info.time.completed !== "number") continue;
251
451
  const input = {
252
452
  sessionID: info.sessionID,
@@ -279,6 +479,9 @@ var tui = async (api, options) => {
279
479
  const offMessage = api.event.on("message.updated", (evt) => {
280
480
  const info = evt.properties.info;
281
481
  if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
482
+ const sessionID = info.sessionID ?? evt.properties.sessionID;
483
+ const providerID = info.providerID ?? info.model?.providerID;
484
+ goProviderTracker.record(sessionID, typeof providerID === "string" ? providerID : void 0);
282
485
  if (info.role !== "assistant") return;
283
486
  const messageInput = {
284
487
  sessionID: info.sessionID ?? evt.properties.sessionID,
@@ -308,6 +511,13 @@ var tui = async (api, options) => {
308
511
  });
309
512
  const offSessionUpdated = api.event.on("session.updated", (evt) => {
310
513
  if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
514
+ const info = evt.properties.info;
515
+ const sessionID = typeof info?.id === "string" ? info.id : void 0;
516
+ const providerID = info?.model?.providerID;
517
+ if (sessionID && typeof providerID === "string") {
518
+ goProviderTracker.record(sessionID, providerID);
519
+ metricListeners.notify();
520
+ }
311
521
  });
312
522
  const offSessionStatus = api.event.on("session.status", (evt) => {
313
523
  if (applySubagentEvent(subagents, evt)) subagentListeners.notify();
@@ -347,6 +557,19 @@ var tui = async (api, options) => {
347
557
  hydrate: () => void hydrateSessionMetrics(props.session_id)
348
558
  }
349
559
  ),
560
+ /* @__PURE__ */ jsx(
561
+ GoUsageSidebar,
562
+ {
563
+ api,
564
+ state: goUsage.state,
565
+ config,
566
+ tracker: goProviderTracker,
567
+ sessionID: props.session_id,
568
+ subscribe: metricListeners.subscribe,
569
+ goUsageSubscribe: goUsageListeners.subscribe,
570
+ refresh: () => void refreshGoUsage()
571
+ }
572
+ ),
350
573
  /* @__PURE__ */ jsx(
351
574
  SubagentSidebar,
352
575
  {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@rejacky/opencode-insights",
4
- "version": "0.1.12",
4
+ "version": "0.2.0",
5
5
  "description": "OpenCode plugin for local request capture, TPS metrics, and subagent status visibility.",
6
6
  "type": "module",
7
7
  "author": "opencode-insights contributors",