oh-my-customcode 0.13.2 → 0.14.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/README.md +34 -107
- package/dist/cli/index.js +377 -10
- package/dist/index.js +3 -2
- package/package.json +1 -1
- package/templates/manifest.json +8 -2
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Like oh-my-zsh transformed shell customization, oh-my-customcode makes personali
|
|
|
21
21
|
|
|
22
22
|
| Feature | Description |
|
|
23
23
|
|---------|-------------|
|
|
24
|
-
| **Batteries Included** | 42 agents, 52 skills, 22 guides, 18 rules, 1 hook, 4 contexts - ready to use out of the box |
|
|
24
|
+
| **Batteries Included** | 42 agents, 52 skills, 22 guides, 18 rules, 1 hook, 4 contexts, ontology graph - ready to use out of the box |
|
|
25
25
|
| **Sub-Agent Model** | Supports hierarchical agent orchestration with specialized roles |
|
|
26
26
|
| **Dead Simple Customization** | Create a folder + markdown file = new agent or skill |
|
|
27
27
|
| **Mix and Match** | Use built-in components, create your own, or combine both |
|
|
@@ -125,111 +125,20 @@ Claude Code selects the appropriate model and parallelizes independent tasks (up
|
|
|
125
125
|
| **QA** | 3 | qa-planner, qa-writer, qa-engineer |
|
|
126
126
|
| **Total** | **42** | |
|
|
127
127
|
|
|
128
|
-
Canonical agent IDs (`templates/.claude/agents/*.md`):
|
|
129
|
-
|
|
130
|
-
```text
|
|
131
|
-
arch-documenter
|
|
132
|
-
arch-speckit-agent
|
|
133
|
-
be-express-expert
|
|
134
|
-
be-fastapi-expert
|
|
135
|
-
be-go-backend-expert
|
|
136
|
-
be-nestjs-expert
|
|
137
|
-
be-springboot-expert
|
|
138
|
-
db-postgres-expert
|
|
139
|
-
db-redis-expert
|
|
140
|
-
db-supabase-expert
|
|
141
|
-
de-airflow-expert
|
|
142
|
-
de-dbt-expert
|
|
143
|
-
de-kafka-expert
|
|
144
|
-
de-pipeline-expert
|
|
145
|
-
de-snowflake-expert
|
|
146
|
-
de-spark-expert
|
|
147
|
-
fe-svelte-agent
|
|
148
|
-
fe-vercel-agent
|
|
149
|
-
fe-vuejs-agent
|
|
150
|
-
infra-aws-expert
|
|
151
|
-
infra-docker-expert
|
|
152
|
-
lang-golang-expert
|
|
153
|
-
lang-java21-expert
|
|
154
|
-
lang-kotlin-expert
|
|
155
|
-
lang-python-expert
|
|
156
|
-
lang-rust-expert
|
|
157
|
-
lang-typescript-expert
|
|
158
|
-
mgr-claude-code-bible
|
|
159
|
-
mgr-creator
|
|
160
|
-
mgr-gitnerd
|
|
161
|
-
mgr-sauron
|
|
162
|
-
mgr-supplier
|
|
163
|
-
mgr-sync-checker
|
|
164
|
-
mgr-updater
|
|
165
|
-
qa-engineer
|
|
166
|
-
qa-planner
|
|
167
|
-
qa-writer
|
|
168
|
-
sys-memory-keeper
|
|
169
|
-
sys-naggy
|
|
170
|
-
tool-bun-expert
|
|
171
|
-
tool-npm-expert
|
|
172
|
-
tool-optimizer
|
|
173
|
-
```
|
|
174
|
-
|
|
175
128
|
### Skills (52)
|
|
176
129
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
airflow-best-practices
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
dev-refactor
|
|
190
|
-
dev-review
|
|
191
|
-
docker-best-practices
|
|
192
|
-
fastapi-best-practices
|
|
193
|
-
fix-refs
|
|
194
|
-
go-backend-best-practices
|
|
195
|
-
go-best-practices
|
|
196
|
-
help
|
|
197
|
-
intent-detection
|
|
198
|
-
kafka-best-practices
|
|
199
|
-
kotlin-best-practices
|
|
200
|
-
lists
|
|
201
|
-
memory-management
|
|
202
|
-
memory-recall
|
|
203
|
-
memory-save
|
|
204
|
-
monitoring-setup
|
|
205
|
-
npm-audit
|
|
206
|
-
npm-publish
|
|
207
|
-
npm-version
|
|
208
|
-
optimize-analyze
|
|
209
|
-
optimize-bundle
|
|
210
|
-
optimize-report
|
|
211
|
-
pipeline-architecture-patterns
|
|
212
|
-
postgres-best-practices
|
|
213
|
-
python-best-practices
|
|
214
|
-
qa-lead-routing
|
|
215
|
-
react-best-practices
|
|
216
|
-
redis-best-practices
|
|
217
|
-
result-aggregation
|
|
218
|
-
rust-best-practices
|
|
219
|
-
sauron-watch
|
|
220
|
-
secretary-routing
|
|
221
|
-
snowflake-best-practices
|
|
222
|
-
spark-best-practices
|
|
223
|
-
springboot-best-practices
|
|
224
|
-
status
|
|
225
|
-
supabase-postgres-best-practices
|
|
226
|
-
typescript-best-practices
|
|
227
|
-
update-docs
|
|
228
|
-
update-external
|
|
229
|
-
vercel-deploy
|
|
230
|
-
web-design-guidelines
|
|
231
|
-
writing-clearly-and-concisely
|
|
232
|
-
```
|
|
130
|
+
| Category | Count | Skills |
|
|
131
|
+
|----------|-------|--------|
|
|
132
|
+
| **Routing** | 4 | secretary-routing, dev-lead-routing, de-lead-routing, qa-lead-routing |
|
|
133
|
+
| **Best Practices** | 18 | go-best-practices, python-best-practices, typescript-best-practices, kotlin-best-practices, rust-best-practices, react-best-practices, fastapi-best-practices, springboot-best-practices, go-backend-best-practices, docker-best-practices, aws-best-practices, postgres-best-practices, supabase-postgres-best-practices, redis-best-practices, airflow-best-practices, dbt-best-practices, kafka-best-practices, snowflake-best-practices |
|
|
134
|
+
| **Development** | 5 | dev-review, dev-refactor, create-agent, intent-detection, web-design-guidelines |
|
|
135
|
+
| **Data Engineering** | 2 | spark-best-practices, pipeline-architecture-patterns |
|
|
136
|
+
| **Optimization** | 3 | optimize-analyze, optimize-bundle, optimize-report |
|
|
137
|
+
| **Memory** | 3 | memory-save, memory-recall, memory-management |
|
|
138
|
+
| **Package Management** | 3 | npm-publish, npm-version, npm-audit |
|
|
139
|
+
| **Operations** | 7 | update-docs, update-external, audit-agents, fix-refs, sauron-watch, monitoring-setup, claude-code-bible |
|
|
140
|
+
| **Utilities** | 5 | lists, help, status, result-aggregation, writing-clearly-and-concisely |
|
|
141
|
+
| **Deploy** | 2 | vercel-deploy, codex-exec |
|
|
233
142
|
|
|
234
143
|
### Guides (22)
|
|
235
144
|
|
|
@@ -260,9 +169,20 @@ Shared context files for cross-agent knowledge and mode configurations.
|
|
|
260
169
|
|
|
261
170
|
### Packages
|
|
262
171
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
172
|
+
#### [ontology-rag](packages/ontology-rag/)
|
|
173
|
+
|
|
174
|
+
Ontology+RAG context engine for intelligent agent context loading.
|
|
175
|
+
|
|
176
|
+
| Feature | Description |
|
|
177
|
+
|---------|-------------|
|
|
178
|
+
| **Ontology Loading** | Parse YAML ontologies (agents, skills, rules) |
|
|
179
|
+
| **Graph Traversal** | Navigate dependency graphs with BFS and PageRank |
|
|
180
|
+
| **Semantic Routing** | LLM-based agent selection with keyword fallback |
|
|
181
|
+
| **Hybrid Search** | 4-signal ranking (keyword, graph, community, importance) |
|
|
182
|
+
| **Token Budget** | Adaptive budget management — reduces token usage by 75-95% |
|
|
183
|
+
| **MCP Server** | Direct integration with Claude Code via MCP protocol |
|
|
184
|
+
|
|
185
|
+
Automatically configured during `omcustom init` when [uv](https://docs.astral.sh/uv/) is available.
|
|
266
186
|
|
|
267
187
|
---
|
|
268
188
|
|
|
@@ -277,6 +197,7 @@ Shared context files for cross-agent knowledge and mode configurations.
|
|
|
277
197
|
| `omcustom list agents` | List agents only |
|
|
278
198
|
| `omcustom doctor` | Verify installation health |
|
|
279
199
|
| `omcustom doctor --fix` | Auto-fix common issues |
|
|
200
|
+
| `omcustom security` | Scan for security issues in hooks and configs |
|
|
280
201
|
|
|
281
202
|
**Global Options:**
|
|
282
203
|
| Option | Description |
|
|
@@ -308,6 +229,12 @@ your-project/
|
|
|
308
229
|
│ ├── react-best-practices/
|
|
309
230
|
│ ├── secretary-routing/
|
|
310
231
|
│ └── ...
|
|
232
|
+
├── ontology/ # Ontology knowledge graph for RAG context
|
|
233
|
+
│ ├── schema.yaml
|
|
234
|
+
│ ├── agents.yaml
|
|
235
|
+
│ ├── skills.yaml
|
|
236
|
+
│ ├── rules.yaml
|
|
237
|
+
│ └── graphs/
|
|
311
238
|
└── guides/ # Reference docs (22 total)
|
|
312
239
|
```
|
|
313
240
|
|
package/dist/cli/index.js
CHANGED
|
@@ -11852,6 +11852,30 @@ var en_default = {
|
|
|
11852
11852
|
fail: "Some index.yaml files are invalid"
|
|
11853
11853
|
}
|
|
11854
11854
|
}
|
|
11855
|
+
},
|
|
11856
|
+
security: {
|
|
11857
|
+
description: "Scan for security issues in hooks, configs, and templates",
|
|
11858
|
+
verboseOption: "Show detailed scan results",
|
|
11859
|
+
scanning: "Running security scan...",
|
|
11860
|
+
passed: "Security scan passed! No issues found.",
|
|
11861
|
+
failed: "Security issues detected.",
|
|
11862
|
+
summary: "Security: {{pass}} passed, {{warn}} warnings, {{fail}} failed",
|
|
11863
|
+
checks: {
|
|
11864
|
+
hooks: {
|
|
11865
|
+
pass: "Hook scripts are safe",
|
|
11866
|
+
warn: "Hook scripts have potential concerns",
|
|
11867
|
+
fail: "Hook scripts contain dangerous patterns"
|
|
11868
|
+
},
|
|
11869
|
+
secrets: {
|
|
11870
|
+
pass: "No secrets found in configuration files",
|
|
11871
|
+
fail: "Secrets or credentials found in configuration"
|
|
11872
|
+
},
|
|
11873
|
+
integrity: {
|
|
11874
|
+
pass: "Template file permissions are secure",
|
|
11875
|
+
warn: "Some files have overly permissive settings",
|
|
11876
|
+
fail: "Security-sensitive files found in project"
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11855
11879
|
}
|
|
11856
11880
|
},
|
|
11857
11881
|
init: {
|
|
@@ -12144,6 +12168,30 @@ var ko_default = {
|
|
|
12144
12168
|
fail: "일부 index.yaml 파일이 잘못됨"
|
|
12145
12169
|
}
|
|
12146
12170
|
}
|
|
12171
|
+
},
|
|
12172
|
+
security: {
|
|
12173
|
+
description: "훅, 설정, 템플릿의 보안 문제 검사",
|
|
12174
|
+
verboseOption: "상세 검사 결과 표시",
|
|
12175
|
+
scanning: "보안 검사 실행 중...",
|
|
12176
|
+
passed: "보안 검사 통과! 문제가 발견되지 않았습니다.",
|
|
12177
|
+
failed: "보안 문제가 발견되었습니다.",
|
|
12178
|
+
summary: "보안: {{pass}}개 통과, {{warn}}개 경고, {{fail}}개 실패",
|
|
12179
|
+
checks: {
|
|
12180
|
+
hooks: {
|
|
12181
|
+
pass: "훅 스크립트가 안전합니다",
|
|
12182
|
+
warn: "훅 스크립트에 잠재적 우려사항이 있습니다",
|
|
12183
|
+
fail: "훅 스크립트에 위험한 패턴이 포함되어 있습니다"
|
|
12184
|
+
},
|
|
12185
|
+
secrets: {
|
|
12186
|
+
pass: "설정 파일에 비밀 정보가 없습니다",
|
|
12187
|
+
fail: "설정 파일에서 비밀 정보 또는 인증 정보가 발견되었습니다"
|
|
12188
|
+
},
|
|
12189
|
+
integrity: {
|
|
12190
|
+
pass: "템플릿 파일 권한이 안전합니다",
|
|
12191
|
+
warn: "일부 파일의 권한이 과도하게 허용되어 있습니다",
|
|
12192
|
+
fail: "보안에 민감한 파일이 프로젝트에 존재합니다"
|
|
12193
|
+
}
|
|
12194
|
+
}
|
|
12147
12195
|
}
|
|
12148
12196
|
},
|
|
12149
12197
|
init: {
|
|
@@ -13083,6 +13131,7 @@ var CLAUDE_LAYOUT = {
|
|
|
13083
13131
|
".claude/contexts",
|
|
13084
13132
|
".claude/agents",
|
|
13085
13133
|
".claude/skills",
|
|
13134
|
+
".claude/ontology",
|
|
13086
13135
|
"guides"
|
|
13087
13136
|
]
|
|
13088
13137
|
};
|
|
@@ -13963,7 +14012,7 @@ async function install(options) {
|
|
|
13963
14012
|
return result;
|
|
13964
14013
|
}
|
|
13965
14014
|
function getAllComponents() {
|
|
13966
|
-
return ["rules", "agents", "skills", "guides", "hooks", "contexts"];
|
|
14015
|
+
return ["rules", "agents", "skills", "guides", "hooks", "contexts", "ontology"];
|
|
13967
14016
|
}
|
|
13968
14017
|
async function installComponent(targetDir, component, options) {
|
|
13969
14018
|
if (component === "entry-md") {
|
|
@@ -14635,6 +14684,320 @@ async function listCommand(type = "all", options = {}) {
|
|
|
14635
14684
|
}
|
|
14636
14685
|
}
|
|
14637
14686
|
|
|
14687
|
+
// src/cli/security.ts
|
|
14688
|
+
import { constants as constants2, promises as fs2 } from "node:fs";
|
|
14689
|
+
import path2 from "node:path";
|
|
14690
|
+
async function pathExists2(targetPath) {
|
|
14691
|
+
try {
|
|
14692
|
+
await fs2.access(targetPath, constants2.F_OK);
|
|
14693
|
+
return true;
|
|
14694
|
+
} catch {
|
|
14695
|
+
return false;
|
|
14696
|
+
}
|
|
14697
|
+
}
|
|
14698
|
+
function isValidUtf8Text(content) {
|
|
14699
|
+
try {
|
|
14700
|
+
content.toString("utf-8");
|
|
14701
|
+
return true;
|
|
14702
|
+
} catch {
|
|
14703
|
+
return false;
|
|
14704
|
+
}
|
|
14705
|
+
}
|
|
14706
|
+
async function findAllFiles(dir2) {
|
|
14707
|
+
const results = [];
|
|
14708
|
+
try {
|
|
14709
|
+
const entries = await fs2.readdir(dir2, { withFileTypes: true });
|
|
14710
|
+
for (const entry of entries) {
|
|
14711
|
+
const fullPath = path2.join(dir2, entry.name);
|
|
14712
|
+
if (entry.isDirectory()) {
|
|
14713
|
+
const subResults = await findAllFiles(fullPath);
|
|
14714
|
+
results.push(...subResults);
|
|
14715
|
+
} else if (entry.isFile()) {
|
|
14716
|
+
results.push(fullPath);
|
|
14717
|
+
}
|
|
14718
|
+
}
|
|
14719
|
+
} catch {}
|
|
14720
|
+
return results;
|
|
14721
|
+
}
|
|
14722
|
+
var DANGEROUS_PATTERNS = [
|
|
14723
|
+
{
|
|
14724
|
+
pattern: /rm\s+-rf\s+[/~]/,
|
|
14725
|
+
name: "rm -rf with root/home path",
|
|
14726
|
+
severity: "fail"
|
|
14727
|
+
},
|
|
14728
|
+
{
|
|
14729
|
+
pattern: /curl\s+.*\|\s*(bash|sh|eval)/,
|
|
14730
|
+
name: "curl pipe to shell",
|
|
14731
|
+
severity: "fail"
|
|
14732
|
+
},
|
|
14733
|
+
{
|
|
14734
|
+
pattern: /wget\s+.*\|\s*(bash|sh|eval)/,
|
|
14735
|
+
name: "wget pipe to shell",
|
|
14736
|
+
severity: "fail"
|
|
14737
|
+
},
|
|
14738
|
+
{ pattern: /\bsudo\b/, name: "sudo usage", severity: "warn" },
|
|
14739
|
+
{ pattern: /chmod\s+777/, name: "chmod 777", severity: "warn" },
|
|
14740
|
+
{ pattern: /\beval\s*\(/, name: "eval() usage", severity: "warn" },
|
|
14741
|
+
{
|
|
14742
|
+
pattern: /\$\{.*:-.*\}.*>\s*\/etc/,
|
|
14743
|
+
name: "write to /etc",
|
|
14744
|
+
severity: "fail"
|
|
14745
|
+
},
|
|
14746
|
+
{
|
|
14747
|
+
pattern: /base64\s+(-d|--decode).*\|\s*(bash|sh)/,
|
|
14748
|
+
name: "base64 decode to shell",
|
|
14749
|
+
severity: "fail"
|
|
14750
|
+
}
|
|
14751
|
+
];
|
|
14752
|
+
function extractCommands(hooks) {
|
|
14753
|
+
const commands = [];
|
|
14754
|
+
if (!hooks || typeof hooks !== "object")
|
|
14755
|
+
return commands;
|
|
14756
|
+
for (const hookName in hooks) {
|
|
14757
|
+
const hook = hooks[hookName];
|
|
14758
|
+
if (hook && typeof hook === "object") {
|
|
14759
|
+
for (const eventName in hook) {
|
|
14760
|
+
const event = hook[eventName];
|
|
14761
|
+
if (Array.isArray(event)) {
|
|
14762
|
+
for (const item of event) {
|
|
14763
|
+
if (typeof item === "object" && item && "command" in item) {
|
|
14764
|
+
commands.push(String(item.command));
|
|
14765
|
+
}
|
|
14766
|
+
}
|
|
14767
|
+
}
|
|
14768
|
+
}
|
|
14769
|
+
}
|
|
14770
|
+
}
|
|
14771
|
+
return commands;
|
|
14772
|
+
}
|
|
14773
|
+
function scanCommands(commands) {
|
|
14774
|
+
const findings = [];
|
|
14775
|
+
let worstSeverity = "pass";
|
|
14776
|
+
for (const command of commands) {
|
|
14777
|
+
for (const { pattern, name, severity } of DANGEROUS_PATTERNS) {
|
|
14778
|
+
if (pattern.test(command)) {
|
|
14779
|
+
findings.push(`${name}: ${command.substring(0, 80)}${command.length > 80 ? "..." : ""}`);
|
|
14780
|
+
if (severity === "fail") {
|
|
14781
|
+
worstSeverity = "fail";
|
|
14782
|
+
} else if (severity === "warn" && worstSeverity === "pass") {
|
|
14783
|
+
worstSeverity = "warn";
|
|
14784
|
+
}
|
|
14785
|
+
}
|
|
14786
|
+
}
|
|
14787
|
+
}
|
|
14788
|
+
return { findings, worstSeverity };
|
|
14789
|
+
}
|
|
14790
|
+
async function checkHookScripts(targetDir, rootDir = ".claude") {
|
|
14791
|
+
const hooksFile = path2.join(targetDir, rootDir, "hooks", "hooks.json");
|
|
14792
|
+
const exists2 = await pathExists2(hooksFile);
|
|
14793
|
+
if (!exists2) {
|
|
14794
|
+
return {
|
|
14795
|
+
name: "Hook scripts",
|
|
14796
|
+
status: "pass",
|
|
14797
|
+
message: i18n.t("cli.security.checks.hooks.pass"),
|
|
14798
|
+
fixable: false
|
|
14799
|
+
};
|
|
14800
|
+
}
|
|
14801
|
+
try {
|
|
14802
|
+
const content = await fs2.readFile(hooksFile, "utf-8");
|
|
14803
|
+
const hooks = JSON.parse(content);
|
|
14804
|
+
const commands = extractCommands(hooks);
|
|
14805
|
+
const { findings, worstSeverity } = scanCommands(commands);
|
|
14806
|
+
if (findings.length > 0) {
|
|
14807
|
+
const message = worstSeverity === "fail" ? i18n.t("cli.security.checks.hooks.fail") : i18n.t("cli.security.checks.hooks.warn");
|
|
14808
|
+
return {
|
|
14809
|
+
name: "Hook scripts",
|
|
14810
|
+
status: worstSeverity,
|
|
14811
|
+
message: `${message} (${findings.length} issues)`,
|
|
14812
|
+
fixable: false,
|
|
14813
|
+
details: findings
|
|
14814
|
+
};
|
|
14815
|
+
}
|
|
14816
|
+
return {
|
|
14817
|
+
name: "Hook scripts",
|
|
14818
|
+
status: "pass",
|
|
14819
|
+
message: i18n.t("cli.security.checks.hooks.pass"),
|
|
14820
|
+
fixable: false
|
|
14821
|
+
};
|
|
14822
|
+
} catch (error2) {
|
|
14823
|
+
return {
|
|
14824
|
+
name: "Hook scripts",
|
|
14825
|
+
status: "warn",
|
|
14826
|
+
message: `Failed to parse hooks.json: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
14827
|
+
fixable: false
|
|
14828
|
+
};
|
|
14829
|
+
}
|
|
14830
|
+
}
|
|
14831
|
+
async function checkConfigSecrets(targetDir, rootDir = ".claude") {
|
|
14832
|
+
const configDir = path2.join(targetDir, rootDir);
|
|
14833
|
+
const exists2 = await pathExists2(configDir);
|
|
14834
|
+
if (!exists2) {
|
|
14835
|
+
return {
|
|
14836
|
+
name: "Config secrets",
|
|
14837
|
+
status: "pass",
|
|
14838
|
+
message: i18n.t("cli.security.checks.secrets.pass"),
|
|
14839
|
+
fixable: false
|
|
14840
|
+
};
|
|
14841
|
+
}
|
|
14842
|
+
const SECRET_PATTERNS = [
|
|
14843
|
+
{
|
|
14844
|
+
pattern: /(?:AWS_SECRET|AWS_ACCESS_KEY|AWS_SESSION)[_A-Z]*\s*[=:]\s*['"]?[A-Za-z0-9/+=]{20,}/,
|
|
14845
|
+
name: "AWS credential"
|
|
14846
|
+
},
|
|
14847
|
+
{
|
|
14848
|
+
pattern: /(?:GITHUB_TOKEN|GH_TOKEN|GITHUB_PAT)\s*[=:]\s*['"]?(?:ghp_|gho_|ghs_|ghr_|github_pat_)[A-Za-z0-9_]+/,
|
|
14849
|
+
name: "GitHub token"
|
|
14850
|
+
},
|
|
14851
|
+
{
|
|
14852
|
+
pattern: /(?:sk-|sk_live_|sk_test_)[A-Za-z0-9]{20,}/,
|
|
14853
|
+
name: "API secret key (sk-*)"
|
|
14854
|
+
},
|
|
14855
|
+
{
|
|
14856
|
+
pattern: /(?:password|passwd|secret)\s*[=:]\s*['"]?[^\s'"]{8,}/i,
|
|
14857
|
+
name: "Hardcoded password/secret"
|
|
14858
|
+
},
|
|
14859
|
+
{
|
|
14860
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
|
|
14861
|
+
name: "Private key"
|
|
14862
|
+
}
|
|
14863
|
+
];
|
|
14864
|
+
const files = await findAllFiles(configDir);
|
|
14865
|
+
const findings = [];
|
|
14866
|
+
for (const file of files) {
|
|
14867
|
+
try {
|
|
14868
|
+
const content = await fs2.readFile(file);
|
|
14869
|
+
if (!isValidUtf8Text(content)) {
|
|
14870
|
+
continue;
|
|
14871
|
+
}
|
|
14872
|
+
const text = content.toString("utf-8");
|
|
14873
|
+
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
14874
|
+
if (pattern.test(text)) {
|
|
14875
|
+
const relativePath = path2.relative(targetDir, file);
|
|
14876
|
+
findings.push(`${relativePath}: ${name}`);
|
|
14877
|
+
}
|
|
14878
|
+
}
|
|
14879
|
+
} catch {}
|
|
14880
|
+
}
|
|
14881
|
+
if (findings.length > 0) {
|
|
14882
|
+
return {
|
|
14883
|
+
name: "Config secrets",
|
|
14884
|
+
status: "fail",
|
|
14885
|
+
message: `${i18n.t("cli.security.checks.secrets.fail")} (${findings.length} found)`,
|
|
14886
|
+
fixable: false,
|
|
14887
|
+
details: findings
|
|
14888
|
+
};
|
|
14889
|
+
}
|
|
14890
|
+
return {
|
|
14891
|
+
name: "Config secrets",
|
|
14892
|
+
status: "pass",
|
|
14893
|
+
message: i18n.t("cli.security.checks.secrets.pass"),
|
|
14894
|
+
fixable: false
|
|
14895
|
+
};
|
|
14896
|
+
}
|
|
14897
|
+
async function checkEnvFiles(targetDir) {
|
|
14898
|
+
const findings = [];
|
|
14899
|
+
let severity = "pass";
|
|
14900
|
+
const envFiles = [".env", ".env.local", ".env.production", ".env.development"];
|
|
14901
|
+
for (const envFile of envFiles) {
|
|
14902
|
+
const envPath = path2.join(targetDir, envFile);
|
|
14903
|
+
if (await pathExists2(envPath)) {
|
|
14904
|
+
findings.push(`Security-sensitive file found: ${envFile}`);
|
|
14905
|
+
severity = "fail";
|
|
14906
|
+
}
|
|
14907
|
+
}
|
|
14908
|
+
return { findings, severity };
|
|
14909
|
+
}
|
|
14910
|
+
async function checkShellPermissions(targetDir, shellScripts) {
|
|
14911
|
+
const findings = [];
|
|
14912
|
+
let severity = "pass";
|
|
14913
|
+
for (const script of shellScripts) {
|
|
14914
|
+
try {
|
|
14915
|
+
const stats = await fs2.stat(script);
|
|
14916
|
+
const mode = stats.mode & 511;
|
|
14917
|
+
const relativePath = path2.relative(targetDir, script);
|
|
14918
|
+
if (mode === 511) {
|
|
14919
|
+
findings.push(`Overly permissive permissions (777): ${relativePath}`);
|
|
14920
|
+
if (severity === "pass") {
|
|
14921
|
+
severity = "warn";
|
|
14922
|
+
}
|
|
14923
|
+
} else if (mode & 2) {
|
|
14924
|
+
findings.push(`World-writable: ${relativePath}`);
|
|
14925
|
+
if (severity === "pass") {
|
|
14926
|
+
severity = "warn";
|
|
14927
|
+
}
|
|
14928
|
+
}
|
|
14929
|
+
} catch {}
|
|
14930
|
+
}
|
|
14931
|
+
return { findings, severity };
|
|
14932
|
+
}
|
|
14933
|
+
async function checkTemplateIntegrity(targetDir) {
|
|
14934
|
+
let worstSeverity = "pass";
|
|
14935
|
+
const allFindings = [];
|
|
14936
|
+
const envCheck = await checkEnvFiles(targetDir);
|
|
14937
|
+
allFindings.push(...envCheck.findings);
|
|
14938
|
+
if (envCheck.severity === "fail") {
|
|
14939
|
+
worstSeverity = "fail";
|
|
14940
|
+
}
|
|
14941
|
+
const allFiles = await findAllFiles(targetDir);
|
|
14942
|
+
const shellScripts = allFiles.filter((f) => f.endsWith(".sh"));
|
|
14943
|
+
const permCheck = await checkShellPermissions(targetDir, shellScripts);
|
|
14944
|
+
allFindings.push(...permCheck.findings);
|
|
14945
|
+
if (permCheck.severity === "warn" && worstSeverity === "pass") {
|
|
14946
|
+
worstSeverity = "warn";
|
|
14947
|
+
}
|
|
14948
|
+
if (allFindings.length > 0) {
|
|
14949
|
+
const message = worstSeverity === "fail" ? i18n.t("cli.security.checks.integrity.fail") : i18n.t("cli.security.checks.integrity.warn");
|
|
14950
|
+
return {
|
|
14951
|
+
name: "Template integrity",
|
|
14952
|
+
status: worstSeverity,
|
|
14953
|
+
message: `${message} (${allFindings.length} issues)`,
|
|
14954
|
+
fixable: false,
|
|
14955
|
+
details: allFindings
|
|
14956
|
+
};
|
|
14957
|
+
}
|
|
14958
|
+
return {
|
|
14959
|
+
name: "Template integrity",
|
|
14960
|
+
status: "pass",
|
|
14961
|
+
message: i18n.t("cli.security.checks.integrity.pass"),
|
|
14962
|
+
fixable: false
|
|
14963
|
+
};
|
|
14964
|
+
}
|
|
14965
|
+
async function securityCommand(_options = {}) {
|
|
14966
|
+
const targetDir = process.cwd();
|
|
14967
|
+
console.log(i18n.t("cli.security.scanning"));
|
|
14968
|
+
console.log("");
|
|
14969
|
+
const layout = getProviderLayout();
|
|
14970
|
+
const checks = await Promise.all([
|
|
14971
|
+
checkHookScripts(targetDir, layout.rootDir),
|
|
14972
|
+
checkConfigSecrets(targetDir, layout.rootDir),
|
|
14973
|
+
checkTemplateIntegrity(targetDir)
|
|
14974
|
+
]);
|
|
14975
|
+
for (const check of checks) {
|
|
14976
|
+
printCheck(check);
|
|
14977
|
+
}
|
|
14978
|
+
const passCount = checks.filter((c) => c.status === "pass").length;
|
|
14979
|
+
const warnCount = checks.filter((c) => c.status === "warn").length;
|
|
14980
|
+
const failCount = checks.filter((c) => c.status === "fail").length;
|
|
14981
|
+
console.log("");
|
|
14982
|
+
if (failCount === 0 && warnCount === 0) {
|
|
14983
|
+
console.log(i18n.t("cli.security.passed"));
|
|
14984
|
+
} else {
|
|
14985
|
+
console.log(i18n.t("cli.security.failed"));
|
|
14986
|
+
}
|
|
14987
|
+
console.log(i18n.t("cli.security.summary", {
|
|
14988
|
+
pass: passCount,
|
|
14989
|
+
warn: warnCount,
|
|
14990
|
+
fail: failCount
|
|
14991
|
+
}));
|
|
14992
|
+
return {
|
|
14993
|
+
success: failCount === 0,
|
|
14994
|
+
checks,
|
|
14995
|
+
passCount,
|
|
14996
|
+
warnCount,
|
|
14997
|
+
failCount
|
|
14998
|
+
};
|
|
14999
|
+
}
|
|
15000
|
+
|
|
14638
15001
|
// src/core/updater.ts
|
|
14639
15002
|
import { join as join8 } from "node:path";
|
|
14640
15003
|
|
|
@@ -14807,11 +15170,11 @@ function getEntryTemplateName2(language) {
|
|
|
14807
15170
|
return language === "ko" ? `${baseName}.md.ko` : `${baseName}.md.en`;
|
|
14808
15171
|
}
|
|
14809
15172
|
async function backupFile(filePath) {
|
|
14810
|
-
const
|
|
15173
|
+
const fs3 = await import("node:fs/promises");
|
|
14811
15174
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
14812
15175
|
const backupPath = `${filePath}.backup-${timestamp}`;
|
|
14813
15176
|
if (await fileExists(filePath)) {
|
|
14814
|
-
await
|
|
15177
|
+
await fs3.copyFile(filePath, backupPath);
|
|
14815
15178
|
debug("update.file_backed_up", { path: filePath, backup: backupPath });
|
|
14816
15179
|
}
|
|
14817
15180
|
}
|
|
@@ -14987,7 +15350,7 @@ async function checkForUpdates(targetDir) {
|
|
|
14987
15350
|
};
|
|
14988
15351
|
}
|
|
14989
15352
|
function getAllUpdateComponents() {
|
|
14990
|
-
return ["rules", "agents", "skills", "guides", "hooks", "contexts"];
|
|
15353
|
+
return ["rules", "agents", "skills", "guides", "hooks", "contexts", "ontology"];
|
|
14991
15354
|
}
|
|
14992
15355
|
async function getLatestVersion() {
|
|
14993
15356
|
const layout = getProviderLayout();
|
|
@@ -15023,8 +15386,8 @@ async function updateComponent(targetDir, component, customizations, options, co
|
|
|
15023
15386
|
skipPaths.push(cc.path);
|
|
15024
15387
|
}
|
|
15025
15388
|
}
|
|
15026
|
-
const
|
|
15027
|
-
const normalizedSkipPaths = skipPaths.map((p) =>
|
|
15389
|
+
const path3 = await import("node:path");
|
|
15390
|
+
const normalizedSkipPaths = skipPaths.map((p) => path3.relative(destPath, join8(targetDir, p)));
|
|
15028
15391
|
await copyDirectory(srcPath, destPath, {
|
|
15029
15392
|
overwrite: true,
|
|
15030
15393
|
skipPaths: normalizedSkipPaths.length > 0 ? normalizedSkipPaths : undefined
|
|
@@ -15045,7 +15408,7 @@ function getComponentPath2(component) {
|
|
|
15045
15408
|
async function backupInstallation(targetDir) {
|
|
15046
15409
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
15047
15410
|
const backupDir = join8(targetDir, `.omcustom-backup-${timestamp}`);
|
|
15048
|
-
const
|
|
15411
|
+
const fs3 = await import("node:fs/promises");
|
|
15049
15412
|
await ensureDirectory(backupDir);
|
|
15050
15413
|
const layout = getProviderLayout();
|
|
15051
15414
|
const dirsToBackup = [layout.rootDir, "guides"];
|
|
@@ -15058,7 +15421,7 @@ async function backupInstallation(targetDir) {
|
|
|
15058
15421
|
}
|
|
15059
15422
|
const entryPath = join8(targetDir, layout.entryFile);
|
|
15060
15423
|
if (await fileExists(entryPath)) {
|
|
15061
|
-
await
|
|
15424
|
+
await fs3.copyFile(entryPath, join8(backupDir, layout.entryFile));
|
|
15062
15425
|
}
|
|
15063
15426
|
return backupDir;
|
|
15064
15427
|
}
|
|
@@ -15131,8 +15494,8 @@ function printUpdateResults(result) {
|
|
|
15131
15494
|
console.log(i18n.t("cli.update.preservedFiles", { count: result.preservedFiles.length }));
|
|
15132
15495
|
}
|
|
15133
15496
|
if (result.backedUpPaths.length > 0) {
|
|
15134
|
-
for (const
|
|
15135
|
-
console.log(i18n.t("cli.update.backupCreated", { path:
|
|
15497
|
+
for (const path3 of result.backedUpPaths) {
|
|
15498
|
+
console.log(i18n.t("cli.update.backupCreated", { path: path3 }));
|
|
15136
15499
|
}
|
|
15137
15500
|
}
|
|
15138
15501
|
for (const warning of result.warnings) {
|
|
@@ -15169,6 +15532,10 @@ function createProgram() {
|
|
|
15169
15532
|
program2.command("doctor").description(i18n.t("cli.doctor.description")).option("--fix", i18n.t("cli.doctor.fixOption")).action(async (options) => {
|
|
15170
15533
|
await doctorCommand(options);
|
|
15171
15534
|
});
|
|
15535
|
+
program2.command("security").description(i18n.t("cli.security.description")).option("--verbose", i18n.t("cli.security.verboseOption")).action(async (options) => {
|
|
15536
|
+
const result = await securityCommand(options);
|
|
15537
|
+
process.exitCode = result.success ? 0 : 1;
|
|
15538
|
+
});
|
|
15172
15539
|
program2.hook("preAction", async (thisCommand, actionCommand) => {
|
|
15173
15540
|
const opts = thisCommand.optsWithGlobals();
|
|
15174
15541
|
const skipCheck = opts.skipVersionCheck || false;
|
package/dist/index.js
CHANGED
|
@@ -771,6 +771,7 @@ var CLAUDE_LAYOUT = {
|
|
|
771
771
|
".claude/contexts",
|
|
772
772
|
".claude/agents",
|
|
773
773
|
".claude/skills",
|
|
774
|
+
".claude/ontology",
|
|
774
775
|
"guides"
|
|
775
776
|
]
|
|
776
777
|
};
|
|
@@ -929,7 +930,7 @@ async function getTemplateManifest() {
|
|
|
929
930
|
};
|
|
930
931
|
}
|
|
931
932
|
function getAllComponents() {
|
|
932
|
-
return ["rules", "agents", "skills", "guides", "hooks", "contexts"];
|
|
933
|
+
return ["rules", "agents", "skills", "guides", "hooks", "contexts", "ontology"];
|
|
933
934
|
}
|
|
934
935
|
async function installComponent(targetDir, component, options) {
|
|
935
936
|
if (component === "entry-md") {
|
|
@@ -1409,7 +1410,7 @@ async function preserveCustomizations(targetDir, customizations) {
|
|
|
1409
1410
|
return preserved;
|
|
1410
1411
|
}
|
|
1411
1412
|
function getAllUpdateComponents() {
|
|
1412
|
-
return ["rules", "agents", "skills", "guides", "hooks", "contexts"];
|
|
1413
|
+
return ["rules", "agents", "skills", "guides", "hooks", "contexts", "ontology"];
|
|
1413
1414
|
}
|
|
1414
1415
|
async function getLatestVersion() {
|
|
1415
1416
|
const layout = getProviderLayout();
|
package/package.json
CHANGED
package/templates/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"lastUpdated": "2026-02-
|
|
2
|
+
"version": "0.3.0",
|
|
3
|
+
"lastUpdated": "2026-02-18T00:00:00.000Z",
|
|
4
4
|
"components": [
|
|
5
5
|
{
|
|
6
6
|
"name": "rules",
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
"path": ".claude/contexts",
|
|
38
38
|
"description": "Context configuration files",
|
|
39
39
|
"files": 4
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "ontology",
|
|
43
|
+
"path": ".claude/ontology",
|
|
44
|
+
"description": "Ontology knowledge graph for RAG-powered agent context",
|
|
45
|
+
"files": 8
|
|
40
46
|
}
|
|
41
47
|
],
|
|
42
48
|
"source": "https://github.com/baekenough/oh-my-customcode"
|