@kodrunhq/opencode-autopilot 1.5.0 → 1.6.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,24 @@
1
+ export { createMemoryCaptureHandler, type MemoryCaptureDeps } from "./capture";
2
+ export * from "./constants";
3
+ export { closeMemoryDb, getMemoryDb, initMemoryDb } from "./database";
4
+ export { computeRelevanceScore, pruneStaleObservations } from "./decay";
5
+ export { createMemoryInjector, type MemoryInjectorConfig } from "./injector";
6
+ export { computeProjectKey } from "./project-key";
7
+ export {
8
+ deleteObservation,
9
+ getAllPreferences,
10
+ getObservationsByProject,
11
+ getProjectByPath,
12
+ insertObservation,
13
+ searchObservations,
14
+ updateAccessCount,
15
+ upsertPreference,
16
+ upsertProject,
17
+ } from "./repository";
18
+ export {
19
+ buildMemoryContext,
20
+ retrieveMemoryContext,
21
+ type ScoredObservation,
22
+ scoreAndRankObservations,
23
+ } from "./retrieval";
24
+ export * from "./types";
@@ -0,0 +1,85 @@
1
+ /**
2
+ * System prompt injection for memory context.
3
+ *
4
+ * Creates a per-session cached injector that retrieves relevant memories
5
+ * and pushes them into the system prompt via output.system.
6
+ *
7
+ * Best-effort: all errors are silently caught to never break the session.
8
+ * Same pattern as skill-injection.ts.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import type { Database } from "bun:sqlite";
14
+ import { retrieveMemoryContext } from "./retrieval";
15
+
16
+ /**
17
+ * Configuration for creating a memory injector.
18
+ */
19
+ export interface MemoryInjectorConfig {
20
+ readonly projectRoot: string;
21
+ readonly tokenBudget: number;
22
+ readonly halfLifeDays: number;
23
+ readonly getDb: () => Database;
24
+ }
25
+
26
+ /**
27
+ * Input shape matching the experimental.chat.system.transform hook signature.
28
+ */
29
+ interface InjectorInput {
30
+ readonly sessionID?: string;
31
+ readonly model: Record<string, unknown>;
32
+ }
33
+
34
+ /**
35
+ * Output shape matching the experimental.chat.system.transform hook signature.
36
+ * system is mutable — the hook API expects callers to push into it.
37
+ */
38
+ interface InjectorOutput {
39
+ system: string[];
40
+ }
41
+
42
+ /**
43
+ * Create a memory injector function for the experimental.chat.system.transform hook.
44
+ *
45
+ * Returns an async function that:
46
+ * 1. Skips if no sessionID is provided
47
+ * 2. Returns cached context for known sessions
48
+ * 3. Retrieves and caches memory context for new sessions
49
+ * 4. Pushes non-empty context to output.system
50
+ *
51
+ * All errors are silently caught (best-effort, per D-24 and skill-injection pattern).
52
+ */
53
+ export function createMemoryInjector(config: MemoryInjectorConfig) {
54
+ const cache = new Map<string, string>();
55
+
56
+ return async (input: InjectorInput, output: InjectorOutput): Promise<void> => {
57
+ try {
58
+ if (!input.sessionID) return;
59
+
60
+ const cached = cache.get(input.sessionID);
61
+ if (cached !== undefined) {
62
+ if (cached.length > 0) {
63
+ output.system.push(cached);
64
+ }
65
+ return;
66
+ }
67
+
68
+ const db = config.getDb();
69
+ const context = retrieveMemoryContext(
70
+ config.projectRoot,
71
+ config.tokenBudget,
72
+ db,
73
+ config.halfLifeDays,
74
+ );
75
+
76
+ cache.set(input.sessionID, context);
77
+
78
+ if (context.length > 0) {
79
+ output.system.push(context);
80
+ }
81
+ } catch (err) {
82
+ console.warn("[opencode-autopilot] memory injection failed:", err);
83
+ }
84
+ };
85
+ }
@@ -0,0 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function computeProjectKey(projectPath: string): string {
4
+ return createHash("sha256").update(projectPath).digest("hex");
5
+ }
@@ -0,0 +1,217 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { OBSERVATION_TYPES } from "./constants";
3
+ import { getMemoryDb } from "./database";
4
+ import { observationSchema, preferenceSchema, projectSchema } from "./schemas";
5
+ import type { Observation, ObservationType, Preference, Project } from "./types";
6
+
7
+ /** Resolve optional db parameter to singleton fallback. */
8
+ function resolveDb(db?: Database): Database {
9
+ return db ?? getMemoryDb();
10
+ }
11
+
12
+ /** Validate observation type at runtime. */
13
+ function parseObservationType(value: unknown): ObservationType {
14
+ if (typeof value === "string" && (OBSERVATION_TYPES as readonly string[]).includes(value)) {
15
+ return value as ObservationType;
16
+ }
17
+ return "context"; // safe fallback for corrupt/unknown types
18
+ }
19
+
20
+ /** Map a snake_case DB row to camelCase Observation. */
21
+ function rowToObservation(row: Record<string, unknown>): Observation {
22
+ return {
23
+ id: row.id as number,
24
+ projectId: (row.project_id as string) ?? null,
25
+ sessionId: row.session_id as string,
26
+ type: parseObservationType(row.type),
27
+ content: row.content as string,
28
+ summary: row.summary as string,
29
+ confidence: row.confidence as number,
30
+ accessCount: row.access_count as number,
31
+ createdAt: row.created_at as string,
32
+ lastAccessed: row.last_accessed as string,
33
+ };
34
+ }
35
+
36
+ /** Map a snake_case DB row to camelCase Project. */
37
+ function rowToProject(row: Record<string, unknown>): Project {
38
+ return {
39
+ id: row.id as string,
40
+ path: row.path as string,
41
+ name: row.name as string,
42
+ lastUpdated: row.last_updated as string,
43
+ };
44
+ }
45
+
46
+ /** Map a snake_case DB row to camelCase Preference. */
47
+ function rowToPreference(row: Record<string, unknown>): Preference {
48
+ return {
49
+ id: row.id as string,
50
+ key: row.key as string,
51
+ value: row.value as string,
52
+ confidence: row.confidence as number,
53
+ sourceSession: (row.source_session as string) ?? null,
54
+ createdAt: row.created_at as string,
55
+ lastUpdated: row.last_updated as string,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Insert an observation. Validates via Zod before writing.
61
+ * Returns the observation with the generated id.
62
+ */
63
+ export function insertObservation(obs: Omit<Observation, "id">, db?: Database): Observation {
64
+ const validated = observationSchema.omit({ id: true }).parse(obs);
65
+ const d = resolveDb(db);
66
+
67
+ d.run(
68
+ `INSERT INTO observations (project_id, session_id, type, content, summary, confidence, access_count, created_at, last_accessed)
69
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
70
+ [
71
+ validated.projectId,
72
+ validated.sessionId,
73
+ validated.type,
74
+ validated.content,
75
+ validated.summary,
76
+ validated.confidence,
77
+ validated.accessCount,
78
+ validated.createdAt,
79
+ validated.lastAccessed,
80
+ ],
81
+ );
82
+
83
+ const row = d.query("SELECT last_insert_rowid() as id").get() as { id: number };
84
+ return { ...validated, id: row.id };
85
+ }
86
+
87
+ /**
88
+ * Search observations using FTS5 MATCH with BM25 ranking.
89
+ * Filters by projectId (null for user-level observations).
90
+ */
91
+ export function searchObservations(
92
+ query: string,
93
+ projectId: string | null,
94
+ limit = 20,
95
+ db?: Database,
96
+ ): Array<Observation & { ftsRank: number }> {
97
+ const d = resolveDb(db);
98
+
99
+ const projectFilter = projectId === null ? "AND o.project_id IS NULL" : "AND o.project_id = ?";
100
+
101
+ // Sanitize FTS5 query — wrap in double-quotes to prevent operator injection
102
+ const safeFtsQuery = `"${query.replace(/"/g, '""')}"`;
103
+ const params: Array<string | number> =
104
+ projectId === null ? [safeFtsQuery, limit] : [safeFtsQuery, projectId, limit];
105
+
106
+ const rows = d
107
+ .query(
108
+ `SELECT o.*, bm25(observations_fts) as fts_rank
109
+ FROM observations_fts f
110
+ JOIN observations o ON o.id = f.rowid
111
+ WHERE observations_fts MATCH ?
112
+ ${projectFilter}
113
+ ORDER BY fts_rank
114
+ LIMIT ?`,
115
+ )
116
+ .all(...params) as Array<Record<string, unknown>>;
117
+
118
+ return rows.map((row) => ({
119
+ ...rowToObservation(row),
120
+ ftsRank: row.fts_rank as number,
121
+ }));
122
+ }
123
+
124
+ /**
125
+ * Create or replace a project record.
126
+ */
127
+ export function upsertProject(project: Project, db?: Database): void {
128
+ const validated = projectSchema.parse(project);
129
+ const d = resolveDb(db);
130
+ d.run(`INSERT OR REPLACE INTO projects (id, path, name, last_updated) VALUES (?, ?, ?, ?)`, [
131
+ validated.id,
132
+ validated.path,
133
+ validated.name,
134
+ validated.lastUpdated,
135
+ ]);
136
+ }
137
+
138
+ /**
139
+ * Get a project by its filesystem path. Returns null if not found.
140
+ */
141
+ export function getProjectByPath(path: string, db?: Database): Project | null {
142
+ const d = resolveDb(db);
143
+ const row = d.query("SELECT * FROM projects WHERE path = ?").get(path) as Record<
144
+ string,
145
+ unknown
146
+ > | null;
147
+ return row ? rowToProject(row) : null;
148
+ }
149
+
150
+ /**
151
+ * Get observations filtered by project_id, ordered by created_at DESC.
152
+ */
153
+ export function getObservationsByProject(
154
+ projectId: string | null,
155
+ limit = 50,
156
+ db?: Database,
157
+ ): readonly Observation[] {
158
+ const d = resolveDb(db);
159
+
160
+ const whereClause = projectId === null ? "WHERE project_id IS NULL" : "WHERE project_id = ?";
161
+ const params: Array<string | number> = projectId === null ? [limit] : [projectId, limit];
162
+
163
+ const rows = d
164
+ .query(`SELECT * FROM observations ${whereClause} ORDER BY created_at DESC LIMIT ?`)
165
+ .all(...params) as Array<Record<string, unknown>>;
166
+
167
+ return rows.map(rowToObservation);
168
+ }
169
+
170
+ /**
171
+ * Create or replace a preference by its id.
172
+ */
173
+ export function upsertPreference(pref: Preference, db?: Database): void {
174
+ const validated = preferenceSchema.parse(pref);
175
+ const d = resolveDb(db);
176
+ d.run(
177
+ `INSERT OR REPLACE INTO preferences (id, key, value, confidence, source_session, created_at, last_updated)
178
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
179
+ [
180
+ validated.id,
181
+ validated.key,
182
+ validated.value,
183
+ validated.confidence,
184
+ validated.sourceSession,
185
+ validated.createdAt,
186
+ validated.lastUpdated,
187
+ ],
188
+ );
189
+ }
190
+
191
+ /**
192
+ * Get all preferences.
193
+ */
194
+ export function getAllPreferences(db?: Database): readonly Preference[] {
195
+ const d = resolveDb(db);
196
+ const rows = d.query("SELECT * FROM preferences").all() as Array<Record<string, unknown>>;
197
+ return rows.map(rowToPreference);
198
+ }
199
+
200
+ /**
201
+ * Delete an observation by id.
202
+ */
203
+ export function deleteObservation(id: number, db?: Database): void {
204
+ const d = resolveDb(db);
205
+ d.run("DELETE FROM observations WHERE id = ?", [id]);
206
+ }
207
+
208
+ /**
209
+ * Increment access_count and update last_accessed for an observation.
210
+ */
211
+ export function updateAccessCount(id: number, db?: Database): void {
212
+ const d = resolveDb(db);
213
+ d.run("UPDATE observations SET access_count = access_count + 1, last_accessed = ? WHERE id = ?", [
214
+ new Date().toISOString(),
215
+ id,
216
+ ]);
217
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * 3-layer progressive disclosure retrieval with token-budgeted context building.
3
+ *
4
+ * Layer 1 (always): Observation summaries grouped by type (up to 5 per group)
5
+ * Layer 2 (if budget allows): Recent Activity timeline
6
+ * Layer 3 (if budget allows): Full content for top 1-2 observations
7
+ *
8
+ * Token budget enforcement: never exceeds CHARS_PER_TOKEN * tokenBudget characters.
9
+ * Same approach as buildMultiSkillContext in src/skills/adaptive-injector.ts.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import type { Database } from "bun:sqlite";
15
+ import { CHARS_PER_TOKEN, DEFAULT_INJECTION_BUDGET } from "./constants";
16
+ import { computeRelevanceScore } from "./decay";
17
+ import { getAllPreferences, getObservationsByProject, getProjectByPath } from "./repository";
18
+ import type { Observation, Preference } from "./types";
19
+
20
+ /**
21
+ * An observation with its computed relevance score.
22
+ */
23
+ export type ScoredObservation = Observation & { readonly relevanceScore: number };
24
+
25
+ /**
26
+ * Score and rank observations by relevance (descending).
27
+ */
28
+ export function scoreAndRankObservations(
29
+ observations: readonly Observation[],
30
+ halfLifeDays?: number,
31
+ ): readonly ScoredObservation[] {
32
+ return observations
33
+ .map((obs) => ({
34
+ ...obs,
35
+ relevanceScore: computeRelevanceScore(
36
+ obs.lastAccessed,
37
+ obs.accessCount,
38
+ obs.type,
39
+ halfLifeDays,
40
+ ),
41
+ }))
42
+ .sort((a, b) => b.relevanceScore - a.relevanceScore);
43
+ }
44
+
45
+ /**
46
+ * Type-to-section header mapping for Layer 1 grouping.
47
+ */
48
+ const SECTION_HEADERS: Readonly<Record<string, string>> = Object.freeze({
49
+ decision: "### Key Decisions",
50
+ pattern: "### Patterns",
51
+ error: "### Recent Errors",
52
+ preference: "### Learned Preferences",
53
+ context: "### Context Notes",
54
+ tool_usage: "### Tool Usage Patterns",
55
+ });
56
+
57
+ /** Section display order (most valuable first). */
58
+ const SECTION_ORDER = ["decision", "pattern", "error", "preference", "context", "tool_usage"];
59
+
60
+ /** Max observations per group in Layer 1. */
61
+ const MAX_PER_GROUP = 5;
62
+
63
+ /** Minimum chars remaining to include Layer 2. */
64
+ const LAYER_2_THRESHOLD = 500;
65
+
66
+ /** Minimum chars remaining to include Layer 3. */
67
+ const LAYER_3_THRESHOLD = 1000;
68
+
69
+ /**
70
+ * Options for building memory context.
71
+ */
72
+ interface BuildMemoryContextOptions {
73
+ readonly projectName: string;
74
+ readonly lastSessionDate: string | null;
75
+ readonly observations: readonly ScoredObservation[];
76
+ readonly preferences: readonly Preference[];
77
+ readonly tokenBudget?: number;
78
+ }
79
+
80
+ /**
81
+ * Build a markdown memory context string within token budget.
82
+ *
83
+ * Uses 3-layer progressive disclosure:
84
+ * - Layer 1: Summaries grouped by type (always included if budget allows)
85
+ * - Layer 2: Recent Activity timeline (if remaining budget > 500 chars)
86
+ * - Layer 3: Full content for top observations (if remaining budget > 1000 chars)
87
+ */
88
+ export function buildMemoryContext(options: BuildMemoryContextOptions): string {
89
+ const {
90
+ projectName,
91
+ lastSessionDate,
92
+ observations,
93
+ preferences,
94
+ tokenBudget = DEFAULT_INJECTION_BUDGET,
95
+ } = options;
96
+
97
+ if (observations.length === 0 && preferences.length === 0) return "";
98
+
99
+ const charBudget = tokenBudget * CHARS_PER_TOKEN;
100
+ let totalChars = 0;
101
+ const parts: string[] = [];
102
+
103
+ // Header
104
+ const header = `## Project Memory (auto-injected)\n**Project:** ${projectName}\n**Last session:** ${lastSessionDate ?? "first session"}\n`;
105
+ if (totalChars + header.length > charBudget) {
106
+ return header.slice(0, charBudget);
107
+ }
108
+ parts.push(header);
109
+ totalChars += header.length;
110
+
111
+ // --- Layer 1: Grouped summaries ---
112
+ // Sort by relevance within the function to ensure highest-first in each group
113
+ const sorted = [...observations].sort((a, b) => b.relevanceScore - a.relevanceScore);
114
+ const grouped = groupByType(sorted);
115
+
116
+ for (const type of SECTION_ORDER) {
117
+ const group = grouped.get(type);
118
+ if (!group || group.length === 0) continue;
119
+
120
+ const sectionHeader = SECTION_HEADERS[type] ?? `### ${type}`;
121
+ const items = group.slice(0, MAX_PER_GROUP);
122
+ const lines = items.map((obs) => `- ${obs.summary} (confidence: ${obs.confidence})`);
123
+ const section = `\n${sectionHeader}\n${lines.join("\n")}\n`;
124
+
125
+ if (totalChars + section.length > charBudget) break;
126
+ parts.push(section);
127
+ totalChars += section.length;
128
+ }
129
+
130
+ // Preferences section
131
+ if (preferences.length > 0) {
132
+ const prefLines = preferences.map((p) => `- **${p.key}:** ${p.value}`);
133
+ const prefSection = `\n### Preferences\n${prefLines.join("\n")}\n`;
134
+
135
+ if (totalChars + prefSection.length <= charBudget) {
136
+ parts.push(prefSection);
137
+ totalChars += prefSection.length;
138
+ }
139
+ }
140
+
141
+ // --- Layer 2: Recent Activity timeline (if budget allows) ---
142
+ const remainingAfterL1 = charBudget - totalChars;
143
+ if (remainingAfterL1 > LAYER_2_THRESHOLD && observations.length > 0) {
144
+ const timeline = buildTimeline(observations);
145
+ if (timeline.length > 0) {
146
+ const timelineSection = `\n### Recent Activity\n${timeline}\n`;
147
+ if (totalChars + timelineSection.length <= charBudget) {
148
+ parts.push(timelineSection);
149
+ totalChars += timelineSection.length;
150
+ }
151
+ }
152
+ }
153
+
154
+ // --- Layer 3: Full content for top observations (if budget allows) ---
155
+ const remainingAfterL2 = charBudget - totalChars;
156
+ if (remainingAfterL2 > LAYER_3_THRESHOLD && observations.length > 0) {
157
+ const topObs = observations.slice(0, 2);
158
+ const detailLines: string[] = [];
159
+ const headerOverhead = "\n### Details\n\n".length;
160
+ let linesBudget = remainingAfterL2 - headerOverhead;
161
+
162
+ for (const obs of topObs) {
163
+ const detail = `**${obs.type}:** ${obs.content}`;
164
+ const cost = detail.length + 1;
165
+ if (cost > linesBudget) break;
166
+ detailLines.push(detail);
167
+ linesBudget -= cost;
168
+ }
169
+
170
+ if (detailLines.length > 0) {
171
+ const detailSection = `\n### Details\n${detailLines.join("\n")}\n`;
172
+ if (totalChars + detailSection.length <= charBudget) {
173
+ parts.push(detailSection);
174
+ totalChars += detailSection.length;
175
+ }
176
+ }
177
+ }
178
+
179
+ const result = parts.join("");
180
+ // Final safety truncation
181
+ return result.length > charBudget ? result.slice(0, charBudget) : result;
182
+ }
183
+
184
+ /**
185
+ * Group scored observations by type, preserving relevance order within groups.
186
+ */
187
+ function groupByType(
188
+ observations: readonly ScoredObservation[],
189
+ ): ReadonlyMap<string, readonly ScoredObservation[]> {
190
+ const groups = new Map<string, ScoredObservation[]>();
191
+
192
+ for (const obs of observations) {
193
+ const existing = groups.get(obs.type);
194
+ if (existing) {
195
+ existing.push(obs);
196
+ } else {
197
+ groups.set(obs.type, [obs]);
198
+ }
199
+ }
200
+
201
+ return groups;
202
+ }
203
+
204
+ /**
205
+ * Build a brief timeline of recent sessions from observations.
206
+ */
207
+ function buildTimeline(observations: readonly ScoredObservation[]): string {
208
+ // Group by session, take last 5 sessions
209
+ const sessions = new Map<string, { date: string; count: number }>();
210
+
211
+ for (const obs of observations) {
212
+ const existing = sessions.get(obs.sessionId);
213
+ if (existing) {
214
+ existing.count++;
215
+ if (new Date(obs.createdAt).getTime() > new Date(existing.date).getTime()) {
216
+ existing.date = obs.createdAt;
217
+ }
218
+ } else {
219
+ sessions.set(obs.sessionId, { date: obs.createdAt, count: 1 });
220
+ }
221
+ }
222
+
223
+ const sorted = [...sessions.values()]
224
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
225
+ .slice(0, 5);
226
+
227
+ return sorted
228
+ .map((s) => {
229
+ const dateStr = s.date.split("T")[0];
230
+ return `- ${dateStr}: ${s.count} observation${s.count !== 1 ? "s" : ""}`;
231
+ })
232
+ .join("\n");
233
+ }
234
+
235
+ /**
236
+ * Convenience function: retrieve memory context for a project path.
237
+ *
238
+ * Ties together: project lookup, observation retrieval, scoring, preferences, and context building.
239
+ */
240
+ export function retrieveMemoryContext(
241
+ projectPath: string,
242
+ tokenBudget?: number,
243
+ db?: Database,
244
+ halfLifeDays?: number,
245
+ ): string {
246
+ const project = getProjectByPath(projectPath, db);
247
+ if (!project) return "";
248
+
249
+ const observations = getObservationsByProject(project.id, 100, db);
250
+ const scored = scoreAndRankObservations(observations, halfLifeDays);
251
+ const preferences = getAllPreferences(db);
252
+
253
+ return buildMemoryContext({
254
+ projectName: project.name,
255
+ lastSessionDate: project.lastUpdated,
256
+ observations: scored,
257
+ preferences,
258
+ tokenBudget,
259
+ });
260
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { OBSERVATION_TYPES } from "./constants";
3
+
4
+ export const observationTypeSchema = z.enum(OBSERVATION_TYPES);
5
+
6
+ export const observationSchema = z.object({
7
+ id: z.number().int().optional(),
8
+ projectId: z.string().nullable(),
9
+ sessionId: z.string(),
10
+ type: observationTypeSchema,
11
+ content: z.string().min(1).max(10000),
12
+ summary: z.string().min(1).max(500),
13
+ confidence: z.number().min(0).max(1).default(0.5),
14
+ accessCount: z.number().int().min(0).default(0),
15
+ createdAt: z.string(),
16
+ lastAccessed: z.string(),
17
+ });
18
+
19
+ export const projectSchema = z.object({
20
+ id: z.string(),
21
+ path: z.string(),
22
+ name: z.string(),
23
+ lastUpdated: z.string(),
24
+ });
25
+
26
+ export const preferenceSchema = z.object({
27
+ id: z.string(),
28
+ key: z.string().min(1).max(200),
29
+ value: z.string().min(1).max(2000),
30
+ confidence: z.number().min(0).max(1).default(0.5),
31
+ sourceSession: z.string().nullable().default(null),
32
+ createdAt: z.string(),
33
+ lastUpdated: z.string(),
34
+ });
@@ -0,0 +1,12 @@
1
+ import type { z } from "zod";
2
+ import type {
3
+ observationSchema,
4
+ observationTypeSchema,
5
+ preferenceSchema,
6
+ projectSchema,
7
+ } from "./schemas";
8
+
9
+ export type ObservationType = z.infer<typeof observationTypeSchema>;
10
+ export type Observation = z.infer<typeof observationSchema>;
11
+ export type Project = z.infer<typeof projectSchema>;
12
+ export type Preference = z.infer<typeof preferenceSchema>;
@@ -314,7 +314,7 @@ async function handleCommit(configPath?: string): Promise<string> {
314
314
  }
315
315
  const newConfig = {
316
316
  ...currentConfig,
317
- version: 4 as const,
317
+ version: 5 as const,
318
318
  configured: true,
319
319
  groups: groupsRecord,
320
320
  overrides: currentConfig.overrides ?? {},