oc-tweaks 0.11.0 → 0.11.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.
- package/dist/cli/init.js +313 -6
- package/dist/index.js +3712 -3487
- package/package.json +1 -1
package/dist/cli/init.js
CHANGED
|
@@ -4,6 +4,284 @@
|
|
|
4
4
|
// src/cli/init.ts
|
|
5
5
|
import { mkdir } from "fs/promises";
|
|
6
6
|
import { dirname } from "path";
|
|
7
|
+
|
|
8
|
+
// src/plugins/auto-memory/diag.ts
|
|
9
|
+
import { statSync } from "node:fs";
|
|
10
|
+
import { join as join2 } from "node:path";
|
|
11
|
+
|
|
12
|
+
// src/plugins/auto-memory/registry.ts
|
|
13
|
+
import { closeSync, openSync, readdirSync, readSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
// src/plugins/auto-memory/frontmatter.ts
|
|
17
|
+
class MemoryFrontmatterParseError extends Error {
|
|
18
|
+
cause;
|
|
19
|
+
constructor(message, cause) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.cause = cause;
|
|
22
|
+
this.name = "MemoryFrontmatterParseError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
var DEFAULT_META = {
|
|
26
|
+
id: "",
|
|
27
|
+
scope: "global",
|
|
28
|
+
type: "note",
|
|
29
|
+
source: "user",
|
|
30
|
+
created_at: "",
|
|
31
|
+
updated_at: "",
|
|
32
|
+
trusted_as_instruction: false
|
|
33
|
+
};
|
|
34
|
+
function parseMinimalYaml(yaml) {
|
|
35
|
+
const result = {};
|
|
36
|
+
for (const rawLine of yaml.split(`
|
|
37
|
+
`)) {
|
|
38
|
+
const line = rawLine.trim();
|
|
39
|
+
if (!line || line.startsWith("#"))
|
|
40
|
+
continue;
|
|
41
|
+
const colonIdx = line.indexOf(":");
|
|
42
|
+
if (colonIdx === -1)
|
|
43
|
+
continue;
|
|
44
|
+
const key = line.slice(0, colonIdx).trim();
|
|
45
|
+
const rawVal = line.slice(colonIdx + 1).trim();
|
|
46
|
+
if (!key)
|
|
47
|
+
continue;
|
|
48
|
+
if (/[\[{]/.test(rawVal) && !/[\]}]/.test(rawVal)) {
|
|
49
|
+
throw new MemoryFrontmatterParseError(`Invalid YAML value for key "${key}": unclosed bracket/brace`);
|
|
50
|
+
}
|
|
51
|
+
result[key] = parseYamlValue(rawVal);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function parseYamlValue(raw) {
|
|
56
|
+
if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
|
|
57
|
+
return raw.slice(1, -1);
|
|
58
|
+
}
|
|
59
|
+
if (raw === "false")
|
|
60
|
+
return false;
|
|
61
|
+
if (raw === "true")
|
|
62
|
+
return true;
|
|
63
|
+
if (/^-?\d+$/.test(raw))
|
|
64
|
+
return parseInt(raw, 10);
|
|
65
|
+
return raw;
|
|
66
|
+
}
|
|
67
|
+
function parseFrontmatter(raw) {
|
|
68
|
+
const stripped = raw.startsWith("\uFEFF") ? raw.slice(1) : raw;
|
|
69
|
+
if (!stripped.startsWith("---")) {
|
|
70
|
+
return { meta: { ...DEFAULT_META }, body: raw };
|
|
71
|
+
}
|
|
72
|
+
const afterOpen = stripped.slice(3);
|
|
73
|
+
const closeIdx = afterOpen.indexOf(`
|
|
74
|
+
---`);
|
|
75
|
+
if (closeIdx === -1) {
|
|
76
|
+
return { meta: { ...DEFAULT_META }, body: raw };
|
|
77
|
+
}
|
|
78
|
+
const yamlBlock = afterOpen.slice(0, closeIdx);
|
|
79
|
+
const afterClose = afterOpen.slice(closeIdx + 4);
|
|
80
|
+
const body = afterClose.startsWith(`
|
|
81
|
+
`) ? afterClose.slice(1) : afterClose;
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = parseMinimalYaml(yamlBlock);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (e instanceof MemoryFrontmatterParseError)
|
|
87
|
+
throw e;
|
|
88
|
+
throw new MemoryFrontmatterParseError("Failed to parse frontmatter YAML", e);
|
|
89
|
+
}
|
|
90
|
+
const meta = {
|
|
91
|
+
...DEFAULT_META,
|
|
92
|
+
...parsed.id !== undefined && { id: String(parsed.id) },
|
|
93
|
+
...parsed.scope !== undefined && { scope: parsed.scope },
|
|
94
|
+
...parsed.type !== undefined && { type: String(parsed.type) },
|
|
95
|
+
...parsed.source !== undefined && { source: String(parsed.source) },
|
|
96
|
+
...parsed.created_at !== undefined && { created_at: String(parsed.created_at) },
|
|
97
|
+
...parsed.updated_at !== undefined && { updated_at: String(parsed.updated_at) },
|
|
98
|
+
trusted_as_instruction: false,
|
|
99
|
+
...parsed.summary !== undefined && { summary: String(parsed.summary) },
|
|
100
|
+
...parsed.disabled !== undefined && { disabled: Boolean(parsed.disabled) },
|
|
101
|
+
...parsed.usage_count !== undefined && { usage_count: Number(parsed.usage_count) },
|
|
102
|
+
...parsed.last_usage !== undefined && { last_usage: String(parsed.last_usage) }
|
|
103
|
+
};
|
|
104
|
+
return { meta, body };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/plugins/auto-memory/registry.ts
|
|
108
|
+
var SKIP_PATTERNS = [
|
|
109
|
+
/^readme/i,
|
|
110
|
+
/^\.ds_store$/i,
|
|
111
|
+
/\.swp$/,
|
|
112
|
+
/\.lock$/,
|
|
113
|
+
/^\.gitignore$/,
|
|
114
|
+
/^\.gitkeep$/
|
|
115
|
+
];
|
|
116
|
+
function shouldSkip(filename) {
|
|
117
|
+
return SKIP_PATTERNS.some((p) => p.test(filename));
|
|
118
|
+
}
|
|
119
|
+
var MAX_READ_BYTES = 2048;
|
|
120
|
+
function readPartial(absPath) {
|
|
121
|
+
const fd = openSync(absPath, "r");
|
|
122
|
+
try {
|
|
123
|
+
const buf = Buffer.alloc(MAX_READ_BYTES);
|
|
124
|
+
const bytesRead = readSync(fd, buf, 0, MAX_READ_BYTES, 0);
|
|
125
|
+
return buf.subarray(0, bytesRead).toString("utf8");
|
|
126
|
+
} finally {
|
|
127
|
+
closeSync(fd);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
var MAX_SUMMARY_CHARS = 240;
|
|
131
|
+
var MAX_SUMMARY_LINES = 5;
|
|
132
|
+
function extractSummary(meta, body) {
|
|
133
|
+
if (meta.summary) {
|
|
134
|
+
return meta.summary.slice(0, MAX_SUMMARY_CHARS);
|
|
135
|
+
}
|
|
136
|
+
const lines = body.split(`
|
|
137
|
+
`);
|
|
138
|
+
const collected = [];
|
|
139
|
+
for (const line of lines) {
|
|
140
|
+
if (collected.length >= MAX_SUMMARY_LINES)
|
|
141
|
+
break;
|
|
142
|
+
if (collected.length > 0 && line.trim() === "")
|
|
143
|
+
break;
|
|
144
|
+
collected.push(line);
|
|
145
|
+
}
|
|
146
|
+
return collected.join(`
|
|
147
|
+
`).slice(0, MAX_SUMMARY_CHARS);
|
|
148
|
+
}
|
|
149
|
+
function scanDir(dir, scope) {
|
|
150
|
+
const map = new Map;
|
|
151
|
+
let filenames;
|
|
152
|
+
try {
|
|
153
|
+
filenames = readdirSync(dir, { withFileTypes: false });
|
|
154
|
+
} catch {
|
|
155
|
+
return map;
|
|
156
|
+
}
|
|
157
|
+
for (const filename of filenames) {
|
|
158
|
+
if (typeof filename !== "string")
|
|
159
|
+
continue;
|
|
160
|
+
if (!filename.endsWith(".md"))
|
|
161
|
+
continue;
|
|
162
|
+
if (shouldSkip(filename))
|
|
163
|
+
continue;
|
|
164
|
+
const absPath = join(dir, filename);
|
|
165
|
+
let partial;
|
|
166
|
+
try {
|
|
167
|
+
partial = readPartial(absPath);
|
|
168
|
+
} catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const { meta, body } = parseFrontmatter(partial);
|
|
172
|
+
if (meta.disabled === true)
|
|
173
|
+
continue;
|
|
174
|
+
const summary = extractSummary(meta, body);
|
|
175
|
+
const tokenEstimate = Math.ceil((summary.length + JSON.stringify(meta).length) / 4);
|
|
176
|
+
const entry = {
|
|
177
|
+
meta,
|
|
178
|
+
absPath,
|
|
179
|
+
scope,
|
|
180
|
+
tokenEstimate,
|
|
181
|
+
summary
|
|
182
|
+
};
|
|
183
|
+
const key = meta.id || filename;
|
|
184
|
+
map.set(key, entry);
|
|
185
|
+
}
|
|
186
|
+
return map;
|
|
187
|
+
}
|
|
188
|
+
function scanMemoryRoots(globalDir, projectDir) {
|
|
189
|
+
const globalMap = scanDir(globalDir, "global");
|
|
190
|
+
const projectMap = scanDir(projectDir, "project");
|
|
191
|
+
const merged = new Map([...globalMap, ...projectMap]);
|
|
192
|
+
return Array.from(merged.values());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/plugins/auto-memory/diag.ts
|
|
196
|
+
function resolveRoots(opts) {
|
|
197
|
+
if (opts.root) {
|
|
198
|
+
return {
|
|
199
|
+
globalDir: join2(opts.root, "global"),
|
|
200
|
+
projectDir: join2(opts.root, "project", ".opencode", "memory")
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
globalDir: opts.globalRoot ?? "",
|
|
205
|
+
projectDir: opts.projectRoot ?? ""
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function fileSizeBytes(absPath) {
|
|
209
|
+
try {
|
|
210
|
+
return statSync(absPath).size;
|
|
211
|
+
} catch {
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function runDiag(opts) {
|
|
216
|
+
const { globalDir, projectDir } = resolveRoots(opts);
|
|
217
|
+
const allEntries = scanMemoryRoots(globalDir, projectDir);
|
|
218
|
+
const globalEntries = allEntries.filter((e) => e.scope === "global");
|
|
219
|
+
const projectEntries = allEntries.filter((e) => e.scope === "project");
|
|
220
|
+
function scopeStats(entries, scope, root) {
|
|
221
|
+
const totalBytes2 = entries.reduce((s, e) => s + fileSizeBytes(e.absPath), 0);
|
|
222
|
+
const estimatedTokens = entries.reduce((s, e) => s + e.tokenEstimate, 0);
|
|
223
|
+
return { scope, root, fileCount: entries.length, totalBytes: totalBytes2, estimatedTokens };
|
|
224
|
+
}
|
|
225
|
+
const scopes = [
|
|
226
|
+
scopeStats(globalEntries, "global", globalDir),
|
|
227
|
+
scopeStats(projectEntries, "project", projectDir)
|
|
228
|
+
];
|
|
229
|
+
const totalBytes = scopes.reduce((s, sc) => s + sc.totalBytes, 0);
|
|
230
|
+
const totalEstimatedTokens = scopes.reduce((s, sc) => s + sc.estimatedTokens, 0);
|
|
231
|
+
const top5ByUsageCount = [...allEntries].sort((a, b) => (b.meta.usage_count ?? 0) - (a.meta.usage_count ?? 0)).slice(0, 5);
|
|
232
|
+
const top5ByUpdatedAt = [...allEntries].sort((a, b) => (b.meta.updated_at ?? "").localeCompare(a.meta.updated_at ?? "")).slice(0, 5);
|
|
233
|
+
return {
|
|
234
|
+
roots: { global: globalDir, project: projectDir },
|
|
235
|
+
scopes,
|
|
236
|
+
totalFiles: allEntries.length,
|
|
237
|
+
totalBytes,
|
|
238
|
+
totalEstimatedTokens,
|
|
239
|
+
top5ByUsageCount,
|
|
240
|
+
top5ByUpdatedAt,
|
|
241
|
+
autoWriteEvents: "no buffer in this session"
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function formatDiagReport(report) {
|
|
245
|
+
const lines = [];
|
|
246
|
+
lines.push("=== memory diag ===");
|
|
247
|
+
lines.push("");
|
|
248
|
+
lines.push("memory roots:");
|
|
249
|
+
lines.push(` global: ${report.roots.global}`);
|
|
250
|
+
lines.push(` project: ${report.roots.project}`);
|
|
251
|
+
lines.push("");
|
|
252
|
+
lines.push("file count per scope:");
|
|
253
|
+
for (const sc of report.scopes) {
|
|
254
|
+
lines.push(` ${sc.scope.padEnd(8)} root=${sc.root} files=${sc.fileCount} bytes=${sc.totalBytes} estimated tokens=${sc.estimatedTokens}`);
|
|
255
|
+
}
|
|
256
|
+
lines.push(` total files=${report.totalFiles} bytes=${report.totalBytes} estimated tokens=${report.totalEstimatedTokens}`);
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push("estimated tokens (total): " + report.totalEstimatedTokens);
|
|
259
|
+
lines.push("");
|
|
260
|
+
lines.push("top 5 by usage_count (desc):");
|
|
261
|
+
if (report.top5ByUsageCount.length === 0) {
|
|
262
|
+
lines.push(" (none)");
|
|
263
|
+
} else {
|
|
264
|
+
for (const e of report.top5ByUsageCount) {
|
|
265
|
+
lines.push(` [${e.scope}] ${e.meta.id || e.absPath} usage_count=${e.meta.usage_count ?? 0} updated_at=${e.meta.updated_at}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
lines.push("");
|
|
269
|
+
lines.push("top 5 by updated_at (desc):");
|
|
270
|
+
if (report.top5ByUpdatedAt.length === 0) {
|
|
271
|
+
lines.push(" (none)");
|
|
272
|
+
} else {
|
|
273
|
+
for (const e of report.top5ByUpdatedAt) {
|
|
274
|
+
lines.push(` [${e.scope}] ${e.meta.id || e.absPath} updated_at=${e.meta.updated_at} usage_count=${e.meta.usage_count ?? 0}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push("auto-write events: " + report.autoWriteEvents);
|
|
279
|
+
return lines.join(`
|
|
280
|
+
`);
|
|
281
|
+
}
|
|
282
|
+
if (false) {}
|
|
283
|
+
|
|
284
|
+
// src/cli/init.ts
|
|
7
285
|
var DEFAULT_CONFIG = {
|
|
8
286
|
notify: { enabled: true },
|
|
9
287
|
compaction: { enabled: true },
|
|
@@ -25,18 +303,47 @@ async function initConfig() {
|
|
|
25
303
|
`);
|
|
26
304
|
return { created: true, path: configPath };
|
|
27
305
|
}
|
|
306
|
+
function parseCliArgs(argv) {
|
|
307
|
+
const args = argv[0] === "tweaks" ? argv.slice(1) : argv;
|
|
308
|
+
if (args.length === 0 || args[0] === "init")
|
|
309
|
+
return { command: "init" };
|
|
310
|
+
if (args[0] === "memory" && args[1] === "diag") {
|
|
311
|
+
return { command: "memory-diag", opts: parseDiagOpts(args.slice(2)) };
|
|
312
|
+
}
|
|
313
|
+
return { command: "init" };
|
|
314
|
+
}
|
|
315
|
+
function parseDiagOpts(args) {
|
|
316
|
+
const opts = {};
|
|
317
|
+
for (let i = 0;i < args.length; i++) {
|
|
318
|
+
const arg = args[i];
|
|
319
|
+
if (arg === "--root" && args[i + 1])
|
|
320
|
+
opts.root = args[++i];
|
|
321
|
+
else if (arg === "--global-root" && args[i + 1])
|
|
322
|
+
opts.globalRoot = args[++i];
|
|
323
|
+
else if (arg === "--project-root" && args[i + 1])
|
|
324
|
+
opts.projectRoot = args[++i];
|
|
325
|
+
}
|
|
326
|
+
return opts;
|
|
327
|
+
}
|
|
28
328
|
var isMain = typeof Bun !== "undefined" && Bun.main === import.meta.path;
|
|
29
329
|
if (isMain) {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
console.log(
|
|
330
|
+
const parsed = parseCliArgs(process.argv.slice(2));
|
|
331
|
+
if (parsed.command === "memory-diag") {
|
|
332
|
+
const report = await runDiag(parsed.opts);
|
|
333
|
+
console.log(formatDiagReport(report));
|
|
34
334
|
} else {
|
|
35
|
-
|
|
36
|
-
|
|
335
|
+
const result = await initConfig();
|
|
336
|
+
if (result.created) {
|
|
337
|
+
console.log(`Created: ${result.path}`);
|
|
338
|
+
console.log("All plugins configured. Edit the file to customize.");
|
|
339
|
+
} else {
|
|
340
|
+
console.log(`Config already exists: ${result.path}`);
|
|
341
|
+
console.log("Nothing changed. Edit the file manually to update your configuration.");
|
|
342
|
+
}
|
|
37
343
|
}
|
|
38
344
|
}
|
|
39
345
|
export {
|
|
346
|
+
parseCliArgs,
|
|
40
347
|
initConfig,
|
|
41
348
|
DEFAULT_CONFIG
|
|
42
349
|
};
|