@viren/claude-code-dashboard 0.0.1
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.md +195 -0
- package/generate-dashboard.mjs +637 -0
- package/package.json +42 -0
- package/src/analysis.mjs +262 -0
- package/src/cli.mjs +135 -0
- package/src/constants.mjs +150 -0
- package/src/discovery.mjs +46 -0
- package/src/freshness.mjs +35 -0
- package/src/helpers.mjs +42 -0
- package/src/html-template.mjs +744 -0
- package/src/markdown.mjs +142 -0
- package/src/mcp.mjs +86 -0
- package/src/render.mjs +264 -0
- package/src/skills.mjs +135 -0
- package/src/templates.mjs +221 -0
- package/src/usage.mjs +60 -0
- package/src/watch.mjs +54 -0
package/src/analysis.mjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { STACK_FILES } from "./constants.mjs";
|
|
4
|
+
import { gitCmd } from "./helpers.mjs";
|
|
5
|
+
|
|
6
|
+
export function computeHealthScore(repo) {
|
|
7
|
+
let score = 0;
|
|
8
|
+
const reasons = [];
|
|
9
|
+
|
|
10
|
+
// Has AGENTS.md/CLAUDE.md (30 points)
|
|
11
|
+
if (repo.hasAgentsFile) {
|
|
12
|
+
score += 30;
|
|
13
|
+
} else {
|
|
14
|
+
reasons.push("add CLAUDE.md");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Has description (10 points)
|
|
18
|
+
if (repo.desc && repo.desc.length > 0) {
|
|
19
|
+
score += 10;
|
|
20
|
+
} else {
|
|
21
|
+
reasons.push("add project description");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Has commands (20 points)
|
|
25
|
+
if (repo.commandCount > 0) {
|
|
26
|
+
score += Math.min(20, repo.commandCount * 10);
|
|
27
|
+
} else {
|
|
28
|
+
reasons.push("add commands");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Has rules (20 points)
|
|
32
|
+
if (repo.ruleCount > 0) {
|
|
33
|
+
score += Math.min(20, repo.ruleCount * 10);
|
|
34
|
+
} else {
|
|
35
|
+
reasons.push("add rules");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Has sections / structured config (10 points)
|
|
39
|
+
if (repo.sectionCount > 0) {
|
|
40
|
+
score += Math.min(10, repo.sectionCount * 2);
|
|
41
|
+
} else {
|
|
42
|
+
reasons.push("add structured sections");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Freshness (10 points)
|
|
46
|
+
if (repo.freshnessClass === "fresh") {
|
|
47
|
+
score += 10;
|
|
48
|
+
} else if (repo.freshnessClass === "aging") {
|
|
49
|
+
score += 5;
|
|
50
|
+
reasons.push("update config (aging)");
|
|
51
|
+
} else {
|
|
52
|
+
reasons.push("update config (stale)");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { score: Math.min(100, score), reasons };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function detectTechStack(repoDir) {
|
|
59
|
+
const stacks = new Set();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const entries = new Set(readdirSync(repoDir));
|
|
63
|
+
|
|
64
|
+
for (const [file, stack] of Object.entries(STACK_FILES)) {
|
|
65
|
+
if (entries.has(file)) stacks.add(stack);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (entries.has("package.json")) {
|
|
69
|
+
// Check for React/Expo in package.json if no framework detected yet
|
|
70
|
+
if (!stacks.has("next") && !stacks.has("expo")) {
|
|
71
|
+
try {
|
|
72
|
+
const pkg = JSON.parse(readFileSync(join(repoDir, "package.json"), "utf8"));
|
|
73
|
+
const allDeps = {
|
|
74
|
+
...(pkg.dependencies || {}),
|
|
75
|
+
...(pkg.devDependencies || {}),
|
|
76
|
+
};
|
|
77
|
+
if (allDeps["expo"]) stacks.add("expo");
|
|
78
|
+
else if (allDeps["next"]) stacks.add("next");
|
|
79
|
+
else if (allDeps["react"]) stacks.add("react");
|
|
80
|
+
} catch {
|
|
81
|
+
/* malformed package.json */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (stacks.size === 0) stacks.add("node");
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
/* unreadable dir */
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { stacks: [...stacks] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function computeDrift(repoDir, configTimestamp) {
|
|
94
|
+
if (!configTimestamp) return { level: "unknown", commitsSince: 0 };
|
|
95
|
+
|
|
96
|
+
// Count commits since the config was last updated
|
|
97
|
+
const countStr = gitCmd(repoDir, "rev-list", "--count", `--since=${configTimestamp}`, "HEAD");
|
|
98
|
+
if (!countStr) return { level: "unknown", commitsSince: 0 };
|
|
99
|
+
|
|
100
|
+
const parsed = Number(countStr);
|
|
101
|
+
if (!Number.isFinite(parsed)) return { level: "unknown", commitsSince: 0 };
|
|
102
|
+
|
|
103
|
+
const commitsSince = Math.max(0, parsed - 1); // -1 to exclude the config commit itself
|
|
104
|
+
|
|
105
|
+
if (commitsSince === 0) return { level: "synced", commitsSince: 0 };
|
|
106
|
+
if (commitsSince <= 5) return { level: "low", commitsSince };
|
|
107
|
+
if (commitsSince <= 20) return { level: "medium", commitsSince };
|
|
108
|
+
return { level: "high", commitsSince };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function findExemplar(stack, configuredRepos) {
|
|
112
|
+
if (!stack || stack.length === 0) return null;
|
|
113
|
+
let best = null;
|
|
114
|
+
let bestScore = -1;
|
|
115
|
+
for (const repo of configuredRepos) {
|
|
116
|
+
const repoStacks = repo.techStack || [];
|
|
117
|
+
const overlap = stack.filter((s) => repoStacks.includes(s)).length;
|
|
118
|
+
const score = overlap * 100 + (repo.healthScore || 0);
|
|
119
|
+
if (overlap > 0 && score > bestScore) {
|
|
120
|
+
bestScore = score;
|
|
121
|
+
best = repo;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return best;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function generateSuggestions(exemplar) {
|
|
128
|
+
if (!exemplar) return [];
|
|
129
|
+
const suggestions = [];
|
|
130
|
+
if (exemplar.hasAgentsFile) suggestions.push("add CLAUDE.md");
|
|
131
|
+
if (exemplar.commands?.length > 0)
|
|
132
|
+
suggestions.push(`add commands (${exemplar.name} has ${exemplar.commands.length})`);
|
|
133
|
+
if (exemplar.rules?.length > 0)
|
|
134
|
+
suggestions.push(`add rules (${exemplar.name} has ${exemplar.rules.length})`);
|
|
135
|
+
return suggestions;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function detectConfigPattern(repo) {
|
|
139
|
+
if (repo.rules.length >= 3) return "modular";
|
|
140
|
+
if (repo.sections.length >= 3) return "monolithic";
|
|
141
|
+
if (repo.commands.length >= 2 && repo.sections.length === 0) return "command-heavy";
|
|
142
|
+
return "minimal";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function computeConfigSimilarity(repoA, repoB) {
|
|
146
|
+
if (!repoA || !repoB) return 0;
|
|
147
|
+
let matches = 0;
|
|
148
|
+
let total = 0;
|
|
149
|
+
|
|
150
|
+
// Section name overlap (Jaccard similarity)
|
|
151
|
+
const sectionsA = new Set((repoA.sections || []).map((s) => s.name || s));
|
|
152
|
+
const sectionsB = new Set((repoB.sections || []).map((s) => s.name || s));
|
|
153
|
+
if (sectionsA.size > 0 || sectionsB.size > 0) {
|
|
154
|
+
const intersection = [...sectionsA].filter((s) => sectionsB.has(s)).length;
|
|
155
|
+
const union = new Set([...sectionsA, ...sectionsB]).size;
|
|
156
|
+
matches += intersection;
|
|
157
|
+
total += union;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Stack overlap
|
|
161
|
+
const stackA = new Set(repoA.techStack || []);
|
|
162
|
+
const stackB = new Set(repoB.techStack || []);
|
|
163
|
+
if (stackA.size > 0 || stackB.size > 0) {
|
|
164
|
+
const intersection = [...stackA].filter((s) => stackB.has(s)).length;
|
|
165
|
+
const union = new Set([...stackA, ...stackB]).size;
|
|
166
|
+
matches += intersection;
|
|
167
|
+
total += union;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Same config pattern bonus
|
|
171
|
+
if (repoA.configPattern && repoA.configPattern === repoB.configPattern) {
|
|
172
|
+
matches += 1;
|
|
173
|
+
total += 1;
|
|
174
|
+
} else {
|
|
175
|
+
total += 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return total > 0 ? Math.round((matches / total) * 100) : 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function matchSkillsToRepo(repo, skills) {
|
|
182
|
+
if (!repo || !skills || skills.length === 0) return [];
|
|
183
|
+
const repoTokens = new Set();
|
|
184
|
+
for (const word of repo.name.toLowerCase().split(/[-_./]/)) {
|
|
185
|
+
if (word.length > 1) repoTokens.add(word);
|
|
186
|
+
}
|
|
187
|
+
for (const s of repo.techStack || []) {
|
|
188
|
+
repoTokens.add(s.toLowerCase());
|
|
189
|
+
}
|
|
190
|
+
for (const sec of repo.sections || []) {
|
|
191
|
+
const name = (sec.name || sec || "").toLowerCase();
|
|
192
|
+
for (const word of name.split(/\s+/)) {
|
|
193
|
+
if (word.length > 2) repoTokens.add(word);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const matched = [];
|
|
197
|
+
for (const skill of skills) {
|
|
198
|
+
const skillTokens = skill.name
|
|
199
|
+
.toLowerCase()
|
|
200
|
+
.split(/[-_./]/)
|
|
201
|
+
.filter((t) => t.length > 1);
|
|
202
|
+
const hits = skillTokens.filter((t) => repoTokens.has(t)).length;
|
|
203
|
+
if (hits > 0) matched.push({ name: skill.name, relevance: hits });
|
|
204
|
+
}
|
|
205
|
+
return matched.sort((a, b) => b.relevance - a.relevance).slice(0, 5);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function lintConfig(repo) {
|
|
209
|
+
const issues = [];
|
|
210
|
+
if (repo.sections) {
|
|
211
|
+
for (const sec of repo.sections) {
|
|
212
|
+
const name = sec.name || sec || "";
|
|
213
|
+
if (/TODO|FIXME|HACK/i.test(name)) {
|
|
214
|
+
issues.push({ level: "warn", message: `Section "${name}" contains TODO/FIXME marker` });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!repo.hasAgentsFile && (repo.commands.length > 0 || repo.rules.length > 0)) {
|
|
219
|
+
issues.push({ level: "info", message: "Has commands/rules but no CLAUDE.md" });
|
|
220
|
+
}
|
|
221
|
+
if (
|
|
222
|
+
repo.hasAgentsFile &&
|
|
223
|
+
repo.commands.length === 0 &&
|
|
224
|
+
repo.rules.length === 0 &&
|
|
225
|
+
(repo.sections || []).length === 0
|
|
226
|
+
) {
|
|
227
|
+
issues.push({
|
|
228
|
+
level: "warn",
|
|
229
|
+
message: "CLAUDE.md exists but has no commands, rules, or sections",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return issues;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function computeDashboardDiff(prev, current) {
|
|
236
|
+
const diff = { added: [], removed: [], changed: [] };
|
|
237
|
+
if (!prev || !current) return diff;
|
|
238
|
+
const prevNames = new Set((prev.configuredRepos || []).map((r) => r.name));
|
|
239
|
+
const currNames = new Set((current.configuredRepos || []).map((r) => r.name));
|
|
240
|
+
for (const name of currNames) {
|
|
241
|
+
if (!prevNames.has(name)) diff.added.push(name);
|
|
242
|
+
}
|
|
243
|
+
for (const name of prevNames) {
|
|
244
|
+
if (!currNames.has(name)) diff.removed.push(name);
|
|
245
|
+
}
|
|
246
|
+
const prevMap = Object.fromEntries((prev.configuredRepos || []).map((r) => [r.name, r]));
|
|
247
|
+
const currMap = Object.fromEntries((current.configuredRepos || []).map((r) => [r.name, r]));
|
|
248
|
+
for (const name of currNames) {
|
|
249
|
+
if (
|
|
250
|
+
prevNames.has(name) &&
|
|
251
|
+
(prevMap[name].healthScore || 0) !== (currMap[name].healthScore || 0)
|
|
252
|
+
) {
|
|
253
|
+
diff.changed.push({
|
|
254
|
+
name,
|
|
255
|
+
field: "healthScore",
|
|
256
|
+
from: prevMap[name].healthScore || 0,
|
|
257
|
+
to: currMap[name].healthScore || 0,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return diff;
|
|
262
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { VERSION, DEFAULT_OUTPUT, HOME } from "./constants.mjs";
|
|
2
|
+
|
|
3
|
+
export function parseArgs(argv) {
|
|
4
|
+
const args = {
|
|
5
|
+
output: DEFAULT_OUTPUT,
|
|
6
|
+
open: false,
|
|
7
|
+
json: false,
|
|
8
|
+
catalog: false,
|
|
9
|
+
command: null,
|
|
10
|
+
template: null,
|
|
11
|
+
dryRun: false,
|
|
12
|
+
quiet: false,
|
|
13
|
+
watch: false,
|
|
14
|
+
diff: false,
|
|
15
|
+
anonymize: false,
|
|
16
|
+
completions: false,
|
|
17
|
+
};
|
|
18
|
+
let i = 2; // skip node + script
|
|
19
|
+
if (argv[2] === "init") {
|
|
20
|
+
args.command = "init";
|
|
21
|
+
i = 3;
|
|
22
|
+
} else if (argv[2] === "lint") {
|
|
23
|
+
args.command = "lint";
|
|
24
|
+
i = 3;
|
|
25
|
+
}
|
|
26
|
+
while (i < argv.length) {
|
|
27
|
+
switch (argv[i]) {
|
|
28
|
+
case "--help":
|
|
29
|
+
case "-h":
|
|
30
|
+
console.log(`claude-code-dashboard v${VERSION}
|
|
31
|
+
|
|
32
|
+
Scans your home directory for git repos with Claude Code configuration
|
|
33
|
+
and generates a self-contained HTML dashboard.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
claude-code-dashboard [options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--output, -o <path> Output path (default: ~/.claude/dashboard.html)
|
|
40
|
+
--json Output full data model as JSON instead of HTML
|
|
41
|
+
--catalog Generate a shareable skill catalog HTML page
|
|
42
|
+
--open Open the dashboard in your default browser after generating
|
|
43
|
+
--quiet Suppress output, just write file
|
|
44
|
+
--watch Regenerate on file changes
|
|
45
|
+
--diff Show changes since last generation
|
|
46
|
+
--anonymize Anonymize paths for shareable export
|
|
47
|
+
--completions Output shell completion script for bash/zsh
|
|
48
|
+
--version, -v Show version
|
|
49
|
+
--help, -h Show this help
|
|
50
|
+
|
|
51
|
+
Subcommands:
|
|
52
|
+
init Scaffold Claude Code config for current directory
|
|
53
|
+
--template, -t <stack> Override auto-detected stack (next, react, python, etc.)
|
|
54
|
+
--dry-run Preview what would be created without writing files
|
|
55
|
+
lint Check all repos for config issues
|
|
56
|
+
|
|
57
|
+
Config file: ~/.claude/dashboard.conf
|
|
58
|
+
Add directories (one per line) to restrict scanning scope.
|
|
59
|
+
Define dependency chains: chain: A -> B -> C
|
|
60
|
+
Lines starting with # are comments.`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
case "--version":
|
|
63
|
+
case "-v":
|
|
64
|
+
console.log(VERSION);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
case "--output":
|
|
67
|
+
case "-o":
|
|
68
|
+
args.output = argv[++i];
|
|
69
|
+
if (!args.output) {
|
|
70
|
+
console.error("Error: --output requires a path argument");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
// Expand ~ at the start of the path
|
|
74
|
+
if (args.output.startsWith("~")) {
|
|
75
|
+
args.output = args.output.replace(/^~/, HOME);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case "--json":
|
|
79
|
+
args.json = true;
|
|
80
|
+
break;
|
|
81
|
+
case "--catalog":
|
|
82
|
+
args.catalog = true;
|
|
83
|
+
break;
|
|
84
|
+
case "--open":
|
|
85
|
+
args.open = true;
|
|
86
|
+
break;
|
|
87
|
+
case "--template":
|
|
88
|
+
case "-t":
|
|
89
|
+
args.template = argv[++i];
|
|
90
|
+
if (!args.template) {
|
|
91
|
+
console.error("Error: --template requires a stack argument");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case "--dry-run":
|
|
96
|
+
args.dryRun = true;
|
|
97
|
+
break;
|
|
98
|
+
case "--quiet":
|
|
99
|
+
args.quiet = true;
|
|
100
|
+
break;
|
|
101
|
+
case "--watch":
|
|
102
|
+
args.watch = true;
|
|
103
|
+
break;
|
|
104
|
+
case "--diff":
|
|
105
|
+
args.diff = true;
|
|
106
|
+
break;
|
|
107
|
+
case "--anonymize":
|
|
108
|
+
args.anonymize = true;
|
|
109
|
+
break;
|
|
110
|
+
case "--completions":
|
|
111
|
+
args.completions = true;
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
console.error(`Unknown option: ${argv[i]}\nRun with --help for usage.`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
return args;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function generateCompletions() {
|
|
123
|
+
console.log(`# claude-code-dashboard completions
|
|
124
|
+
# eval "$(claude-code-dashboard --completions)"
|
|
125
|
+
if [ -n "$ZSH_VERSION" ]; then
|
|
126
|
+
_claude_code_dashboard() {
|
|
127
|
+
local -a opts; opts=(init lint --output --open --json --catalog --quiet --watch --diff --anonymize --completions --help --version)
|
|
128
|
+
if (( CURRENT == 2 )); then _describe 'option' opts; fi
|
|
129
|
+
}; compdef _claude_code_dashboard claude-code-dashboard
|
|
130
|
+
elif [ -n "$BASH_VERSION" ]; then
|
|
131
|
+
_claude_code_dashboard() { COMPREPLY=( $(compgen -W "init lint --output --open --json --catalog --quiet --watch --diff --anonymize --completions --help --version" -- "\${COMP_WORDS[COMP_CWORD]}") ); }
|
|
132
|
+
complete -F _claude_code_dashboard claude-code-dashboard
|
|
133
|
+
fi`);
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
|
|
4
|
+
export const VERSION = "0.0.1";
|
|
5
|
+
|
|
6
|
+
export const HOME = homedir();
|
|
7
|
+
export const CLAUDE_DIR = join(HOME, ".claude");
|
|
8
|
+
export const DEFAULT_OUTPUT = join(CLAUDE_DIR, "dashboard.html");
|
|
9
|
+
export const CONF = join(CLAUDE_DIR, "dashboard.conf");
|
|
10
|
+
export const MAX_DEPTH = 5;
|
|
11
|
+
|
|
12
|
+
// Freshness thresholds (seconds)
|
|
13
|
+
export const ONE_DAY = 86_400;
|
|
14
|
+
export const TWO_DAYS = 172_800;
|
|
15
|
+
export const THIRTY_DAYS = 2_592_000;
|
|
16
|
+
export const NINETY_DAYS = 7_776_000;
|
|
17
|
+
export const ONE_YEAR = 31_536_000;
|
|
18
|
+
|
|
19
|
+
// Directories to skip during repo discovery
|
|
20
|
+
export const PRUNE = new Set([
|
|
21
|
+
"node_modules",
|
|
22
|
+
".Trash",
|
|
23
|
+
"Library",
|
|
24
|
+
".cache",
|
|
25
|
+
".npm",
|
|
26
|
+
".yarn",
|
|
27
|
+
".pnpm",
|
|
28
|
+
".local",
|
|
29
|
+
".cargo",
|
|
30
|
+
".rustup",
|
|
31
|
+
".gradle",
|
|
32
|
+
".m2",
|
|
33
|
+
".cocoapods",
|
|
34
|
+
".android",
|
|
35
|
+
"Caches",
|
|
36
|
+
".virtualenvs",
|
|
37
|
+
".pyenv",
|
|
38
|
+
".nvm",
|
|
39
|
+
".rbenv",
|
|
40
|
+
".gem",
|
|
41
|
+
".docker",
|
|
42
|
+
".orbstack",
|
|
43
|
+
"go",
|
|
44
|
+
"venv",
|
|
45
|
+
"__pycache__",
|
|
46
|
+
".tox",
|
|
47
|
+
".git",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// Lines matching these patterns are skipped when extracting project descriptions.
|
|
51
|
+
// Override by adding a YAML frontmatter `description:` field to your CLAUDE.md.
|
|
52
|
+
const BOILERPLATE_PATTERNS = [
|
|
53
|
+
"This file provides guidance",
|
|
54
|
+
"CLAUDE.md.*symlink",
|
|
55
|
+
"AGENTS.md.*should contain",
|
|
56
|
+
"Always-loaded guidance",
|
|
57
|
+
"Guidance for coding agents",
|
|
58
|
+
"Rules are split into focused files",
|
|
59
|
+
"Detailed implementation guidance lives in",
|
|
60
|
+
];
|
|
61
|
+
export const BOILERPLATE_RE = new RegExp(BOILERPLATE_PATTERNS.join("|"));
|
|
62
|
+
|
|
63
|
+
export const STACK_FILES = {
|
|
64
|
+
"next.config.js": "next",
|
|
65
|
+
"next.config.mjs": "next",
|
|
66
|
+
"next.config.ts": "next",
|
|
67
|
+
"Cargo.toml": "rust",
|
|
68
|
+
"go.mod": "go",
|
|
69
|
+
"requirements.txt": "python",
|
|
70
|
+
"pyproject.toml": "python",
|
|
71
|
+
"setup.py": "python",
|
|
72
|
+
"Package.swift": "swift",
|
|
73
|
+
Gemfile: "ruby",
|
|
74
|
+
"pom.xml": "java",
|
|
75
|
+
"build.gradle": "java",
|
|
76
|
+
"build.gradle.kts": "java",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const SKILL_CATEGORIES = {
|
|
80
|
+
workflow: ["plan", "workflow", "branch", "commit", "pr-", "review", "ship", "deploy", "execute"],
|
|
81
|
+
"code-quality": ["lint", "test-", "quality", "format", "refactor", "clean", "verify", "tdd"],
|
|
82
|
+
debugging: ["debug", "diagnose", "troubleshoot", "ci-fix", "stack-trace", "breakpoint"],
|
|
83
|
+
research: [
|
|
84
|
+
"research",
|
|
85
|
+
"search",
|
|
86
|
+
"analyze",
|
|
87
|
+
"explore",
|
|
88
|
+
"investigate",
|
|
89
|
+
"compare",
|
|
90
|
+
"competitive",
|
|
91
|
+
"audit",
|
|
92
|
+
"find",
|
|
93
|
+
],
|
|
94
|
+
integrations: ["slack", "github", "figma", "linear", "jira", "notion", "snowflake", "api", "mcp"],
|
|
95
|
+
"project-specific": ["storybook", "react-native"],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const CATEGORY_ORDER = [
|
|
99
|
+
"workflow",
|
|
100
|
+
"code-quality",
|
|
101
|
+
"debugging",
|
|
102
|
+
"research",
|
|
103
|
+
"integrations",
|
|
104
|
+
"project-specific",
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
// Last verified against Claude Code v2.1.72 (March 2026)
|
|
108
|
+
export const QUICK_REFERENCE = {
|
|
109
|
+
essentialCommands: [
|
|
110
|
+
{ cmd: "/help", desc: "Show help and available commands" },
|
|
111
|
+
{ cmd: "/compact", desc: "Compact conversation to free context" },
|
|
112
|
+
{ cmd: "/model", desc: "Switch AI model" },
|
|
113
|
+
{ cmd: "/diff", desc: "Interactive diff viewer for changes" },
|
|
114
|
+
{ cmd: "/status", desc: "Version, model, account info" },
|
|
115
|
+
{ cmd: "/cost", desc: "Show token usage statistics" },
|
|
116
|
+
{ cmd: "/plan", desc: "Enter plan mode for complex tasks" },
|
|
117
|
+
{ cmd: "/config", desc: "Open settings interface" },
|
|
118
|
+
{ cmd: "/mcp", desc: "Manage MCP server connections" },
|
|
119
|
+
{ cmd: "/memory", desc: "Edit CLAUDE.md, toggle auto-memory" },
|
|
120
|
+
{ cmd: "/permissions", desc: "View or update tool permissions" },
|
|
121
|
+
{ cmd: "/init", desc: "Initialize project with CLAUDE.md" },
|
|
122
|
+
{ cmd: "/insights", desc: "Generate usage analytics report" },
|
|
123
|
+
{ cmd: "/export", desc: "Export conversation as plain text" },
|
|
124
|
+
{ cmd: "/pr-comments", desc: "Fetch GitHub PR review comments" },
|
|
125
|
+
{ cmd: "/doctor", desc: "Diagnose installation issues" },
|
|
126
|
+
],
|
|
127
|
+
tools: [
|
|
128
|
+
{ name: "Bash", desc: "Execute shell commands" },
|
|
129
|
+
{ name: "Read", desc: "Read files (text, images, PDFs, notebooks)" },
|
|
130
|
+
{ name: "Write", desc: "Create new files" },
|
|
131
|
+
{ name: "Edit", desc: "Modify files via exact string replacement" },
|
|
132
|
+
{ name: "Grep", desc: "Search file contents with regex" },
|
|
133
|
+
{ name: "Glob", desc: "Find files by pattern" },
|
|
134
|
+
{ name: "Agent", desc: "Launch specialized sub-agents" },
|
|
135
|
+
{ name: "WebSearch", desc: "Search the web" },
|
|
136
|
+
{ name: "WebFetch", desc: "Fetch URL content" },
|
|
137
|
+
{ name: "LSP", desc: "Code intelligence (go-to-def, references)" },
|
|
138
|
+
],
|
|
139
|
+
shortcuts: [
|
|
140
|
+
{ keys: "/", desc: "Quick command search" },
|
|
141
|
+
{ keys: "!", desc: "Bash mode (run directly)" },
|
|
142
|
+
{ keys: "@", desc: "File path autocomplete" },
|
|
143
|
+
{ keys: "Ctrl+C", desc: "Cancel generation" },
|
|
144
|
+
{ keys: "Ctrl+L", desc: "Clear screen" },
|
|
145
|
+
{ keys: "Ctrl+R", desc: "Search history" },
|
|
146
|
+
{ keys: "Shift+Tab", desc: "Toggle permission mode" },
|
|
147
|
+
{ keys: "Esc Esc", desc: "Rewind conversation" },
|
|
148
|
+
{ keys: "Tab", desc: "Toggle thinking" },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { PRUNE, CONF, HOME } from "./constants.mjs";
|
|
4
|
+
|
|
5
|
+
export function findGitRepos(roots, maxDepth) {
|
|
6
|
+
const repos = [];
|
|
7
|
+
function walk(dir, depth) {
|
|
8
|
+
if (depth > maxDepth) return;
|
|
9
|
+
let entries;
|
|
10
|
+
try {
|
|
11
|
+
entries = readdirSync(dir);
|
|
12
|
+
} catch {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (entry === ".git") {
|
|
17
|
+
repos.push(dir);
|
|
18
|
+
return; // don't recurse inside a git repo's subdirs
|
|
19
|
+
}
|
|
20
|
+
if (PRUNE.has(entry)) continue;
|
|
21
|
+
const full = join(dir, entry);
|
|
22
|
+
try {
|
|
23
|
+
if (statSync(full).isDirectory()) walk(full, depth + 1);
|
|
24
|
+
} catch {
|
|
25
|
+
/* permission denied, symlink loops, etc */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const root of roots) {
|
|
30
|
+
if (existsSync(root)) walk(root, 0);
|
|
31
|
+
}
|
|
32
|
+
return repos;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getScanRoots() {
|
|
36
|
+
if (existsSync(CONF)) {
|
|
37
|
+
const dirs = readFileSync(CONF, "utf8")
|
|
38
|
+
.split("\n")
|
|
39
|
+
.map((l) => l.replace(/#.*/, "").trim())
|
|
40
|
+
.filter((l) => l && !l.startsWith("chain:"))
|
|
41
|
+
.map((l) => l.replace(/^~/, HOME))
|
|
42
|
+
.filter((d) => existsSync(d));
|
|
43
|
+
if (dirs.length) return dirs;
|
|
44
|
+
}
|
|
45
|
+
return [HOME];
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ONE_DAY, TWO_DAYS, THIRTY_DAYS, NINETY_DAYS, ONE_YEAR } from "./constants.mjs";
|
|
2
|
+
import { gitCmd } from "./helpers.mjs";
|
|
3
|
+
|
|
4
|
+
export function getFreshness(repoDir) {
|
|
5
|
+
const ts = gitCmd(
|
|
6
|
+
repoDir,
|
|
7
|
+
"log",
|
|
8
|
+
"-1",
|
|
9
|
+
"--format=%ct",
|
|
10
|
+
"--",
|
|
11
|
+
"CLAUDE.md",
|
|
12
|
+
"AGENTS.md",
|
|
13
|
+
".claude/",
|
|
14
|
+
);
|
|
15
|
+
const parsed = Number(ts);
|
|
16
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function relativeTime(ts) {
|
|
20
|
+
if (!ts) return "unknown";
|
|
21
|
+
const diff = Math.floor(Date.now() / 1000) - ts;
|
|
22
|
+
if (diff < ONE_DAY) return "today";
|
|
23
|
+
if (diff < TWO_DAYS) return "yesterday";
|
|
24
|
+
if (diff < THIRTY_DAYS) return `${Math.floor(diff / ONE_DAY)}d ago`;
|
|
25
|
+
if (diff < ONE_YEAR) return `${Math.floor(diff / THIRTY_DAYS)}mo ago`;
|
|
26
|
+
return `${Math.floor(diff / ONE_YEAR)}y ago`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function freshnessClass(ts) {
|
|
30
|
+
if (!ts) return "stale";
|
|
31
|
+
const diff = Math.floor(Date.now() / 1000) - ts;
|
|
32
|
+
if (diff < THIRTY_DAYS) return "fresh";
|
|
33
|
+
if (diff < NINETY_DAYS) return "aging";
|
|
34
|
+
return "stale";
|
|
35
|
+
}
|
package/src/helpers.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import { HOME } from "./constants.mjs";
|
|
3
|
+
|
|
4
|
+
export const esc = (s) =>
|
|
5
|
+
s
|
|
6
|
+
.replace(/&/g, "&")
|
|
7
|
+
.replace(/</g, "<")
|
|
8
|
+
.replace(/>/g, ">")
|
|
9
|
+
.replace(/"/g, """)
|
|
10
|
+
.replace(/'/g, "'");
|
|
11
|
+
|
|
12
|
+
export const shortPath = (p) => p.replace(HOME, "~");
|
|
13
|
+
|
|
14
|
+
/** Format large token counts as MM/BB shorthand. Guards against undefined/NaN. */
|
|
15
|
+
export function formatTokens(n) {
|
|
16
|
+
n = Number(n) || 0;
|
|
17
|
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B tokens`;
|
|
18
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M tokens`;
|
|
19
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K tokens`;
|
|
20
|
+
return `${n} tokens`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Run a git command safely using execFileSync (no shell injection). */
|
|
24
|
+
export function gitCmd(repoDir, ...args) {
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync("git", ["-C", repoDir, ...args], {
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
timeout: 10_000,
|
|
29
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
30
|
+
}).trim();
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function anonymizePath(p) {
|
|
37
|
+
return p
|
|
38
|
+
.replace(/^\/Users\/[^/]+\//, "~/")
|
|
39
|
+
.replace(/^\/home\/[^/]+\//, "~/")
|
|
40
|
+
.replace(/^C:\\Users\\[^\\]+\\/, "~\\")
|
|
41
|
+
.replace(/^C:\/Users\/[^/]+\//, "~/");
|
|
42
|
+
}
|