opencode-manager 0.4.0 → 0.4.2

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.
@@ -7,13 +7,7 @@
7
7
 
8
8
  import { Command, type OptionValues } from "commander"
9
9
  import { parseGlobalOptions, type GlobalOptions } from "../index"
10
- import {
11
- computeGlobalTokenSummary,
12
- computeProjectTokenSummary,
13
- computeSessionTokenSummary,
14
- loadProjectRecords,
15
- loadSessionRecords,
16
- } from "../../lib/opencode-data"
10
+ import { createProviderFromGlobalOptions } from "../../lib/opencode-data-provider"
17
11
  import { getOutputOptions, printAggregateTokensOutput, printTokensOutput } from "../output"
18
12
  import { handleError } from "../errors"
19
13
  import { findProjectById, findSessionById } from "../resolvers"
@@ -101,6 +95,16 @@ export function registerTokensCommands(parent: Command): void {
101
95
  handleError(error, globalOpts.format)
102
96
  }
103
97
  })
98
+
99
+ tokens.addHelpText(
100
+ "after",
101
+ [
102
+ "",
103
+ "Examples:",
104
+ " opencode-manager tokens session --session <id> --experimental-sqlite",
105
+ " opencode-manager tokens global --db ~/.local/share/opencode/opencode.db",
106
+ ].join("\n")
107
+ )
104
108
  }
105
109
 
106
110
  /**
@@ -110,14 +114,17 @@ async function handleTokensSession(
110
114
  globalOpts: GlobalOptions,
111
115
  sessionOpts: TokensSessionOptions
112
116
  ): Promise<void> {
117
+ // Create provider based on global options (JSONL or SQLite)
118
+ const provider = createProviderFromGlobalOptions(globalOpts)
119
+
113
120
  // Load all sessions to find the one we want
114
- const sessions = await loadSessionRecords({ root: globalOpts.root })
121
+ const sessions = await provider.loadSessionRecords()
115
122
 
116
123
  // Find the session by ID
117
124
  const session = findSessionById(sessions, sessionOpts.session)
118
125
 
119
126
  // Compute token summary for the session
120
- const summary = await computeSessionTokenSummary(session, globalOpts.root)
127
+ const summary = await provider.computeSessionTokenSummary(session)
121
128
 
122
129
  // Output the result
123
130
  const outputOpts = getOutputOptions(globalOpts)
@@ -131,20 +138,22 @@ async function handleTokensProject(
131
138
  globalOpts: GlobalOptions,
132
139
  projectOpts: TokensProjectOptions
133
140
  ): Promise<void> {
141
+ // Create provider based on global options (JSONL or SQLite)
142
+ const provider = createProviderFromGlobalOptions(globalOpts)
143
+
134
144
  // Load all projects to validate the project exists
135
- const projects = await loadProjectRecords({ root: globalOpts.root })
145
+ const projects = await provider.loadProjectRecords()
136
146
 
137
147
  // Find the project by ID (throws if not found)
138
148
  findProjectById(projects, projectOpts.project)
139
149
 
140
150
  // Load all sessions to compute token summary
141
- const sessions = await loadSessionRecords({ root: globalOpts.root })
151
+ const sessions = await provider.loadSessionRecords()
142
152
 
143
153
  // Compute token summary for the project
144
- const summary = await computeProjectTokenSummary(
154
+ const summary = await provider.computeProjectTokenSummary(
145
155
  projectOpts.project,
146
- sessions,
147
- globalOpts.root
156
+ sessions
148
157
  )
149
158
 
150
159
  // Output the result
@@ -156,11 +165,14 @@ async function handleTokensProject(
156
165
  * Handle the tokens global command.
157
166
  */
158
167
  async function handleTokensGlobal(globalOpts: GlobalOptions): Promise<void> {
168
+ // Create provider based on global options (JSONL or SQLite)
169
+ const provider = createProviderFromGlobalOptions(globalOpts)
170
+
159
171
  // Load all sessions to compute global token summary
160
- const sessions = await loadSessionRecords({ root: globalOpts.root })
172
+ const sessions = await provider.loadSessionRecords()
161
173
 
162
174
  // Compute token summary across all sessions
163
- const summary = await computeGlobalTokenSummary(sessions, globalOpts.root)
175
+ const summary = await provider.computeGlobalTokenSummary(sessions)
164
176
 
165
177
  // Output the result
166
178
  const outputOpts = getOutputOptions(globalOpts)
package/src/cli/index.ts CHANGED
@@ -51,6 +51,14 @@ export interface GlobalOptions {
51
51
  clipboard: boolean
52
52
  /** Directory for backup copies before deletion */
53
53
  backupDir?: string
54
+ /** Use SQLite database instead of JSONL files (experimental) */
55
+ experimentalSqlite: boolean
56
+ /** Path to SQLite database (implies --experimental-sqlite) */
57
+ dbPath?: string
58
+ /** Fail fast on any SQLite error or malformed data */
59
+ sqliteStrict: boolean
60
+ /** Wait for SQLite write locks to clear before failing */
61
+ forceWrite: boolean
54
62
  }
55
63
 
56
64
  /**
@@ -66,6 +74,10 @@ export const DEFAULT_OPTIONS: GlobalOptions = {
66
74
  quiet: false,
67
75
  clipboard: false,
68
76
  backupDir: undefined,
77
+ experimentalSqlite: false,
78
+ dbPath: undefined,
79
+ sqliteStrict: false,
80
+ forceWrite: false,
69
81
  }
70
82
 
71
83
  /**
@@ -73,11 +85,12 @@ export const DEFAULT_OPTIONS: GlobalOptions = {
73
85
  */
74
86
  function createProgram(): Command {
75
87
  const program = new Command()
88
+ program.configureHelp({ showGlobalOptions: true })
76
89
 
77
90
  program
78
91
  .name("opencode-manager")
79
92
  .description("CLI for managing OpenCode metadata stores")
80
- .version("0.4.0")
93
+ .version("0.4.2")
81
94
  // Global options
82
95
  .option(
83
96
  "-r, --root <path>",
@@ -108,6 +121,25 @@ function createProgram(): Command {
108
121
  .option("-q, --quiet", "Suppress non-essential output", DEFAULT_OPTIONS.quiet)
109
122
  .option("-c, --clipboard", "Copy output to clipboard", DEFAULT_OPTIONS.clipboard)
110
123
  .option("--backup-dir <path>", "Directory for backup copies before deletion")
124
+ .option(
125
+ "--experimental-sqlite",
126
+ "Use SQLite database instead of JSONL files (experimental; schema may change)",
127
+ DEFAULT_OPTIONS.experimentalSqlite
128
+ )
129
+ .option(
130
+ "--db <path>",
131
+ "Path to SQLite database (implies --experimental-sqlite). Default: ~/.local/share/opencode/opencode.db"
132
+ )
133
+ .option(
134
+ "--sqlite-strict",
135
+ "Fail on any SQLite warning or malformed data (no partial results)",
136
+ DEFAULT_OPTIONS.sqliteStrict
137
+ )
138
+ .option(
139
+ "--force-write",
140
+ "Wait for SQLite write locks to clear before failing",
141
+ DEFAULT_OPTIONS.forceWrite
142
+ )
111
143
 
112
144
  // Projects subcommand group
113
145
  registerProjectsCommands(program)
@@ -132,6 +164,10 @@ function createProgram(): Command {
132
164
  * Resolves paths and converts types as needed.
133
165
  */
134
166
  export function parseGlobalOptions(opts: Record<string, unknown>): GlobalOptions {
167
+ // --db implies --experimental-sqlite
168
+ const dbPath = opts.db ? resolve(String(opts.db)) : undefined
169
+ const experimentalSqlite = Boolean(opts.experimentalSqlite) || dbPath !== undefined
170
+
135
171
  return {
136
172
  root: resolve(String(opts.root ?? DEFAULT_OPTIONS.root)),
137
173
  format: validateFormat(String(opts.format ?? DEFAULT_OPTIONS.format)),
@@ -142,6 +178,10 @@ export function parseGlobalOptions(opts: Record<string, unknown>): GlobalOptions
142
178
  quiet: Boolean(opts.quiet ?? DEFAULT_OPTIONS.quiet),
143
179
  clipboard: Boolean(opts.clipboard ?? DEFAULT_OPTIONS.clipboard),
144
180
  backupDir: opts.backupDir ? resolve(String(opts.backupDir)) : undefined,
181
+ experimentalSqlite,
182
+ dbPath,
183
+ sqliteStrict: Boolean(opts.sqliteStrict ?? DEFAULT_OPTIONS.sqliteStrict),
184
+ forceWrite: Boolean(opts.forceWrite ?? DEFAULT_OPTIONS.forceWrite),
145
185
  }
146
186
  }
147
187
 
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * These helpers provide consistent ID resolution across all CLI commands,
5
5
  * supporting both exact matches and flexible matching patterns.
6
+ *
7
+ * Resolvers can optionally accept a DataProvider to support both JSONL and
8
+ * SQLite backends. When no provider is given, they fall back to direct JSONL
9
+ * loading for backward compatibility.
6
10
  */
7
11
 
8
12
  import {
@@ -13,6 +17,7 @@ import {
13
17
  type ProjectRecord,
14
18
  type SessionRecord,
15
19
  } from "../lib/opencode-data"
20
+ import { type DataProvider } from "../lib/opencode-data-provider"
16
21
  import { NotFoundError, projectNotFound, sessionNotFound } from "./errors"
17
22
 
18
23
  // ========================
@@ -29,6 +34,13 @@ export interface ResolveSessionOptions extends SessionLoadOptions {
29
34
  * Defaults to false.
30
35
  */
31
36
  allowPrefix?: boolean
37
+
38
+ /**
39
+ * Optional data provider for backend-agnostic data loading.
40
+ * When provided, uses the provider's loadSessionRecords method.
41
+ * When omitted, falls back to direct JSONL loading for backward compatibility.
42
+ */
43
+ provider?: DataProvider
32
44
  }
33
45
 
34
46
  /**
@@ -81,7 +93,7 @@ export function findSessionsByPrefix(
81
93
  * Supports exact matching and optional prefix matching.
82
94
  *
83
95
  * @param sessionId - Session ID or prefix to resolve
84
- * @param options - Resolution options including root and projectId filters
96
+ * @param options - Resolution options including root, projectId filters, and optional provider
85
97
  * @returns Resolution result with session and metadata
86
98
  * @throws NotFoundError if no session matches
87
99
  * @throws NotFoundError if prefix matches multiple sessions (ambiguous)
@@ -90,10 +102,13 @@ export async function resolveSessionId(
90
102
  sessionId: string,
91
103
  options: ResolveSessionOptions = {}
92
104
  ): Promise<ResolveSessionResult> {
93
- const sessions = await loadSessionRecords({
94
- root: options.root,
95
- projectId: options.projectId,
96
- })
105
+ // Use provider if available, otherwise fall back to direct JSONL loading
106
+ const sessions = options.provider
107
+ ? await options.provider.loadSessionRecords({ projectId: options.projectId })
108
+ : await loadSessionRecords({
109
+ root: options.root,
110
+ projectId: options.projectId,
111
+ })
97
112
 
98
113
  // Try exact match first
99
114
  const exactMatch = sessions.find((s) => s.sessionId === sessionId)
@@ -146,6 +161,13 @@ export interface ResolveProjectOptions extends LoadOptions {
146
161
  * Defaults to false.
147
162
  */
148
163
  allowPrefix?: boolean
164
+
165
+ /**
166
+ * Optional data provider for backend-agnostic data loading.
167
+ * When provided, uses the provider's loadProjectRecords method.
168
+ * When omitted, falls back to direct JSONL loading for backward compatibility.
169
+ */
170
+ provider?: DataProvider
149
171
  }
150
172
 
151
173
  /**
@@ -198,7 +220,7 @@ export function findProjectsByPrefix(
198
220
  * Supports exact matching and optional prefix matching.
199
221
  *
200
222
  * @param projectId - Project ID or prefix to resolve
201
- * @param options - Resolution options including root
223
+ * @param options - Resolution options including root and optional provider
202
224
  * @returns Resolution result with project and metadata
203
225
  * @throws NotFoundError if no project matches
204
226
  * @throws NotFoundError if prefix matches multiple projects (ambiguous)
@@ -207,9 +229,12 @@ export async function resolveProjectId(
207
229
  projectId: string,
208
230
  options: ResolveProjectOptions = {}
209
231
  ): Promise<ResolveProjectResult> {
210
- const projects = await loadProjectRecords({
211
- root: options.root,
212
- })
232
+ // Use provider if available, otherwise fall back to direct JSONL loading
233
+ const projects = options.provider
234
+ ? await options.provider.loadProjectRecords()
235
+ : await loadProjectRecords({
236
+ root: options.root,
237
+ })
213
238
 
214
239
  // Try exact match first
215
240
  const exactMatch = projects.find((p) => p.projectId === projectId)