opencodekit 0.18.3 → 0.18.5
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/index.js +407 -17
- package/dist/template/.opencode/.version +1 -1
- package/dist/template/.opencode/AGENTS.md +13 -1
- package/dist/template/.opencode/agent/build.md +4 -1
- package/dist/template/.opencode/agent/explore.md +5 -35
- package/dist/template/.opencode/command/verify.md +63 -12
- package/dist/template/.opencode/memory/research/benchmark-framework.md +162 -0
- package/dist/template/.opencode/memory/research/effectiveness-audit.md +213 -0
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +1429 -1678
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/lib/memory-helpers.ts +3 -129
- package/dist/template/.opencode/plugin/lib/memory-hooks.ts +4 -60
- package/dist/template/.opencode/plugin/memory.ts +0 -3
- package/dist/template/.opencode/skill/agent-teams/SKILL.md +16 -1
- package/dist/template/.opencode/skill/beads/SKILL.md +22 -0
- package/dist/template/.opencode/skill/brainstorming/SKILL.md +28 -0
- package/dist/template/.opencode/skill/code-navigation/SKILL.md +130 -0
- package/dist/template/.opencode/skill/condition-based-waiting/SKILL.md +12 -0
- package/dist/template/.opencode/skill/context-management/SKILL.md +122 -113
- package/dist/template/.opencode/skill/defense-in-depth/SKILL.md +20 -0
- package/dist/template/.opencode/skill/design-system-audit/SKILL.md +113 -112
- package/dist/template/.opencode/skill/dispatching-parallel-agents/SKILL.md +8 -0
- package/dist/template/.opencode/skill/executing-plans/SKILL.md +7 -0
- package/dist/template/.opencode/skill/memory-system/SKILL.md +50 -266
- package/dist/template/.opencode/skill/mockup-to-code/SKILL.md +21 -6
- package/dist/template/.opencode/skill/receiving-code-review/SKILL.md +8 -0
- package/dist/template/.opencode/skill/requesting-code-review/SKILL.md +242 -105
- package/dist/template/.opencode/skill/root-cause-tracing/SKILL.md +15 -0
- package/dist/template/.opencode/skill/session-management/SKILL.md +4 -103
- package/dist/template/.opencode/skill/subagent-driven-development/SKILL.md +23 -2
- package/dist/template/.opencode/skill/swarm-coordination/SKILL.md +17 -1
- package/dist/template/.opencode/skill/systematic-debugging/SKILL.md +21 -0
- package/dist/template/.opencode/skill/tool-priority/SKILL.md +34 -16
- package/dist/template/.opencode/skill/ui-ux-research/SKILL.md +5 -127
- package/dist/template/.opencode/skill/verification-before-completion/SKILL.md +36 -0
- package/dist/template/.opencode/skill/verification-before-completion/references/VERIFICATION_PROTOCOL.md +133 -29
- package/dist/template/.opencode/skill/visual-analysis/SKILL.md +20 -7
- package/dist/template/.opencode/skill/writing-plans/SKILL.md +7 -0
- package/dist/template/.opencode/tool/context7.ts +9 -1
- package/dist/template/.opencode/tool/grepsearch.ts +9 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { cac } from "cac";
|
|
4
4
|
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
5
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import * as p from "@clack/prompts";
|
|
7
7
|
import color from "picocolors";
|
|
8
8
|
import { z } from "zod";
|
|
@@ -18,7 +18,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
18
18
|
|
|
19
19
|
//#endregion
|
|
20
20
|
//#region package.json
|
|
21
|
-
var version = "0.18.
|
|
21
|
+
var version = "0.18.5";
|
|
22
22
|
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/utils/errors.ts
|
|
@@ -153,6 +153,15 @@ const ToolActionSchema = z.enum([
|
|
|
153
153
|
"delete"
|
|
154
154
|
]);
|
|
155
155
|
const ToolOptionsSchema = z.object({ json: z.boolean().optional().default(false) });
|
|
156
|
+
const PatchActionSchema = z.enum([
|
|
157
|
+
"list",
|
|
158
|
+
"create",
|
|
159
|
+
"apply",
|
|
160
|
+
"diff",
|
|
161
|
+
"remove",
|
|
162
|
+
"disable",
|
|
163
|
+
"enable"
|
|
164
|
+
]);
|
|
156
165
|
const CompletionShellSchema = z.enum([
|
|
157
166
|
"bash",
|
|
158
167
|
"zsh",
|
|
@@ -2515,6 +2524,22 @@ function getPatchesDir(opencodeDir) {
|
|
|
2515
2524
|
return join(opencodeDir, PATCHES_DIR);
|
|
2516
2525
|
}
|
|
2517
2526
|
/**
|
|
2527
|
+
* Get the template root directory (from dist/template or dev mode).
|
|
2528
|
+
*/
|
|
2529
|
+
function getTemplateRoot$2() {
|
|
2530
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2531
|
+
const possiblePaths = [
|
|
2532
|
+
join(__dirname, "template"),
|
|
2533
|
+
join(__dirname, "..", "..", ".opencode"),
|
|
2534
|
+
join(__dirname, "..", "template")
|
|
2535
|
+
];
|
|
2536
|
+
for (const path of possiblePaths) {
|
|
2537
|
+
if (existsSync(join(path, ".opencode"))) return path;
|
|
2538
|
+
if (existsSync(join(path, "opencode.json"))) return dirname(path);
|
|
2539
|
+
}
|
|
2540
|
+
return null;
|
|
2541
|
+
}
|
|
2542
|
+
/**
|
|
2518
2543
|
* Load patch metadata from .patches.json.
|
|
2519
2544
|
*/
|
|
2520
2545
|
function loadPatchMetadata(opencodeDir) {
|
|
@@ -2581,6 +2606,22 @@ function savePatch(opencodeDir, relativePath, templateContent, userContent) {
|
|
|
2581
2606
|
return entry;
|
|
2582
2607
|
}
|
|
2583
2608
|
/**
|
|
2609
|
+
* Remove a patch for a file.
|
|
2610
|
+
*/
|
|
2611
|
+
function removePatch(opencodeDir, relativePath) {
|
|
2612
|
+
const metadata = loadPatchMetadata(opencodeDir);
|
|
2613
|
+
const entry = metadata.patches[relativePath];
|
|
2614
|
+
if (!entry) return false;
|
|
2615
|
+
const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
|
|
2616
|
+
if (existsSync(patchPath)) {
|
|
2617
|
+
const { rmSync } = __require("node:fs");
|
|
2618
|
+
rmSync(patchPath);
|
|
2619
|
+
}
|
|
2620
|
+
delete metadata.patches[relativePath];
|
|
2621
|
+
savePatchMetadata(opencodeDir, metadata);
|
|
2622
|
+
return true;
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2584
2625
|
* Apply a patch to file content.
|
|
2585
2626
|
* @returns The patched content, or null if patch failed.
|
|
2586
2627
|
*/
|
|
@@ -2595,6 +2636,14 @@ function applyAllPatches(opencodeDir) {
|
|
|
2595
2636
|
const patchesDir = getPatchesDir(opencodeDir);
|
|
2596
2637
|
const results = [];
|
|
2597
2638
|
for (const [relativePath, entry] of Object.entries(metadata.patches)) {
|
|
2639
|
+
if (entry.disabled) {
|
|
2640
|
+
results.push({
|
|
2641
|
+
success: true,
|
|
2642
|
+
file: relativePath,
|
|
2643
|
+
message: "Skipped (disabled)"
|
|
2644
|
+
});
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2598
2647
|
const filePath = join(opencodeDir, relativePath);
|
|
2599
2648
|
const patchPath = join(patchesDir, entry.patchFile);
|
|
2600
2649
|
if (!existsSync(filePath)) {
|
|
@@ -2637,6 +2686,59 @@ function applyAllPatches(opencodeDir) {
|
|
|
2637
2686
|
}
|
|
2638
2687
|
return results;
|
|
2639
2688
|
}
|
|
2689
|
+
/**
|
|
2690
|
+
* Check the status of all patches.
|
|
2691
|
+
*/
|
|
2692
|
+
function checkPatchStatus(opencodeDir, templateRoot) {
|
|
2693
|
+
const metadata = loadPatchMetadata(opencodeDir);
|
|
2694
|
+
const statuses = [];
|
|
2695
|
+
for (const [relativePath, entry] of Object.entries(metadata.patches)) {
|
|
2696
|
+
if (!existsSync(join(opencodeDir, relativePath))) {
|
|
2697
|
+
statuses.push({
|
|
2698
|
+
relativePath,
|
|
2699
|
+
entry,
|
|
2700
|
+
status: "missing",
|
|
2701
|
+
message: "User file no longer exists"
|
|
2702
|
+
});
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2705
|
+
if (templateRoot) {
|
|
2706
|
+
const templateFilePath = join(templateRoot, ".opencode", relativePath);
|
|
2707
|
+
if (existsSync(templateFilePath)) {
|
|
2708
|
+
const templateContent = readFileSync(templateFilePath, "utf-8");
|
|
2709
|
+
if (calculateHash(templateContent) !== entry.originalHash) {
|
|
2710
|
+
const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
|
|
2711
|
+
if (existsSync(patchPath)) if (applyPatch$1(templateContent, readFileSync(patchPath, "utf-8")) === false) statuses.push({
|
|
2712
|
+
relativePath,
|
|
2713
|
+
entry,
|
|
2714
|
+
status: "conflict",
|
|
2715
|
+
message: "Template changed and patch cannot apply cleanly"
|
|
2716
|
+
});
|
|
2717
|
+
else statuses.push({
|
|
2718
|
+
relativePath,
|
|
2719
|
+
entry,
|
|
2720
|
+
status: "stale",
|
|
2721
|
+
message: "Template changed but patch can still apply"
|
|
2722
|
+
});
|
|
2723
|
+
else statuses.push({
|
|
2724
|
+
relativePath,
|
|
2725
|
+
entry,
|
|
2726
|
+
status: "missing",
|
|
2727
|
+
message: "Patch file missing"
|
|
2728
|
+
});
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
statuses.push({
|
|
2734
|
+
relativePath,
|
|
2735
|
+
entry,
|
|
2736
|
+
status: "clean",
|
|
2737
|
+
message: "Patch is up to date"
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
return statuses;
|
|
2741
|
+
}
|
|
2640
2742
|
|
|
2641
2743
|
//#endregion
|
|
2642
2744
|
//#region src/commands/init.ts
|
|
@@ -2721,15 +2823,15 @@ const MODEL_PRESETS = {
|
|
|
2721
2823
|
}
|
|
2722
2824
|
},
|
|
2723
2825
|
recommend: {
|
|
2724
|
-
model: "
|
|
2826
|
+
model: "github-copilot/gpt-5.4",
|
|
2725
2827
|
agents: {
|
|
2726
2828
|
build: "github-copilot/claude-opus-4.6",
|
|
2727
|
-
plan: "
|
|
2728
|
-
review: "
|
|
2729
|
-
explore: "
|
|
2730
|
-
general: "github-copilot/
|
|
2731
|
-
vision: "
|
|
2732
|
-
scout: "
|
|
2829
|
+
plan: "github-copilot/gpt-5.4",
|
|
2830
|
+
review: "github-copilot/claude-opus-4.6",
|
|
2831
|
+
explore: "github-copilot/claude-haiku-4.5",
|
|
2832
|
+
general: "github-copilot/gpt-5.3-codex",
|
|
2833
|
+
vision: "github-copilot/gemini-3.1-pro-preview",
|
|
2834
|
+
scout: "github-copilot/claude-sonnet-4.6",
|
|
2733
2835
|
painter: "proxypal/gemini-3.1-flash-image"
|
|
2734
2836
|
}
|
|
2735
2837
|
}
|
|
@@ -2751,7 +2853,7 @@ const AGENT_DESCRIPTIONS = {
|
|
|
2751
2853
|
review: "Code review and debugging",
|
|
2752
2854
|
explore: "Fast codebase search",
|
|
2753
2855
|
general: "Quick, simple tasks",
|
|
2754
|
-
|
|
2856
|
+
painter: "Image generation and editing",
|
|
2755
2857
|
vision: "Visual analysis (quality)",
|
|
2756
2858
|
scout: "External research/docs",
|
|
2757
2859
|
compaction: "Context summarization"
|
|
@@ -2760,11 +2862,11 @@ async function promptCustomModels(targetDir) {
|
|
|
2760
2862
|
const configPath = join(targetDir, ".opencode", "opencode.json");
|
|
2761
2863
|
if (!existsSync(configPath)) return;
|
|
2762
2864
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
2763
|
-
p.log.info(color.dim("Enter model IDs (e.g.,
|
|
2865
|
+
p.log.info(color.dim("Enter model IDs (e.g., github-copilot/gpt-5.4, proxypal/gemini-3.1-flash-image)"));
|
|
2764
2866
|
p.log.info(color.dim("Press Enter to keep current value\n"));
|
|
2765
2867
|
const mainModel = await p.text({
|
|
2766
2868
|
message: "Main session model",
|
|
2767
|
-
placeholder: config.model || "
|
|
2869
|
+
placeholder: config.model || "github-copilot/gpt-5.4",
|
|
2768
2870
|
defaultValue: config.model
|
|
2769
2871
|
});
|
|
2770
2872
|
if (p.isCancel(mainModel)) {
|
|
@@ -3033,7 +3135,7 @@ async function initCommand(rawOptions = {}) {
|
|
|
3033
3135
|
{
|
|
3034
3136
|
value: "recommend",
|
|
3035
3137
|
label: "Recommended models",
|
|
3036
|
-
hint: "
|
|
3138
|
+
hint: "gpt-5.4, opus-4.6, sonnet-4.6, gemini-3.1"
|
|
3037
3139
|
},
|
|
3038
3140
|
{
|
|
3039
3141
|
value: "custom",
|
|
@@ -3498,7 +3600,7 @@ function copyDirWithPreserve(src, dest, preserveFiles, preserveDirs, manifest, b
|
|
|
3498
3600
|
const destPath = join(dest, entry.name);
|
|
3499
3601
|
if (entry.isDirectory()) if (preserveDirs.includes(entry.name)) {
|
|
3500
3602
|
if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
|
|
3501
|
-
const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, entry.name);
|
|
3603
|
+
const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, dest, entry.name);
|
|
3502
3604
|
added.push(...subResult.added);
|
|
3503
3605
|
updated.push(...subResult.updated);
|
|
3504
3606
|
preserved.push(...subResult.preserved);
|
|
@@ -3524,7 +3626,7 @@ function copyDirWithPreserve(src, dest, preserveFiles, preserveDirs, manifest, b
|
|
|
3524
3626
|
preserved
|
|
3525
3627
|
};
|
|
3526
3628
|
}
|
|
3527
|
-
function copyDirPreserveExisting(src, dest, manifest, basePath = "") {
|
|
3629
|
+
function copyDirPreserveExisting(src, dest, manifest, opencodeDir, basePath = "") {
|
|
3528
3630
|
const added = [];
|
|
3529
3631
|
const updated = [];
|
|
3530
3632
|
const preserved = [];
|
|
@@ -3534,7 +3636,7 @@ function copyDirPreserveExisting(src, dest, manifest, basePath = "") {
|
|
|
3534
3636
|
const destPath = join(dest, entry.name);
|
|
3535
3637
|
if (entry.isDirectory()) {
|
|
3536
3638
|
if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
|
|
3537
|
-
const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, join(basePath, entry.name));
|
|
3639
|
+
const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, opencodeDir, join(basePath, entry.name));
|
|
3538
3640
|
added.push(...subResult.added);
|
|
3539
3641
|
updated.push(...subResult.updated);
|
|
3540
3642
|
preserved.push(...subResult.preserved);
|
|
@@ -3546,6 +3648,9 @@ function copyDirPreserveExisting(src, dest, manifest, basePath = "") {
|
|
|
3546
3648
|
} else if (fileModificationStatus(destPath, relativePath, manifest) === "unmodified") {
|
|
3547
3649
|
copyFileSync(srcPath, destPath);
|
|
3548
3650
|
updated.push(relativePath);
|
|
3651
|
+
} else if (loadPatchMetadata(opencodeDir).patches[relativePath]) {
|
|
3652
|
+
copyFileSync(srcPath, destPath);
|
|
3653
|
+
updated.push(relativePath);
|
|
3549
3654
|
} else preserved.push(relativePath);
|
|
3550
3655
|
}
|
|
3551
3656
|
}
|
|
@@ -3646,7 +3751,10 @@ async function upgradeCommand(rawOptions = {}) {
|
|
|
3646
3751
|
}
|
|
3647
3752
|
if (result.updated.length > 0) p.log.success(`Updated ${result.updated.length} files`);
|
|
3648
3753
|
if (result.added.length > 0) p.log.success(`Added ${result.added.length} files`);
|
|
3649
|
-
if (result.preserved.length > 0)
|
|
3754
|
+
if (result.preserved.length > 0) {
|
|
3755
|
+
p.log.info(`Preserved ${result.preserved.length} user files`);
|
|
3756
|
+
p.log.info(color.dim(" Tip: Run 'ock patch create <file>' to save customizations as reapplyable patches"));
|
|
3757
|
+
}
|
|
3650
3758
|
if (patchResults.success > 0) p.log.success(`Reapplied ${patchResults.success} patches`);
|
|
3651
3759
|
const orphans = findUpgradeOrphans(getAllFiles(opencodeDir), getAllFiles(templateOpencode));
|
|
3652
3760
|
if (orphans.length > 0) {
|
|
@@ -4053,6 +4161,285 @@ async function statusCommand() {
|
|
|
4053
4161
|
p.outro(color.dim(".opencode/"));
|
|
4054
4162
|
}
|
|
4055
4163
|
|
|
4164
|
+
//#endregion
|
|
4165
|
+
//#region src/commands/patch.ts
|
|
4166
|
+
function getOpencodeDir() {
|
|
4167
|
+
const opencodeDir = join(resolve("."), ".opencode");
|
|
4168
|
+
if (!existsSync(opencodeDir)) {
|
|
4169
|
+
notInitialized();
|
|
4170
|
+
return null;
|
|
4171
|
+
}
|
|
4172
|
+
return opencodeDir;
|
|
4173
|
+
}
|
|
4174
|
+
function listPatches(opencodeDir) {
|
|
4175
|
+
const metadata = loadPatchMetadata(opencodeDir);
|
|
4176
|
+
const entries = Object.entries(metadata.patches);
|
|
4177
|
+
if (entries.length === 0) {
|
|
4178
|
+
showEmpty("patches", "ock patch create <file>");
|
|
4179
|
+
return;
|
|
4180
|
+
}
|
|
4181
|
+
const statuses = checkPatchStatus(opencodeDir, getTemplateRoot$2());
|
|
4182
|
+
const statusMap = new Map(statuses.map((s) => [s.relativePath, s]));
|
|
4183
|
+
p.intro(color.bgCyan(color.black(` ${entries.length} patch${entries.length === 1 ? "" : "es"} `)));
|
|
4184
|
+
for (const [relativePath, entry] of entries) {
|
|
4185
|
+
const ps = statusMap.get(relativePath);
|
|
4186
|
+
const statusLabel = ps ? formatStatus(ps.status) : color.dim("unknown");
|
|
4187
|
+
const disabledLabel = entry.disabled ? color.yellow(" [disabled]") : "";
|
|
4188
|
+
const descLabel = entry.description ? color.dim(` — ${entry.description}`) : "";
|
|
4189
|
+
p.log.info(`${color.cyan(relativePath)}${disabledLabel}${descLabel}\n Status: ${statusLabel} Created: ${color.dim(entry.createdAt.slice(0, 10))} Version: ${color.dim(entry.templateVersion)}`);
|
|
4190
|
+
}
|
|
4191
|
+
p.outro(color.dim("Use 'ock patch diff <file>' to view changes"));
|
|
4192
|
+
}
|
|
4193
|
+
function formatStatus(status) {
|
|
4194
|
+
switch (status) {
|
|
4195
|
+
case "clean": return color.green("clean");
|
|
4196
|
+
case "stale": return color.yellow("stale");
|
|
4197
|
+
case "conflict": return color.red("conflict");
|
|
4198
|
+
case "missing": return color.red("missing");
|
|
4199
|
+
default: return color.dim(status);
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
async function createPatch$1(opencodeDir) {
|
|
4203
|
+
const fileArg = process.argv[4];
|
|
4204
|
+
if (!fileArg) {
|
|
4205
|
+
p.log.error("Usage: ock patch create <file>");
|
|
4206
|
+
p.log.info(color.dim("File path is relative to .opencode/ (e.g., skill/beads/SKILL.md)"));
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
const relativePath = fileArg;
|
|
4210
|
+
const userFilePath = join(opencodeDir, relativePath);
|
|
4211
|
+
if (!existsSync(userFilePath)) {
|
|
4212
|
+
notFound("file", relativePath);
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
const templateRoot = getTemplateRoot$2();
|
|
4216
|
+
if (!templateRoot) {
|
|
4217
|
+
p.log.error("Cannot find template root — unable to compute diff");
|
|
4218
|
+
p.log.info(color.dim("Make sure ock is installed correctly"));
|
|
4219
|
+
return;
|
|
4220
|
+
}
|
|
4221
|
+
const templateFilePath = join(templateRoot, ".opencode", relativePath);
|
|
4222
|
+
if (!existsSync(templateFilePath)) {
|
|
4223
|
+
p.log.error(`No template file for ${color.cyan(relativePath)}`);
|
|
4224
|
+
p.log.info(color.dim("Only template-originated files can be patched"));
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
const templateContent = readFileSync(templateFilePath, "utf-8");
|
|
4228
|
+
const userContent = readFileSync(userFilePath, "utf-8");
|
|
4229
|
+
if (calculateHash(templateContent) === calculateHash(userContent)) {
|
|
4230
|
+
p.log.warn(`${color.cyan(relativePath)} is identical to template — nothing to patch`);
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
if (loadPatchMetadata(opencodeDir).patches[relativePath]) {
|
|
4234
|
+
const overwrite = await p.confirm({
|
|
4235
|
+
message: `Patch already exists for ${color.cyan(relativePath)}. Overwrite?`,
|
|
4236
|
+
initialValue: false
|
|
4237
|
+
});
|
|
4238
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
4239
|
+
p.cancel("Cancelled");
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
const description = await p.text({
|
|
4244
|
+
message: "Description (optional)",
|
|
4245
|
+
placeholder: "e.g., Custom agent prompt for our team"
|
|
4246
|
+
});
|
|
4247
|
+
if (p.isCancel(description)) {
|
|
4248
|
+
p.cancel("Cancelled");
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
const entry = savePatch(opencodeDir, relativePath, templateContent, userContent);
|
|
4252
|
+
if (description && typeof description === "string" && description.trim()) {
|
|
4253
|
+
entry.description = description.trim();
|
|
4254
|
+
const updatedMetadata = loadPatchMetadata(opencodeDir);
|
|
4255
|
+
updatedMetadata.patches[relativePath] = entry;
|
|
4256
|
+
savePatchMetadata(opencodeDir, updatedMetadata);
|
|
4257
|
+
}
|
|
4258
|
+
p.log.success(`Created patch for ${color.cyan(relativePath)}`);
|
|
4259
|
+
p.log.info(color.dim(`Patch file: ${entry.patchFile}`));
|
|
4260
|
+
}
|
|
4261
|
+
function applyPatches(opencodeDir) {
|
|
4262
|
+
const fileArg = process.argv[4];
|
|
4263
|
+
const metadata = loadPatchMetadata(opencodeDir);
|
|
4264
|
+
if (Object.entries(metadata.patches).length === 0) {
|
|
4265
|
+
showEmpty("patches", "ock patch create <file>");
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
if (fileArg) {
|
|
4269
|
+
const entry = metadata.patches[fileArg];
|
|
4270
|
+
if (!entry) {
|
|
4271
|
+
notFound("patch", fileArg);
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
if (entry.disabled) {
|
|
4275
|
+
p.log.warn(`Patch for ${color.cyan(fileArg)} is disabled — enable it first`);
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
applySinglePatch(opencodeDir, fileArg, entry);
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
const results = applyAllPatches(opencodeDir);
|
|
4282
|
+
const success = results.filter((r) => r.success && r.message !== "Skipped (disabled)").length;
|
|
4283
|
+
const skipped = results.filter((r) => r.message === "Skipped (disabled)").length;
|
|
4284
|
+
const conflicts = results.filter((r) => r.conflict).length;
|
|
4285
|
+
if (success > 0) p.log.success(`Applied ${success} patch${success === 1 ? "" : "es"}`);
|
|
4286
|
+
if (skipped > 0) p.log.info(color.dim(`Skipped ${skipped} disabled patch${skipped === 1 ? "" : "es"}`));
|
|
4287
|
+
if (conflicts > 0) p.log.warn(`${conflicts} conflict${conflicts === 1 ? "" : "s"} — see .rej files in ${color.cyan(".opencode/patches/")}`);
|
|
4288
|
+
if (success === 0 && conflicts === 0 && skipped === 0) p.log.info("No patches to apply");
|
|
4289
|
+
}
|
|
4290
|
+
function applySinglePatch(opencodeDir, relativePath, entry) {
|
|
4291
|
+
const filePath = join(opencodeDir, relativePath);
|
|
4292
|
+
const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
|
|
4293
|
+
if (!existsSync(filePath)) {
|
|
4294
|
+
p.log.error(`Target file missing: ${color.cyan(relativePath)}`);
|
|
4295
|
+
return;
|
|
4296
|
+
}
|
|
4297
|
+
if (!existsSync(patchPath)) {
|
|
4298
|
+
p.log.error(`Patch file missing: ${color.cyan(entry.patchFile)}`);
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
const result = applyPatch$1(readFileSync(filePath, "utf-8"), readFileSync(patchPath, "utf-8"));
|
|
4302
|
+
if (result === false) {
|
|
4303
|
+
p.log.error(`Conflict applying patch to ${color.cyan(relativePath)}`);
|
|
4304
|
+
p.log.info(color.dim("Template may have changed — try 'ock patch create' to recreate"));
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
writeFileSync(filePath, result, "utf-8");
|
|
4308
|
+
p.log.success(`Applied patch to ${color.cyan(relativePath)}`);
|
|
4309
|
+
}
|
|
4310
|
+
function showDiff(opencodeDir) {
|
|
4311
|
+
const fileArg = process.argv[4];
|
|
4312
|
+
if (!fileArg) {
|
|
4313
|
+
const metadata = loadPatchMetadata(opencodeDir);
|
|
4314
|
+
const entries = Object.entries(metadata.patches);
|
|
4315
|
+
if (entries.length === 0) {
|
|
4316
|
+
showEmpty("patches", "ock patch create <file>");
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
for (const [relativePath, entry] of entries) showSingleDiff(opencodeDir, relativePath, entry);
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
const entry = loadPatchMetadata(opencodeDir).patches[fileArg];
|
|
4323
|
+
if (!entry) {
|
|
4324
|
+
notFound("patch", fileArg);
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
showSingleDiff(opencodeDir, fileArg, entry);
|
|
4328
|
+
}
|
|
4329
|
+
function showSingleDiff(opencodeDir, relativePath, entry) {
|
|
4330
|
+
const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
|
|
4331
|
+
if (!existsSync(patchPath)) {
|
|
4332
|
+
p.log.error(`Patch file missing: ${color.cyan(entry.patchFile)}`);
|
|
4333
|
+
return;
|
|
4334
|
+
}
|
|
4335
|
+
const patchContent = readFileSync(patchPath, "utf-8");
|
|
4336
|
+
const disabledLabel = entry.disabled ? color.yellow(" [disabled]") : "";
|
|
4337
|
+
console.log(`\n${color.bold(color.cyan(relativePath))}${disabledLabel}`);
|
|
4338
|
+
if (entry.description) console.log(color.dim(` ${entry.description}`));
|
|
4339
|
+
console.log(color.dim("─".repeat(60)));
|
|
4340
|
+
for (const line of patchContent.split("\n")) if (line.startsWith("+++") || line.startsWith("---")) console.log(color.bold(line));
|
|
4341
|
+
else if (line.startsWith("+")) console.log(color.green(line));
|
|
4342
|
+
else if (line.startsWith("-")) console.log(color.red(line));
|
|
4343
|
+
else if (line.startsWith("@@")) console.log(color.cyan(line));
|
|
4344
|
+
else console.log(color.dim(line));
|
|
4345
|
+
console.log();
|
|
4346
|
+
}
|
|
4347
|
+
async function removePatchCmd(opencodeDir) {
|
|
4348
|
+
const fileArg = process.argv[4];
|
|
4349
|
+
if (!fileArg) {
|
|
4350
|
+
p.log.error("Usage: ock patch remove <file>");
|
|
4351
|
+
p.log.info(color.dim("Use 'ock patch list' to see available patches"));
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
if (!loadPatchMetadata(opencodeDir).patches[fileArg]) {
|
|
4355
|
+
notFound("patch", fileArg);
|
|
4356
|
+
return;
|
|
4357
|
+
}
|
|
4358
|
+
const confirm = await p.confirm({
|
|
4359
|
+
message: `Remove patch for ${color.cyan(fileArg)}?`,
|
|
4360
|
+
initialValue: false
|
|
4361
|
+
});
|
|
4362
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
4363
|
+
p.cancel("Cancelled");
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
if (removePatch(opencodeDir, fileArg)) p.log.success(`Removed patch for ${color.cyan(fileArg)}`);
|
|
4367
|
+
else p.log.error(`Failed to remove patch for ${color.cyan(fileArg)}`);
|
|
4368
|
+
}
|
|
4369
|
+
function togglePatch(opencodeDir, disable) {
|
|
4370
|
+
const fileArg = process.argv[4];
|
|
4371
|
+
if (!fileArg) {
|
|
4372
|
+
p.log.error(`Usage: ock patch ${disable ? "disable" : "enable"} <file>`);
|
|
4373
|
+
p.log.info(color.dim("Use 'ock patch list' to see available patches"));
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
const metadata = loadPatchMetadata(opencodeDir);
|
|
4377
|
+
const entry = metadata.patches[fileArg];
|
|
4378
|
+
if (!entry) {
|
|
4379
|
+
notFound("patch", fileArg);
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
if (disable && entry.disabled) {
|
|
4383
|
+
p.log.warn(`Patch for ${color.cyan(fileArg)} is already disabled`);
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4386
|
+
if (!disable && !entry.disabled) {
|
|
4387
|
+
p.log.warn(`Patch for ${color.cyan(fileArg)} is already enabled`);
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
entry.disabled = disable || void 0;
|
|
4391
|
+
metadata.patches[fileArg] = entry;
|
|
4392
|
+
savePatchMetadata(opencodeDir, metadata);
|
|
4393
|
+
if (disable) {
|
|
4394
|
+
p.log.success(`Disabled patch for ${color.cyan(fileArg)}`);
|
|
4395
|
+
p.log.info(color.dim("This patch will be skipped during upgrades"));
|
|
4396
|
+
} else {
|
|
4397
|
+
p.log.success(`Enabled patch for ${color.cyan(fileArg)}`);
|
|
4398
|
+
p.log.info(color.dim("This patch will be applied during upgrades"));
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
async function patchCommand(action) {
|
|
4402
|
+
const opencodeDir = getOpencodeDir();
|
|
4403
|
+
if (!opencodeDir) return;
|
|
4404
|
+
const validatedAction = parseAction(PatchActionSchema, action);
|
|
4405
|
+
if (!validatedAction) {
|
|
4406
|
+
listPatches(opencodeDir);
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
switch (validatedAction) {
|
|
4410
|
+
case "list":
|
|
4411
|
+
listPatches(opencodeDir);
|
|
4412
|
+
break;
|
|
4413
|
+
case "create":
|
|
4414
|
+
await createPatch$1(opencodeDir);
|
|
4415
|
+
break;
|
|
4416
|
+
case "apply":
|
|
4417
|
+
applyPatches(opencodeDir);
|
|
4418
|
+
break;
|
|
4419
|
+
case "diff":
|
|
4420
|
+
showDiff(opencodeDir);
|
|
4421
|
+
break;
|
|
4422
|
+
case "remove":
|
|
4423
|
+
await removePatchCmd(opencodeDir);
|
|
4424
|
+
break;
|
|
4425
|
+
case "disable":
|
|
4426
|
+
togglePatch(opencodeDir, true);
|
|
4427
|
+
break;
|
|
4428
|
+
case "enable":
|
|
4429
|
+
togglePatch(opencodeDir, false);
|
|
4430
|
+
break;
|
|
4431
|
+
default: unknownAction(action ?? "", [
|
|
4432
|
+
"list",
|
|
4433
|
+
"create",
|
|
4434
|
+
"apply",
|
|
4435
|
+
"diff",
|
|
4436
|
+
"remove",
|
|
4437
|
+
"disable",
|
|
4438
|
+
"enable"
|
|
4439
|
+
]);
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4056
4443
|
//#endregion
|
|
4057
4444
|
//#region src/tui/utils/keyboard.ts
|
|
4058
4445
|
/**
|
|
@@ -5218,6 +5605,9 @@ cli.command("config [action]", "Edit opencode.json (model, mcp, permission, vali
|
|
|
5218
5605
|
cli.command("upgrade", "Update .opencode/ templates to latest version").option("--force", "Force upgrade even if already up to date").option("--check", "Check for updates without upgrading").option("--prune", "Manually select orphan files to delete").option("--prune-all", "Auto-delete all orphan files").action(async (options) => {
|
|
5219
5606
|
await upgradeCommand(options);
|
|
5220
5607
|
});
|
|
5608
|
+
cli.command("patch [action]", "Manage template patches (list, create, apply, diff, remove, disable, enable)").action(async (action) => {
|
|
5609
|
+
await patchCommand(action);
|
|
5610
|
+
});
|
|
5221
5611
|
cli.command("completion [shell]", "Generate shell completion script (bash, zsh, fish)").action(async (shell) => {
|
|
5222
5612
|
await completionCommand(shell);
|
|
5223
5613
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.18.
|
|
1
|
+
0.18.4
|
|
@@ -220,7 +220,7 @@ For major tracked work:
|
|
|
220
220
|
|
|
221
221
|
## Edit Protocol
|
|
222
222
|
|
|
223
|
-
`str_replace` failures are the #1 source of LLM coding failures.
|
|
223
|
+
`str_replace` failures are the #1 source of LLM coding failures. When tilth MCP is available with `--edit`, prefer hash-anchored edits (see below). Otherwise, use structured edits:
|
|
224
224
|
|
|
225
225
|
1. **LOCATE** — Use LSP tools (goToDefinition, findReferences) to find exact positions
|
|
226
226
|
2. **READ** — Get fresh file content around target (offset: line-10, limit: 30)
|
|
@@ -241,6 +241,18 @@ Files over ~500 lines become hard to maintain and review. Extract helpers, split
|
|
|
241
241
|
|
|
242
242
|
**Use the `structured-edit` skill for complex edits.**
|
|
243
243
|
|
|
244
|
+
### Hash-Anchored Edits (MCP)
|
|
245
|
+
|
|
246
|
+
When tilth MCP is available with `--edit` mode, use hash-anchored edits for higher reliability:
|
|
247
|
+
|
|
248
|
+
1. **READ** via `tilth_read` — output includes `line:hash|content` format per line
|
|
249
|
+
2. **EDIT** via `tilth_edit` — reference lines by their `line:hash` anchor
|
|
250
|
+
3. **REJECT** — if file changed since last read, hashes won't match; re-read and retry
|
|
251
|
+
|
|
252
|
+
**Benefits**: Eliminates `str_replace` failures entirely. If the file changed between read and edit, the operation fails safely (no silent corruption).
|
|
253
|
+
|
|
254
|
+
**Fallback**: Without tilth, use the standard LOCATE→READ→VERIFY→EDIT→CONFIRM flow above.
|
|
255
|
+
|
|
244
256
|
---
|
|
245
257
|
|
|
246
258
|
## Output Style
|
|
@@ -79,7 +79,9 @@ Implement requested work, verify with fresh evidence, and coordinate subagents o
|
|
|
79
79
|
|
|
80
80
|
- No success claims without fresh verification output
|
|
81
81
|
- Verification failures are **signals, not condemnations** — adjust and proceed
|
|
82
|
-
- Re-run typecheck/lint/tests after meaningful edits
|
|
82
|
+
- Re-run typecheck/lint/tests after meaningful edits (use incremental mode — changed files only)
|
|
83
|
+
- Run typecheck + lint in parallel, then tests sequentially
|
|
84
|
+
- Check `.beads/verify.log` cache before re-running — skip if no changes since last PASS
|
|
83
85
|
- If verification fails twice on the same approach, **escalate with learnings**, not frustration
|
|
84
86
|
|
|
85
87
|
## Ritual Structure
|
|
@@ -170,6 +172,7 @@ Load contextually when needed:
|
|
|
170
172
|
| UI work | `frontend-design`, `react-best-practices` |
|
|
171
173
|
| Parallel orchestration | `swarm-coordination`, `beads-bridge` |
|
|
172
174
|
| Before completion | `requesting-code-review`, `finishing-a-development-branch` |
|
|
175
|
+
| Codebase exploration | `code-navigation` |
|
|
173
176
|
|
|
174
177
|
## Execution Mode
|
|
175
178
|
|
|
@@ -29,43 +29,13 @@ You are a read-only codebase explorer. You output concise, evidence-backed findi
|
|
|
29
29
|
|
|
30
30
|
Find relevant files, symbols, and usage paths quickly for the caller.
|
|
31
31
|
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
- Never modify files — read-only is a hard constraint
|
|
35
|
-
- Return absolute paths in final output
|
|
36
|
-
- Cite `file:line` evidence whenever possible
|
|
37
|
-
- Prefer semantic lookup (LSP) before broad text search when it improves precision
|
|
38
|
-
- Stop when you can answer with concrete evidence or when additional search only repeats confirmed paths
|
|
39
|
-
|
|
40
|
-
## Workflow
|
|
32
|
+
## Skills
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
2. Validate symbol flow with LSP (`goToDefinition`, `findReferences`)
|
|
44
|
-
3. Use `grep` for targeted pattern checks
|
|
45
|
-
4. Read only relevant sections
|
|
46
|
-
5. Return findings + next steps
|
|
34
|
+
Always load:
|
|
47
35
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
| --------------- | ----------------------------- | ------------------------------------------ |
|
|
52
|
-
| `quick` | 1-3 files, direct answer | Simple lookups, known symbol names |
|
|
53
|
-
| `medium` | 3-6 files, include call paths | Understanding feature flow |
|
|
54
|
-
| `very thorough` | Dependency map + edge cases | Complex refactor prep, architecture review |
|
|
55
|
-
|
|
56
|
-
## Output
|
|
57
|
-
|
|
58
|
-
- **Files**: absolute paths with line refs
|
|
59
|
-
- **Findings**: concise, evidence-backed
|
|
60
|
-
- **Next Steps** (optional): recommended actions for the caller
|
|
61
|
-
|
|
62
|
-
## Identity
|
|
63
|
-
|
|
64
|
-
You are a read-only codebase explorer. You output concise, evidence-backed findings with absolute paths only.
|
|
65
|
-
|
|
66
|
-
## Task
|
|
67
|
-
|
|
68
|
-
Find relevant files, symbols, and usage paths quickly for the caller.
|
|
36
|
+
```typescript
|
|
37
|
+
skill({ name: "code-navigation" });
|
|
38
|
+
```
|
|
69
39
|
|
|
70
40
|
## Rules
|
|
71
41
|
|