oh-my-opencode-serverlocal 0.1.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/LICENSE +21 -0
- package/README.ja-JP.md +775 -0
- package/README.ko-KR.md +796 -0
- package/README.md +792 -0
- package/README.zh-CN.md +765 -0
- package/dist/agents/council.d.ts +27 -0
- package/dist/agents/councillor.d.ts +2 -0
- package/dist/agents/designer.d.ts +2 -0
- package/dist/agents/explorer.d.ts +2 -0
- package/dist/agents/fixer.d.ts +2 -0
- package/dist/agents/index.d.ts +31 -0
- package/dist/agents/librarian.d.ts +2 -0
- package/dist/agents/observer.d.ts +2 -0
- package/dist/agents/oracle.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +28 -0
- package/dist/agents/permissions.d.ts +10 -0
- package/dist/cli/background-subagents.d.ts +13 -0
- package/dist/cli/companion.d.ts +4 -0
- package/dist/cli/config-io.d.ts +25 -0
- package/dist/cli/config-manager.d.ts +4 -0
- package/dist/cli/custom-skills-registry.d.ts +18 -0
- package/dist/cli/custom-skills.d.ts +13 -0
- package/dist/cli/doctor.d.ts +38 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3565 -0
- package/dist/cli/install.d.ts +7 -0
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/paths.d.ts +35 -0
- package/dist/cli/providers.d.ts +145 -0
- package/dist/cli/skills.d.ts +24 -0
- package/dist/cli/system.d.ts +6 -0
- package/dist/cli/types.d.ts +47 -0
- package/dist/companion/manager.d.ts +50 -0
- package/dist/companion/updater.d.ts +36 -0
- package/dist/config/agent-mcps.d.ts +11 -0
- package/dist/config/constants.d.ts +38 -0
- package/dist/config/council-schema.d.ts +137 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/loader.d.ts +88 -0
- package/dist/config/runtime-preset.d.ts +11 -0
- package/dist/config/schema.d.ts +1107 -0
- package/dist/config/strip-orchestrator-model.d.ts +9 -0
- package/dist/config/utils.d.ts +18 -0
- package/dist/council/council-manager.d.ts +53 -0
- package/dist/council/index.d.ts +1 -0
- package/dist/hooks/apply-patch/codec.d.ts +7 -0
- package/dist/hooks/apply-patch/errors.d.ts +25 -0
- package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
- package/dist/hooks/apply-patch/index.d.ts +15 -0
- package/dist/hooks/apply-patch/matching.d.ts +26 -0
- package/dist/hooks/apply-patch/operations.d.ts +3 -0
- package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
- package/dist/hooks/apply-patch/resolution.d.ts +19 -0
- package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
- package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
- package/dist/hooks/apply-patch/types.d.ts +80 -0
- package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +38 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +12 -0
- package/dist/hooks/auto-update-checker/index.d.ts +18 -0
- package/dist/hooks/auto-update-checker/skill-sync.d.ts +67 -0
- package/dist/hooks/auto-update-checker/types.d.ts +34 -0
- package/dist/hooks/chat-headers.d.ts +16 -0
- package/dist/hooks/command-hook-utils.d.ts +5 -0
- package/dist/hooks/deepwork/index.d.ts +13 -0
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
- package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
- package/dist/hooks/filter-available-skills/index.d.ts +19 -0
- package/dist/hooks/foreground-fallback/index.d.ts +122 -0
- package/dist/hooks/image-hook.d.ts +8 -0
- package/dist/hooks/index.d.ts +16 -0
- package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
- package/dist/hooks/loop-command/index.d.ts +13 -0
- package/dist/hooks/phase-reminder/index.d.ts +25 -0
- package/dist/hooks/post-file-tool-nudge/index.d.ts +23 -0
- package/dist/hooks/reflect/index.d.ts +13 -0
- package/dist/hooks/session-lifecycle.d.ts +10 -0
- package/dist/hooks/task-session-manager/index.d.ts +58 -0
- package/dist/hooks/task-session-manager/pending-call-tracker.d.ts +13 -0
- package/dist/hooks/task-session-manager/task-context-tracker.d.ts +14 -0
- package/dist/hooks/types.d.ts +26 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +39519 -0
- package/dist/interview/dashboard-manager.d.ts +21 -0
- package/dist/interview/dashboard.d.ts +67 -0
- package/dist/interview/document.d.ts +29 -0
- package/dist/interview/helpers.d.ts +10 -0
- package/dist/interview/index.d.ts +1 -0
- package/dist/interview/manager.d.ts +21 -0
- package/dist/interview/parser.d.ts +11 -0
- package/dist/interview/prompts.d.ts +7 -0
- package/dist/interview/server.d.ts +15 -0
- package/dist/interview/service.d.ts +45 -0
- package/dist/interview/session-server.d.ts +21 -0
- package/dist/interview/types.d.ts +111 -0
- package/dist/interview/ui.d.ts +11 -0
- package/dist/loop/loop-session.d.ts +64 -0
- package/dist/mcp/context7.d.ts +6 -0
- package/dist/mcp/grep-app.d.ts +6 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/websearch.d.ts +9 -0
- package/dist/multiplexer/cmux/close-policy.d.ts +20 -0
- package/dist/multiplexer/cmux/index.d.ts +102 -0
- package/dist/multiplexer/cmux/session-lifecycle.d.ts +92 -0
- package/dist/multiplexer/cmux/session-state.d.ts +45 -0
- package/dist/multiplexer/factory.d.ts +17 -0
- package/dist/multiplexer/herdr/index.d.ts +35 -0
- package/dist/multiplexer/index.d.ts +12 -0
- package/dist/multiplexer/session-manager.d.ts +69 -0
- package/dist/multiplexer/shared.d.ts +31 -0
- package/dist/multiplexer/tmux/index.d.ts +26 -0
- package/dist/multiplexer/types.d.ts +57 -0
- package/dist/multiplexer/zellij/index.d.ts +47 -0
- package/dist/tools/acp-run.d.ts +3 -0
- package/dist/tools/ast-grep/cli.d.ts +15 -0
- package/dist/tools/ast-grep/constants.d.ts +25 -0
- package/dist/tools/ast-grep/downloader.d.ts +5 -0
- package/dist/tools/ast-grep/index.d.ts +10 -0
- package/dist/tools/ast-grep/tools.d.ts +3 -0
- package/dist/tools/ast-grep/types.d.ts +30 -0
- package/dist/tools/ast-grep/utils.d.ts +4 -0
- package/dist/tools/cancel-task.d.ts +16 -0
- package/dist/tools/council.d.ts +10 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/preset-manager.d.ts +26 -0
- package/dist/tools/smartfetch/binary.d.ts +3 -0
- package/dist/tools/smartfetch/cache.d.ts +6 -0
- package/dist/tools/smartfetch/constants.d.ts +12 -0
- package/dist/tools/smartfetch/index.d.ts +3 -0
- package/dist/tools/smartfetch/network.d.ts +38 -0
- package/dist/tools/smartfetch/secondary-model.d.ts +35 -0
- package/dist/tools/smartfetch/tool.d.ts +3 -0
- package/dist/tools/smartfetch/types.d.ts +122 -0
- package/dist/tools/smartfetch/utils.d.ts +20 -0
- package/dist/tui-state.d.ts +18 -0
- package/dist/tui.d.ts +13 -0
- package/dist/tui.js +1112 -0
- package/dist/utils/agent-variant.d.ts +23 -0
- package/dist/utils/background-job-board.d.ts +113 -0
- package/dist/utils/background-job-coordinator.d.ts +72 -0
- package/dist/utils/background-job-store.d.ts +46 -0
- package/dist/utils/compat.d.ts +29 -0
- package/dist/utils/councillor-models.d.ts +20 -0
- package/dist/utils/env.d.ts +3 -0
- package/dist/utils/escape-html.d.ts +1 -0
- package/dist/utils/frontmatter.d.ts +6 -0
- package/dist/utils/guards.d.ts +4 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/internal-initiator.d.ts +11 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/polling.d.ts +21 -0
- package/dist/utils/session.d.ts +75 -0
- package/dist/utils/subagent-depth.d.ts +35 -0
- package/dist/utils/system-collapse.d.ts +6 -0
- package/dist/utils/task.d.ts +20 -0
- package/dist/utils/zip-extractor.d.ts +1 -0
- package/package.json +108 -0
- package/src/companion/companion-manifest.json +12 -0
- package/src/skills/clonedeps/README.md +23 -0
- package/src/skills/clonedeps/SKILL.md +240 -0
- package/src/skills/clonedeps/codemap.md +32 -0
- package/src/skills/codemap/README.md +59 -0
- package/src/skills/codemap/SKILL.md +163 -0
- package/src/skills/codemap/codemap.md +36 -0
- package/src/skills/codemap/scripts/codemap.mjs +483 -0
- package/src/skills/codemap/scripts/codemap.test.ts +129 -0
- package/src/skills/codemap.md +78 -0
- package/src/skills/deepwork/SKILL.md +122 -0
- package/src/skills/loop-engineering/SKILL.md +30 -0
- package/src/skills/oh-my-opencode-slim/SKILL.md +326 -0
- package/src/skills/reflect/SKILL.md +326 -0
- package/src/skills/simplify/README.md +19 -0
- package/src/skills/simplify/SKILL.md +138 -0
- package/src/skills/simplify/codemap.md +36 -0
- package/src/skills/verification-planning/SKILL.md +102 -0
- package/src/skills/worktrees/SKILL.md +171 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
export const VERSION = '1.0.0';
|
|
17
|
+
export const STATE_DIR = '.slim';
|
|
18
|
+
export const STATE_FILE = 'codemap.json';
|
|
19
|
+
export const LEGACY_STATE_FILE = 'cartography.json';
|
|
20
|
+
export const CODEMAP_FILE = 'codemap.md';
|
|
21
|
+
|
|
22
|
+
export class PatternMatcher {
|
|
23
|
+
regex;
|
|
24
|
+
|
|
25
|
+
constructor(patterns) {
|
|
26
|
+
if (!patterns.length) {
|
|
27
|
+
this.regex = null;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const regexParts = patterns.map((pattern) => {
|
|
32
|
+
let reg = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
33
|
+
reg = reg.replace(/\\\*\\\*\//g, '(?:.*/)?');
|
|
34
|
+
reg = reg.replace(/\\\*\\\*/g, '.*');
|
|
35
|
+
reg = reg.replace(/\\\*/g, '[^/]*');
|
|
36
|
+
reg = reg.replace(/\\\?/g, '.');
|
|
37
|
+
|
|
38
|
+
if (pattern.endsWith('/')) {
|
|
39
|
+
reg += '.*';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (pattern.startsWith('/')) {
|
|
43
|
+
reg = `^${reg.slice(1)}`;
|
|
44
|
+
} else {
|
|
45
|
+
reg = `(?:^|.*/)${reg}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return `(?:${reg}$)`;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.regex = new RegExp(regexParts.join('|'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
matches(filePath) {
|
|
55
|
+
if (!this.regex) return false;
|
|
56
|
+
return this.regex.test(filePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loadGitignore(root) {
|
|
61
|
+
const gitignorePath = path.join(root, '.gitignore');
|
|
62
|
+
if (!existsSync(gitignorePath)) return [];
|
|
63
|
+
|
|
64
|
+
return readFileSync(gitignorePath, 'utf8')
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function walkFiles(root) {
|
|
71
|
+
const files = [];
|
|
72
|
+
|
|
73
|
+
function visit(currentDir) {
|
|
74
|
+
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
75
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
if (!entry.name.startsWith('.')) {
|
|
78
|
+
visit(fullPath);
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (entry.isFile()) {
|
|
84
|
+
files.push(fullPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
visit(root);
|
|
90
|
+
return files.sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function selectFiles(
|
|
94
|
+
root,
|
|
95
|
+
includePatterns,
|
|
96
|
+
excludePatterns,
|
|
97
|
+
exceptions,
|
|
98
|
+
gitignorePatterns,
|
|
99
|
+
) {
|
|
100
|
+
const includeMatcher = new PatternMatcher(includePatterns);
|
|
101
|
+
const excludeMatcher = new PatternMatcher(excludePatterns);
|
|
102
|
+
const gitignoreMatcher = new PatternMatcher(gitignorePatterns);
|
|
103
|
+
const exceptionSet = new Set(exceptions);
|
|
104
|
+
|
|
105
|
+
return walkFiles(root).filter((fullPath) => {
|
|
106
|
+
let relPath = path.relative(root, fullPath).replaceAll(path.sep, '/');
|
|
107
|
+
if (relPath.startsWith('./')) {
|
|
108
|
+
relPath = relPath.slice(2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (gitignoreMatcher.matches(relPath)) return false;
|
|
112
|
+
if (excludeMatcher.matches(relPath) && !exceptionSet.has(relPath)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return includeMatcher.matches(relPath) || exceptionSet.has(relPath);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function computeFileHash(filePath) {
|
|
121
|
+
try {
|
|
122
|
+
const buffer = readFileSync(filePath);
|
|
123
|
+
return createHash('md5').update(buffer).digest('hex');
|
|
124
|
+
} catch {
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function computeFolderHash(folder, fileHashes) {
|
|
130
|
+
const folderFiles = Object.entries(fileHashes)
|
|
131
|
+
.filter(
|
|
132
|
+
([filePath]) =>
|
|
133
|
+
filePath.startsWith(`${folder}/`) ||
|
|
134
|
+
(folder === '.' && !filePath.includes('/')),
|
|
135
|
+
)
|
|
136
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
137
|
+
|
|
138
|
+
if (!folderFiles.length) return '';
|
|
139
|
+
|
|
140
|
+
const hasher = createHash('md5');
|
|
141
|
+
for (const [filePath, hash] of folderFiles) {
|
|
142
|
+
hasher.update(`${filePath}:${hash}\n`);
|
|
143
|
+
}
|
|
144
|
+
return hasher.digest('hex');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getFoldersWithFiles(files, root) {
|
|
148
|
+
const folders = new Set(['.']);
|
|
149
|
+
|
|
150
|
+
for (const filePath of files) {
|
|
151
|
+
const relPath = path.relative(root, filePath).replaceAll(path.sep, '/');
|
|
152
|
+
const parts = relPath.split('/').slice(0, -1);
|
|
153
|
+
for (let i = 0; i < parts.length; i++) {
|
|
154
|
+
folders.add(parts.slice(0, i + 1).join('/'));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return folders;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function migrateLegacyState(root) {
|
|
162
|
+
const stateDir = path.join(root, STATE_DIR);
|
|
163
|
+
const legacyPath = path.join(stateDir, LEGACY_STATE_FILE);
|
|
164
|
+
const statePath = path.join(stateDir, STATE_FILE);
|
|
165
|
+
|
|
166
|
+
if (existsSync(statePath) || !existsSync(legacyPath)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
mkdirSync(stateDir, { recursive: true });
|
|
171
|
+
renameSync(legacyPath, statePath);
|
|
172
|
+
console.log(
|
|
173
|
+
`Migrated ${STATE_DIR}/${LEGACY_STATE_FILE} -> ${STATE_DIR}/${STATE_FILE}`,
|
|
174
|
+
);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function loadState(root) {
|
|
179
|
+
migrateLegacyState(root);
|
|
180
|
+
const statePath = path.join(root, STATE_DIR, STATE_FILE);
|
|
181
|
+
if (!existsSync(statePath)) return null;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(readFileSync(statePath, 'utf8'));
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function saveState(root, state) {
|
|
191
|
+
const stateDir = path.join(root, STATE_DIR);
|
|
192
|
+
mkdirSync(stateDir, { recursive: true });
|
|
193
|
+
writeFileSync(
|
|
194
|
+
path.join(stateDir, STATE_FILE),
|
|
195
|
+
`${JSON.stringify(state, null, 2)}\n`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function createEmptyCodemap(folderPath, folderName) {
|
|
200
|
+
const codemapPath = path.join(folderPath, CODEMAP_FILE);
|
|
201
|
+
if (existsSync(codemapPath)) return;
|
|
202
|
+
|
|
203
|
+
const content = `# ${folderName}/
|
|
204
|
+
|
|
205
|
+
<!-- Fixer: Fill in this section with architectural understanding -->
|
|
206
|
+
|
|
207
|
+
## Responsibility
|
|
208
|
+
|
|
209
|
+
<!-- What is this folder's job in the system? -->
|
|
210
|
+
|
|
211
|
+
## Design
|
|
212
|
+
|
|
213
|
+
<!-- Key patterns, abstractions, architectural decisions -->
|
|
214
|
+
|
|
215
|
+
## Flow
|
|
216
|
+
|
|
217
|
+
<!-- How does data/control flow through this module? -->
|
|
218
|
+
|
|
219
|
+
## Integration
|
|
220
|
+
|
|
221
|
+
<!-- How does it connect to other parts of the system? -->
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
writeFileSync(codemapPath, content);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildState(
|
|
228
|
+
root,
|
|
229
|
+
includePatterns,
|
|
230
|
+
excludePatterns,
|
|
231
|
+
exceptions,
|
|
232
|
+
selectedFiles,
|
|
233
|
+
) {
|
|
234
|
+
const fileHashes = {};
|
|
235
|
+
for (const filePath of selectedFiles) {
|
|
236
|
+
const relPath = path.relative(root, filePath).replaceAll(path.sep, '/');
|
|
237
|
+
fileHashes[relPath] = computeFileHash(filePath);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const folders = getFoldersWithFiles(selectedFiles, root);
|
|
241
|
+
const folderHashes = {};
|
|
242
|
+
for (const folder of folders) {
|
|
243
|
+
folderHashes[folder] = computeFolderHash(folder, fileHashes);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const state = {
|
|
247
|
+
metadata: {
|
|
248
|
+
version: VERSION,
|
|
249
|
+
last_run: new Date().toISOString(),
|
|
250
|
+
root,
|
|
251
|
+
include_patterns: includePatterns,
|
|
252
|
+
exclude_patterns: excludePatterns,
|
|
253
|
+
exceptions,
|
|
254
|
+
},
|
|
255
|
+
file_hashes: fileHashes,
|
|
256
|
+
folder_hashes: folderHashes,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return { state, folders };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function cmdInit({ root, include = [], exclude = [], exception = [] }) {
|
|
263
|
+
const resolvedRoot = path.resolve(root);
|
|
264
|
+
if (!existsSync(resolvedRoot) || !statSync(resolvedRoot).isDirectory()) {
|
|
265
|
+
console.error(`Error: ${resolvedRoot} is not a directory`);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const includePatterns = include.length ? include : ['**/*'];
|
|
270
|
+
const excludePatterns = exclude;
|
|
271
|
+
const exceptions = exception;
|
|
272
|
+
const gitignore = loadGitignore(resolvedRoot);
|
|
273
|
+
|
|
274
|
+
console.log(`Scanning ${resolvedRoot}...`);
|
|
275
|
+
console.log(`Include patterns: ${JSON.stringify(includePatterns)}`);
|
|
276
|
+
console.log(`Exclude patterns: ${JSON.stringify(excludePatterns)}`);
|
|
277
|
+
console.log(`Exceptions: ${JSON.stringify(exceptions)}`);
|
|
278
|
+
|
|
279
|
+
const selectedFiles = selectFiles(
|
|
280
|
+
resolvedRoot,
|
|
281
|
+
includePatterns,
|
|
282
|
+
excludePatterns,
|
|
283
|
+
exceptions,
|
|
284
|
+
gitignore,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
console.log(`Selected ${selectedFiles.length} files`);
|
|
288
|
+
|
|
289
|
+
const { state, folders } = buildState(
|
|
290
|
+
resolvedRoot,
|
|
291
|
+
includePatterns,
|
|
292
|
+
excludePatterns,
|
|
293
|
+
exceptions,
|
|
294
|
+
selectedFiles,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
saveState(resolvedRoot, state);
|
|
298
|
+
console.log(`Created ${STATE_DIR}/${STATE_FILE}`);
|
|
299
|
+
|
|
300
|
+
for (const folder of folders) {
|
|
301
|
+
const folderPath =
|
|
302
|
+
folder === '.' ? resolvedRoot : path.join(resolvedRoot, folder);
|
|
303
|
+
const folderName = folder === '.' ? path.basename(resolvedRoot) : folder;
|
|
304
|
+
createEmptyCodemap(folderPath, folderName);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(`Created ${folders.size} empty codemap.md files`);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function cmdChanges({ root }) {
|
|
312
|
+
const resolvedRoot = path.resolve(root);
|
|
313
|
+
const state = loadState(resolvedRoot);
|
|
314
|
+
if (!state) {
|
|
315
|
+
console.error("No codemap state found. Run 'init' first.");
|
|
316
|
+
return 1;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const metadata = state.metadata ?? {};
|
|
320
|
+
const includePatterns = metadata.include_patterns ?? ['**/*'];
|
|
321
|
+
const excludePatterns = metadata.exclude_patterns ?? [];
|
|
322
|
+
const exceptions = metadata.exceptions ?? [];
|
|
323
|
+
const gitignore = loadGitignore(resolvedRoot);
|
|
324
|
+
|
|
325
|
+
const currentFiles = selectFiles(
|
|
326
|
+
resolvedRoot,
|
|
327
|
+
includePatterns,
|
|
328
|
+
excludePatterns,
|
|
329
|
+
exceptions,
|
|
330
|
+
gitignore,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const currentHashes = Object.fromEntries(
|
|
334
|
+
currentFiles.map((filePath) => [
|
|
335
|
+
path.relative(resolvedRoot, filePath).replaceAll(path.sep, '/'),
|
|
336
|
+
computeFileHash(filePath),
|
|
337
|
+
]),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const savedHashes = state.file_hashes ?? {};
|
|
341
|
+
const currentPaths = new Set(Object.keys(currentHashes));
|
|
342
|
+
const savedPaths = new Set(Object.keys(savedHashes));
|
|
343
|
+
|
|
344
|
+
const added = [...currentPaths]
|
|
345
|
+
.filter((filePath) => !savedPaths.has(filePath))
|
|
346
|
+
.sort();
|
|
347
|
+
const removed = [...savedPaths]
|
|
348
|
+
.filter((filePath) => !currentPaths.has(filePath))
|
|
349
|
+
.sort();
|
|
350
|
+
const modified = [...currentPaths]
|
|
351
|
+
.filter((filePath) => savedPaths.has(filePath))
|
|
352
|
+
.filter((filePath) => currentHashes[filePath] !== savedHashes[filePath])
|
|
353
|
+
.sort();
|
|
354
|
+
|
|
355
|
+
if (!added.length && !removed.length && !modified.length) {
|
|
356
|
+
console.log('No changes detected.');
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (added.length) {
|
|
361
|
+
console.log(`\n${added.length} added:`);
|
|
362
|
+
for (const filePath of added) console.log(` + ${filePath}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (removed.length) {
|
|
366
|
+
console.log(`\n${removed.length} removed:`);
|
|
367
|
+
for (const filePath of removed) console.log(` - ${filePath}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (modified.length) {
|
|
371
|
+
console.log(`\n${modified.length} modified:`);
|
|
372
|
+
for (const filePath of modified) console.log(` ~ ${filePath}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const affectedFolders = new Set(['.']);
|
|
376
|
+
for (const filePath of [...added, ...removed, ...modified]) {
|
|
377
|
+
const parts = filePath.split('/').slice(0, -1);
|
|
378
|
+
for (let i = 0; i < parts.length; i++) {
|
|
379
|
+
affectedFolders.add(parts.slice(0, i + 1).join('/'));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const sortedFolders = [...affectedFolders].sort();
|
|
384
|
+
console.log(`\n${sortedFolders.length} folders affected:`);
|
|
385
|
+
for (const folder of sortedFolders) {
|
|
386
|
+
console.log(` ${folder}/`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function cmdUpdate({ root }) {
|
|
393
|
+
const resolvedRoot = path.resolve(root);
|
|
394
|
+
const state = loadState(resolvedRoot);
|
|
395
|
+
if (!state) {
|
|
396
|
+
console.error("No codemap state found. Run 'init' first.");
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const metadata = state.metadata ?? {};
|
|
401
|
+
const includePatterns = metadata.include_patterns ?? ['**/*'];
|
|
402
|
+
const excludePatterns = metadata.exclude_patterns ?? [];
|
|
403
|
+
const exceptions = metadata.exceptions ?? [];
|
|
404
|
+
const gitignore = loadGitignore(resolvedRoot);
|
|
405
|
+
|
|
406
|
+
const selectedFiles = selectFiles(
|
|
407
|
+
resolvedRoot,
|
|
408
|
+
includePatterns,
|
|
409
|
+
excludePatterns,
|
|
410
|
+
exceptions,
|
|
411
|
+
gitignore,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const { state: nextState } = buildState(
|
|
415
|
+
resolvedRoot,
|
|
416
|
+
includePatterns,
|
|
417
|
+
excludePatterns,
|
|
418
|
+
exceptions,
|
|
419
|
+
selectedFiles,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
saveState(resolvedRoot, nextState);
|
|
423
|
+
console.log(
|
|
424
|
+
`Updated ${STATE_DIR}/${STATE_FILE} with ${selectedFiles.length} files`,
|
|
425
|
+
);
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function parseArgs(argv) {
|
|
430
|
+
const [command, ...rest] = argv;
|
|
431
|
+
const options = { include: [], exclude: [], exception: [] };
|
|
432
|
+
|
|
433
|
+
for (let i = 0; i < rest.length; i++) {
|
|
434
|
+
const arg = rest[i];
|
|
435
|
+
const value = rest[i + 1];
|
|
436
|
+
|
|
437
|
+
if (!arg?.startsWith('--')) continue;
|
|
438
|
+
if (value === undefined || value.startsWith('--')) {
|
|
439
|
+
throw new Error(`Missing value for ${arg}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const key = arg.slice(2);
|
|
443
|
+
if (key === 'include' || key === 'exclude' || key === 'exception') {
|
|
444
|
+
options[key].push(value);
|
|
445
|
+
} else if (key === 'root') {
|
|
446
|
+
options.root = value;
|
|
447
|
+
} else {
|
|
448
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
i++;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return { command, options };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function main(argv = process.argv.slice(2)) {
|
|
458
|
+
try {
|
|
459
|
+
const { command, options } = parseArgs(argv);
|
|
460
|
+
|
|
461
|
+
if (!command || !options.root) {
|
|
462
|
+
console.error(
|
|
463
|
+
'Usage: codemap.mjs <init|changes|update> --root /path [--include glob] [--exclude glob] [--exception path]',
|
|
464
|
+
);
|
|
465
|
+
return 1;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (command === 'init') return cmdInit(options);
|
|
469
|
+
if (command === 'changes') return cmdChanges(options);
|
|
470
|
+
if (command === 'update') return cmdUpdate(options);
|
|
471
|
+
|
|
472
|
+
console.error(`Unknown command: ${command}`);
|
|
473
|
+
return 1;
|
|
474
|
+
} catch (error) {
|
|
475
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
481
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
|
|
482
|
+
process.exit(main());
|
|
483
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
mock.restore();
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
computeFileHash,
|
|
17
|
+
computeFolderHash,
|
|
18
|
+
loadState,
|
|
19
|
+
PatternMatcher,
|
|
20
|
+
selectFiles,
|
|
21
|
+
} = await import('./codemap.mjs');
|
|
22
|
+
|
|
23
|
+
const tempDirs: string[] = [];
|
|
24
|
+
|
|
25
|
+
function createTempDir() {
|
|
26
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), 'codemap-'));
|
|
27
|
+
tempDirs.push(dir);
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
for (const dir of tempDirs.splice(0)) {
|
|
33
|
+
rmSync(dir, { force: true, recursive: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('PatternMatcher', () => {
|
|
38
|
+
test('matches expected paths', () => {
|
|
39
|
+
const matcher = new PatternMatcher([
|
|
40
|
+
'node_modules/',
|
|
41
|
+
'dist/',
|
|
42
|
+
'*.log',
|
|
43
|
+
'src/**/*.ts',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
expect(matcher.matches('node_modules/foo.js')).toBe(true);
|
|
47
|
+
expect(matcher.matches('vendor/node_modules/bar.js')).toBe(true);
|
|
48
|
+
expect(matcher.matches('dist/main.js')).toBe(true);
|
|
49
|
+
expect(matcher.matches('src/dist/output.js')).toBe(true);
|
|
50
|
+
expect(matcher.matches('error.log')).toBe(true);
|
|
51
|
+
expect(matcher.matches('logs/access.log')).toBe(true);
|
|
52
|
+
expect(matcher.matches('src/index.ts')).toBe(true);
|
|
53
|
+
expect(matcher.matches('src/utils/helper.ts')).toBe(true);
|
|
54
|
+
expect(matcher.matches('README.md')).toBe(false);
|
|
55
|
+
expect(matcher.matches('tests/test.py')).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('hash helpers', () => {
|
|
60
|
+
test('computes file hash', () => {
|
|
61
|
+
const dir = createTempDir();
|
|
62
|
+
const filePath = path.join(dir, 'file.txt');
|
|
63
|
+
writeFileSync(filePath, 'test content');
|
|
64
|
+
|
|
65
|
+
expect(computeFileHash(filePath)).toBe('9473fdd0d880a43c21b7778d34872157');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('computes stable folder hash', () => {
|
|
69
|
+
const fileHashes = {
|
|
70
|
+
'src/a.ts': 'hash-a',
|
|
71
|
+
'src/b.ts': 'hash-b',
|
|
72
|
+
'tests/test.ts': 'hash-test',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const hash1 = computeFolderHash('src', fileHashes);
|
|
76
|
+
const hash2 = computeFolderHash('src', fileHashes);
|
|
77
|
+
const hash3 = computeFolderHash('src', {
|
|
78
|
+
'src/a.ts': 'hash-a-modified',
|
|
79
|
+
'src/b.ts': 'hash-b',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(hash1).toBe(hash2);
|
|
83
|
+
expect(hash1).not.toBe(hash3);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('selectFiles', () => {
|
|
88
|
+
test('respects include and exclude patterns', () => {
|
|
89
|
+
const root = createTempDir();
|
|
90
|
+
mkdirSync(path.join(root, 'src'));
|
|
91
|
+
mkdirSync(path.join(root, 'node_modules'));
|
|
92
|
+
writeFileSync(path.join(root, 'src', 'index.ts'), 'code');
|
|
93
|
+
writeFileSync(path.join(root, 'src', 'index.test.ts'), 'test');
|
|
94
|
+
writeFileSync(path.join(root, 'node_modules', 'foo.js'), 'dep');
|
|
95
|
+
writeFileSync(path.join(root, 'package.json'), '{}');
|
|
96
|
+
|
|
97
|
+
const selected = selectFiles(
|
|
98
|
+
root,
|
|
99
|
+
['src/**/*.ts', 'package.json'],
|
|
100
|
+
['**/*.test.ts', 'node_modules/'],
|
|
101
|
+
[],
|
|
102
|
+
[],
|
|
103
|
+
).map((filePath) =>
|
|
104
|
+
path.relative(root, filePath).split(path.sep).join('/'),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(selected).toEqual(['package.json', 'src/index.ts']);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('loadState', () => {
|
|
112
|
+
test('migrates legacy cartography state', () => {
|
|
113
|
+
const root = createTempDir();
|
|
114
|
+
const slimDir = path.join(root, '.slim');
|
|
115
|
+
mkdirSync(slimDir);
|
|
116
|
+
|
|
117
|
+
const legacyState = { metadata: { version: '1.0.0' } };
|
|
118
|
+
writeFileSync(
|
|
119
|
+
path.join(slimDir, 'cartography.json'),
|
|
120
|
+
JSON.stringify(legacyState),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(loadState(root)).toEqual(legacyState);
|
|
124
|
+
expect(existsSync(path.join(slimDir, 'cartography.json'))).toBe(false);
|
|
125
|
+
expect(
|
|
126
|
+
JSON.parse(readFileSync(path.join(slimDir, 'codemap.json'), 'utf8')),
|
|
127
|
+
).toEqual(legacyState);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# src/skills/
|
|
2
|
+
|
|
3
|
+
## Responsibility
|
|
4
|
+
|
|
5
|
+
- Owns metadata-driven OpenCode custom skills shipped with this package
|
|
6
|
+
- Maintains the skill contract artifacts (`SKILL.md`, `README.md`, per-skill helper files) that are copied into `${configDir}/skills` at install time
|
|
7
|
+
- Preserves a canonical registry boundary: runtime code consumes skill definitions as data, not as executable plugin dependencies
|
|
8
|
+
- Skills are partitioned into orchestrator-only workflows and general-purpose skills for broad reuse
|
|
9
|
+
|
|
10
|
+
## Design
|
|
11
|
+
|
|
12
|
+
### Skill Registry
|
|
13
|
+
|
|
14
|
+
- `CUSTOM_SKILLS` in `src/cli/custom-skills-registry.ts` is the authoritative skill manifest for bundled skills
|
|
15
|
+
- Each entry maps folder name + `sourcePath` to an install-time consumer
|
|
16
|
+
- Skills are categorized by purpose and access scope:
|
|
17
|
+
|
|
18
|
+
| Skill | Type | Purpose |
|
|
19
|
+
|-------|------|-------|
|
|
20
|
+
| `codemap/` | General-purpose | Repository mapping and codebase documentation skill |
|
|
21
|
+
| `clonedeps/` | General-purpose | Workflow skill for dependency source mirroring and inspection |
|
|
22
|
+
| `simplify/` | General-purpose | Readability and maintainability guidance skill |
|
|
23
|
+
| `deepwork/` | Orchestrator-only | Heavy coding sessions, multi-phase implementation, and risky refactors |
|
|
24
|
+
| `verification-planning/` | Orchestrator-only | Project-specific evidence planning and verification affordances before non-trivial implementation |
|
|
25
|
+
| `reflect/` | Orchestrator-only | Learning from repeated work and suggesting reusable improvements |
|
|
26
|
+
| `worktrees/` | Orchestrator-only | Safe Git worktree lanes for parallel, risky, or isolated work |
|
|
27
|
+
| `oh-my-opencode-slim/` | Orchestrator-only | Plugin configuration and self-improvement guidance |
|
|
28
|
+
|
|
29
|
+
### Installation Pipeline
|
|
30
|
+
|
|
31
|
+
- `install.ts` runs `installCustomSkill()` which recursively copies bundled skill directories into the OpenCode skills directory
|
|
32
|
+
- During plugin release, the `files` whitelist in `package.json` must include `src/skills` so `src/skills/**` survive `npm pack`
|
|
33
|
+
- OpenCode plugin startup discovers these installed folders and reads each `SKILL.md` as a prompt-level contract
|
|
34
|
+
|
|
35
|
+
### Runtime Consumption
|
|
36
|
+
|
|
37
|
+
- Files are considered static runtime payload
|
|
38
|
+
- No plugin TS module in `src/` imports these files directly
|
|
39
|
+
- They are loaded by OpenCode via filesystem installation at runtime
|
|
40
|
+
|
|
41
|
+
## Flow
|
|
42
|
+
|
|
43
|
+
1. **Skill Discovery**: `src/cli/custom-skills-registry.ts` defines `CUSTOM_SKILLS` array with skill metadata
|
|
44
|
+
2. **Installation**: `bun run install` delegates to `src/cli/install.ts`, where `installCustomSkills()` gates copying of each `CUSTOM_SKILLS` entry
|
|
45
|
+
3. **Validation**: `installCustomSkill()` computes `packageRoot`, validates `sourcePath`, then performs a recursive directory copy via `copyDirRecursive()`
|
|
46
|
+
4. **Distribution**: During plugin release, `package.json` `files` whitelist ensures `src/skills/**` are included in the published tarball
|
|
47
|
+
5. **Runtime Discovery**: OpenCode plugin startup discovers installed skill folders and reads each `SKILL.md` as a prompt-level contract
|
|
48
|
+
|
|
49
|
+
## Integration
|
|
50
|
+
|
|
51
|
+
### Build & Release Dependencies
|
|
52
|
+
|
|
53
|
+
- `src/cli/custom-skills-registry.ts`: Source-of-truth registry consumed by installer and permission helpers
|
|
54
|
+
- `src/cli/install.ts`: Contains `installCustomSkills()` and `installCustomSkill()` functions
|
|
55
|
+
- `verify-release-artifact.ts`: Enforces artifact completeness by asserting key bundled skill payloads are present in the tarball:
|
|
56
|
+
- `src/skills/simplify/SKILL.md`
|
|
57
|
+
- `src/skills/codemap/SKILL.md`
|
|
58
|
+
- `src/skills/clonedeps/SKILL.md`
|
|
59
|
+
- `src/skills/deepwork/SKILL.md`
|
|
60
|
+
- `src/skills/verification-planning/SKILL.md`
|
|
61
|
+
- `src/skills/reflect/SKILL.md`
|
|
62
|
+
- `src/skills/worktrees/SKILL.md`
|
|
63
|
+
- `src/skills/oh-my-opencode-slim/SKILL.md`
|
|
64
|
+
- `package.json` scripts (`verify:release`, `build`) rely on these assets to ensure install-time skill availability
|
|
65
|
+
|
|
66
|
+
### Permission System
|
|
67
|
+
|
|
68
|
+
- `src/cli/skills.ts:getSkillPermissionsForAgent()` auto-populates permission rules for bundled skills when agent policy is derived from built-in recommendations
|
|
69
|
+
- Bundled skills are treated as data payloads with explicit permission boundaries defined in the skill manifests
|
|
70
|
+
|
|
71
|
+
### Skill Contracts
|
|
72
|
+
|
|
73
|
+
Each skill directory contains:
|
|
74
|
+
- `SKILL.md`: Skill contract defining name, description, and usage contract
|
|
75
|
+
- `README.md`: Documentation and examples for the skill
|
|
76
|
+
- Optional helper files and subdirectories for skill-specific functionality
|
|
77
|
+
|
|
78
|
+
These artifacts are copied verbatim to the OpenCode skills directory during installation and serve as the skill's interface definition.
|