coding-friend-cli 1.0.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/dist/chunk-6CGGT2FD.js +32 -0
- package/dist/chunk-6DUFTBTO.js +14 -0
- package/dist/chunk-AQXTNLQD.js +39 -0
- package/dist/chunk-HRVSKMNA.js +31 -0
- package/dist/chunk-IUTXHCP7.js +28 -0
- package/dist/chunk-KZT4AFDW.js +44 -0
- package/dist/chunk-VHZQ6KEU.js +73 -0
- package/dist/host-3GAEZKKJ.js +83 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +34 -0
- package/dist/init-ONRXFOZ5.js +431 -0
- package/dist/json-2XS56OJY.js +10 -0
- package/dist/mcp-LMMIFH4B.js +104 -0
- package/dist/postinstall.d.ts +1 -0
- package/dist/postinstall.js +8 -0
- package/dist/statusline-7D6YU5YM.js +64 -0
- package/dist/update-K5PYOB52.js +160 -0
- package/lib/learn-host/next-env.d.ts +6 -0
- package/lib/learn-host/next.config.ts +9 -0
- package/lib/learn-host/package-lock.json +3943 -0
- package/lib/learn-host/package.json +31 -0
- package/lib/learn-host/postcss.config.mjs +7 -0
- package/lib/learn-host/scripts/build-search-index.ts +11 -0
- package/lib/learn-host/src/app/[category]/[slug]/page.tsx +61 -0
- package/lib/learn-host/src/app/[category]/page.tsx +35 -0
- package/lib/learn-host/src/app/globals.css +31 -0
- package/lib/learn-host/src/app/layout.tsx +32 -0
- package/lib/learn-host/src/app/page.tsx +63 -0
- package/lib/learn-host/src/app/search/page.tsx +94 -0
- package/lib/learn-host/src/app/search/search-index.json +42 -0
- package/lib/learn-host/src/components/Breadcrumbs.tsx +28 -0
- package/lib/learn-host/src/components/DocCard.tsx +32 -0
- package/lib/learn-host/src/components/MarkdownRenderer.tsx +13 -0
- package/lib/learn-host/src/components/MobileNav.tsx +56 -0
- package/lib/learn-host/src/components/SearchBar.tsx +36 -0
- package/lib/learn-host/src/components/Sidebar.tsx +44 -0
- package/lib/learn-host/src/components/TagBadge.tsx +12 -0
- package/lib/learn-host/src/components/ThemeToggle.tsx +30 -0
- package/lib/learn-host/src/lib/docs.ts +113 -0
- package/lib/learn-host/src/lib/search.ts +27 -0
- package/lib/learn-host/src/lib/types.ts +31 -0
- package/lib/learn-host/tsconfig.json +21 -0
- package/lib/learn-mcp/package-lock.json +1829 -0
- package/lib/learn-mcp/package.json +24 -0
- package/lib/learn-mcp/src/bin/learn-mcp.ts +2 -0
- package/lib/learn-mcp/src/index.ts +17 -0
- package/lib/learn-mcp/src/lib/docs.ts +199 -0
- package/lib/learn-mcp/src/lib/knowledge.ts +95 -0
- package/lib/learn-mcp/src/lib/types.ts +36 -0
- package/lib/learn-mcp/src/server.ts +22 -0
- package/lib/learn-mcp/src/tools/create-doc.ts +29 -0
- package/lib/learn-mcp/src/tools/get-review-list.ts +29 -0
- package/lib/learn-mcp/src/tools/improve-doc.ts +95 -0
- package/lib/learn-mcp/src/tools/list-categories.ts +19 -0
- package/lib/learn-mcp/src/tools/list-docs.ts +30 -0
- package/lib/learn-mcp/src/tools/read-doc.ts +29 -0
- package/lib/learn-mcp/src/tools/search-docs.ts +23 -0
- package/lib/learn-mcp/src/tools/track-knowledge.ts +35 -0
- package/lib/learn-mcp/src/tools/update-doc.ts +43 -0
- package/lib/learn-mcp/tsconfig.json +15 -0
- package/package.json +47 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CONFIG
|
|
3
|
+
} from "./chunk-HRVSKMNA.js";
|
|
4
|
+
import {
|
|
5
|
+
ensureShellCompletion,
|
|
6
|
+
hasShellCompletion
|
|
7
|
+
} from "./chunk-VHZQ6KEU.js";
|
|
8
|
+
import {
|
|
9
|
+
run
|
|
10
|
+
} from "./chunk-6CGGT2FD.js";
|
|
11
|
+
import {
|
|
12
|
+
claudeSettingsPath,
|
|
13
|
+
globalConfigPath,
|
|
14
|
+
localConfigPath,
|
|
15
|
+
resolvePath
|
|
16
|
+
} from "./chunk-AQXTNLQD.js";
|
|
17
|
+
import {
|
|
18
|
+
log
|
|
19
|
+
} from "./chunk-6DUFTBTO.js";
|
|
20
|
+
import {
|
|
21
|
+
mergeJson,
|
|
22
|
+
readJson
|
|
23
|
+
} from "./chunk-IUTXHCP7.js";
|
|
24
|
+
|
|
25
|
+
// src/commands/init.ts
|
|
26
|
+
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
27
|
+
import { appendFileSync, existsSync, readFileSync } from "fs";
|
|
28
|
+
import { homedir } from "os";
|
|
29
|
+
function isGitRepo() {
|
|
30
|
+
return run("git", ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
31
|
+
}
|
|
32
|
+
function checkDocsFolders() {
|
|
33
|
+
const folders = ["docs/plans", "docs/memory", "docs/research", "docs/learn"];
|
|
34
|
+
return folders.every((f) => existsSync(f));
|
|
35
|
+
}
|
|
36
|
+
function checkGitignore() {
|
|
37
|
+
if (!existsSync(".gitignore")) return false;
|
|
38
|
+
return readFileSync(".gitignore", "utf-8").includes("# coding-friend");
|
|
39
|
+
}
|
|
40
|
+
function checkLanguage() {
|
|
41
|
+
const local = readJson(localConfigPath());
|
|
42
|
+
const global = readJson(globalConfigPath());
|
|
43
|
+
return !!(local?.language || global?.language);
|
|
44
|
+
}
|
|
45
|
+
function checkLearnConfig() {
|
|
46
|
+
const local = readJson(localConfigPath());
|
|
47
|
+
const global = readJson(globalConfigPath());
|
|
48
|
+
return !!(local?.learn || global?.learn);
|
|
49
|
+
}
|
|
50
|
+
function getResolvedOutputDir() {
|
|
51
|
+
const local = readJson(localConfigPath());
|
|
52
|
+
if (local?.learn?.outputDir) return resolvePath(local.learn.outputDir);
|
|
53
|
+
const global = readJson(globalConfigPath());
|
|
54
|
+
if (global?.learn?.outputDir) return resolvePath(global.learn.outputDir);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function isExternalOutputDir(outputDir) {
|
|
58
|
+
if (!outputDir) return false;
|
|
59
|
+
const cwd = process.cwd();
|
|
60
|
+
return !outputDir.startsWith(cwd);
|
|
61
|
+
}
|
|
62
|
+
function checkClaudePermissions(outputDir) {
|
|
63
|
+
const settings = readJson(claudeSettingsPath());
|
|
64
|
+
if (!settings) return false;
|
|
65
|
+
const permissions = settings.permissions;
|
|
66
|
+
if (!permissions?.allow) return false;
|
|
67
|
+
const homePath = outputDir.replace(homedir(), "~");
|
|
68
|
+
return permissions.allow.some(
|
|
69
|
+
(rule) => rule.includes(outputDir) || rule.includes(homePath)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
async function setupDocsFolders() {
|
|
73
|
+
const folders = ["docs/plans", "docs/memory", "docs/research", "docs/learn"];
|
|
74
|
+
const created = [];
|
|
75
|
+
for (const f of folders) {
|
|
76
|
+
if (!existsSync(f)) {
|
|
77
|
+
run("mkdir", ["-p", f]);
|
|
78
|
+
created.push(f);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (created.length > 0) {
|
|
82
|
+
log.success(`Created: ${created.join(", ")}`);
|
|
83
|
+
} else {
|
|
84
|
+
log.dim("All docs folders already exist.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function setupGitignore() {
|
|
88
|
+
const choice = await select({
|
|
89
|
+
message: "Add coding-friend artifacts to .gitignore?",
|
|
90
|
+
choices: [
|
|
91
|
+
{ name: "Yes, ignore all", value: "all" },
|
|
92
|
+
{ name: "Partial \u2014 pick which to ignore", value: "partial" },
|
|
93
|
+
{ name: "No \u2014 keep everything tracked", value: "none" }
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
if (choice === "none") {
|
|
97
|
+
log.dim("Skipped .gitignore config.");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const allEntries = [
|
|
101
|
+
"docs/plans/",
|
|
102
|
+
"docs/memory/",
|
|
103
|
+
"docs/research/",
|
|
104
|
+
"docs/learn/",
|
|
105
|
+
".coding-friend/"
|
|
106
|
+
];
|
|
107
|
+
let entries = allEntries;
|
|
108
|
+
if (choice === "partial") {
|
|
109
|
+
entries = await checkbox({
|
|
110
|
+
message: "Which folders to ignore?",
|
|
111
|
+
choices: allEntries.map((e) => ({ name: e, value: e }))
|
|
112
|
+
});
|
|
113
|
+
if (entries.length === 0) {
|
|
114
|
+
log.dim("Nothing selected.");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const existing = existsSync(".gitignore") ? readFileSync(".gitignore", "utf-8") : "";
|
|
119
|
+
const newEntries = entries.filter((e) => !existing.includes(e));
|
|
120
|
+
if (newEntries.length === 0) {
|
|
121
|
+
log.dim("All entries already in .gitignore.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const block = `
|
|
125
|
+
# coding-friend
|
|
126
|
+
${newEntries.join("\n")}
|
|
127
|
+
`;
|
|
128
|
+
appendFileSync(".gitignore", block);
|
|
129
|
+
log.success(`Added to .gitignore: ${newEntries.join(", ")}`);
|
|
130
|
+
}
|
|
131
|
+
async function setupLanguage() {
|
|
132
|
+
const choice = await select({
|
|
133
|
+
message: "What language should generated docs be written in?",
|
|
134
|
+
choices: [
|
|
135
|
+
{ name: "English", value: "en" },
|
|
136
|
+
{ name: "Vietnamese", value: "vi" },
|
|
137
|
+
{ name: "Other", value: "_other" }
|
|
138
|
+
]
|
|
139
|
+
});
|
|
140
|
+
if (choice === "_other") {
|
|
141
|
+
const lang = await input({ message: "Enter language name:" });
|
|
142
|
+
return lang || "en";
|
|
143
|
+
}
|
|
144
|
+
return choice;
|
|
145
|
+
}
|
|
146
|
+
async function setupLearnConfig(gitAvailable = true) {
|
|
147
|
+
const locationChoice = await select({
|
|
148
|
+
message: "Where to store learning docs?",
|
|
149
|
+
choices: [
|
|
150
|
+
{ name: "In this project (docs/learn/)", value: "local" },
|
|
151
|
+
{ name: "A separate folder", value: "external" }
|
|
152
|
+
]
|
|
153
|
+
});
|
|
154
|
+
let outputDir = "docs/learn";
|
|
155
|
+
let isExternal = false;
|
|
156
|
+
if (locationChoice === "external") {
|
|
157
|
+
outputDir = await input({
|
|
158
|
+
message: "Enter path (absolute or ~/...):",
|
|
159
|
+
validate: (val) => val.length > 0 ? true : "Path cannot be empty"
|
|
160
|
+
});
|
|
161
|
+
isExternal = true;
|
|
162
|
+
const resolved = resolvePath(outputDir);
|
|
163
|
+
if (!existsSync(resolved)) {
|
|
164
|
+
const create = await confirm({
|
|
165
|
+
message: `Folder ${resolved} doesn't exist. Create it?`,
|
|
166
|
+
default: true
|
|
167
|
+
});
|
|
168
|
+
if (create) {
|
|
169
|
+
run("mkdir", ["-p", resolved]);
|
|
170
|
+
log.success(`Created ${resolved}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const catChoice = await select({
|
|
175
|
+
message: "Categories for organizing learning docs?",
|
|
176
|
+
choices: [
|
|
177
|
+
{
|
|
178
|
+
name: "Use defaults (concepts, patterns, languages, tools, debugging)",
|
|
179
|
+
value: "defaults"
|
|
180
|
+
},
|
|
181
|
+
{ name: "Customize", value: "custom" }
|
|
182
|
+
]
|
|
183
|
+
});
|
|
184
|
+
let categories = DEFAULT_CONFIG.learn.categories;
|
|
185
|
+
if (catChoice === "custom") {
|
|
186
|
+
console.log('Enter categories (format: "name: description"). Empty line to finish.');
|
|
187
|
+
const customCats = [];
|
|
188
|
+
let keepGoing = true;
|
|
189
|
+
while (keepGoing) {
|
|
190
|
+
const line = await input({
|
|
191
|
+
message: `Category ${customCats.length + 1}:`
|
|
192
|
+
});
|
|
193
|
+
if (!line) {
|
|
194
|
+
keepGoing = false;
|
|
195
|
+
} else {
|
|
196
|
+
const [name, ...descParts] = line.split(":");
|
|
197
|
+
customCats.push({
|
|
198
|
+
name: name.trim(),
|
|
199
|
+
description: descParts.join(":").trim() || name.trim()
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (customCats.length > 0) categories = customCats;
|
|
204
|
+
}
|
|
205
|
+
let autoCommit = false;
|
|
206
|
+
if (isExternal && gitAvailable) {
|
|
207
|
+
autoCommit = await confirm({
|
|
208
|
+
message: "Auto-commit learning docs to git after each /cf-learn?",
|
|
209
|
+
default: false
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const indexChoice = await select({
|
|
213
|
+
message: "How should learning docs be indexed?",
|
|
214
|
+
choices: [
|
|
215
|
+
{ name: "No index", value: "none" },
|
|
216
|
+
{ name: "Single README at root", value: "single" },
|
|
217
|
+
{ name: "Per-category READMEs", value: "per-category" }
|
|
218
|
+
]
|
|
219
|
+
});
|
|
220
|
+
let readmeIndex = false;
|
|
221
|
+
if (indexChoice === "single") readmeIndex = true;
|
|
222
|
+
else if (indexChoice === "per-category") readmeIndex = "per-category";
|
|
223
|
+
return { outputDir, categories, autoCommit, readmeIndex, isExternal };
|
|
224
|
+
}
|
|
225
|
+
async function setupClaudePermissions(outputDir, autoCommit) {
|
|
226
|
+
const resolved = resolvePath(outputDir);
|
|
227
|
+
const homePath = resolved.startsWith(homedir()) ? resolved.replace(homedir(), "~") : resolved;
|
|
228
|
+
const rules = [
|
|
229
|
+
`Read(${homePath}/**)`,
|
|
230
|
+
`Edit(${homePath}/**)`,
|
|
231
|
+
`Write(${homePath}/**)`
|
|
232
|
+
];
|
|
233
|
+
if (autoCommit) {
|
|
234
|
+
rules.push(`Bash(cd ${homePath} && git add:*)`);
|
|
235
|
+
rules.push(`Bash(cd ${homePath} && git commit:*)`);
|
|
236
|
+
}
|
|
237
|
+
const settingsPath = claudeSettingsPath();
|
|
238
|
+
const settings = readJson(settingsPath);
|
|
239
|
+
if (!settings) {
|
|
240
|
+
log.warn(
|
|
241
|
+
"~/.claude/settings.json not found. Create it via Claude Code settings first."
|
|
242
|
+
);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const permissions = settings.permissions ?? {};
|
|
246
|
+
const existing = permissions.allow ?? [];
|
|
247
|
+
const missing = rules.filter(
|
|
248
|
+
(r) => !existing.some((e) => e === r || e.includes(homePath))
|
|
249
|
+
);
|
|
250
|
+
if (missing.length === 0) {
|
|
251
|
+
log.dim("All permission rules already configured.");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
console.log("\nTo avoid repeated permission prompts, add these rules:");
|
|
255
|
+
for (const r of missing) {
|
|
256
|
+
console.log(` ${r}`);
|
|
257
|
+
}
|
|
258
|
+
const ok = await confirm({
|
|
259
|
+
message: "Add these to ~/.claude/settings.json?",
|
|
260
|
+
default: true
|
|
261
|
+
});
|
|
262
|
+
if (!ok) {
|
|
263
|
+
log.dim("Skipped. You'll get prompted each time.");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
permissions.allow = [...existing, ...missing];
|
|
267
|
+
settings.permissions = permissions;
|
|
268
|
+
const { readJson: _r, writeJson: _w, ...restImports } = await import("./json-2XS56OJY.js");
|
|
269
|
+
_w(settingsPath, settings);
|
|
270
|
+
log.success(`Added ${missing.length} permission rules.`);
|
|
271
|
+
}
|
|
272
|
+
function isDefaultConfig(config) {
|
|
273
|
+
if (config.language && config.language !== "en") return false;
|
|
274
|
+
if (config.learn) {
|
|
275
|
+
const l = config.learn;
|
|
276
|
+
if (l.outputDir && l.outputDir !== "docs/learn") return false;
|
|
277
|
+
if (l.autoCommit) return false;
|
|
278
|
+
if (l.readmeIndex) return false;
|
|
279
|
+
if (l.categories) {
|
|
280
|
+
const defaultNames = DEFAULT_CONFIG.learn.categories.map((c) => c.name);
|
|
281
|
+
const configNames = l.categories.map((c) => c.name);
|
|
282
|
+
if (JSON.stringify(defaultNames) !== JSON.stringify(configNames))
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
async function saveConfig(config) {
|
|
289
|
+
if (isDefaultConfig(config)) {
|
|
290
|
+
log.dim(
|
|
291
|
+
"All settings match defaults \u2014 no config file needed."
|
|
292
|
+
);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const target = await select({
|
|
296
|
+
message: "Save settings as global or project-only?",
|
|
297
|
+
choices: [
|
|
298
|
+
{ name: "Global (all projects)", value: "global" },
|
|
299
|
+
{ name: "This project only", value: "local" },
|
|
300
|
+
{ name: "Both", value: "both" }
|
|
301
|
+
]
|
|
302
|
+
});
|
|
303
|
+
if (target === "global" || target === "both") {
|
|
304
|
+
mergeJson(globalConfigPath(), config);
|
|
305
|
+
log.success(`Saved to ${globalConfigPath()}`);
|
|
306
|
+
}
|
|
307
|
+
if (target === "local" || target === "both") {
|
|
308
|
+
mergeJson(localConfigPath(), config);
|
|
309
|
+
log.success(`Saved to ${localConfigPath()}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function initCommand() {
|
|
313
|
+
console.log("=== \u{1F33F} Coding Friend Init \u{1F33F} ===");
|
|
314
|
+
console.log();
|
|
315
|
+
const gitAvailable = isGitRepo();
|
|
316
|
+
if (!gitAvailable) {
|
|
317
|
+
log.warn("Not inside a git repo \u2014 git-related steps will be skipped.");
|
|
318
|
+
console.log();
|
|
319
|
+
}
|
|
320
|
+
const resolvedOutputDir = getResolvedOutputDir();
|
|
321
|
+
const hasExternalDir = checkLearnConfig() && isExternalOutputDir(resolvedOutputDir);
|
|
322
|
+
const steps = [
|
|
323
|
+
{ name: "docs", label: "Create docs folders", done: checkDocsFolders() },
|
|
324
|
+
...gitAvailable ? [{ name: "gitignore", label: "Configure .gitignore", done: checkGitignore() }] : [],
|
|
325
|
+
{ name: "language", label: "Set docs language", done: checkLanguage() },
|
|
326
|
+
{ name: "learn", label: "Configure /cf-learn", done: checkLearnConfig() },
|
|
327
|
+
{ name: "completion", label: "Setup shell tab completion", done: hasShellCompletion() }
|
|
328
|
+
];
|
|
329
|
+
if (hasExternalDir && resolvedOutputDir) {
|
|
330
|
+
steps.push({
|
|
331
|
+
name: "permissions",
|
|
332
|
+
label: "Configure Claude permissions",
|
|
333
|
+
done: checkClaudePermissions(resolvedOutputDir)
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
console.log("coding-friend setup status:");
|
|
337
|
+
for (const step of steps) {
|
|
338
|
+
const status = step.done ? "\x1B[32m[done]\x1B[0m" : "\x1B[33m[pending]\x1B[0m";
|
|
339
|
+
console.log(` ${status} ${step.label}`);
|
|
340
|
+
}
|
|
341
|
+
console.log();
|
|
342
|
+
const pending = steps.filter((s) => !s.done);
|
|
343
|
+
if (pending.length === 0) {
|
|
344
|
+
log.success("Everything is already configured!");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const action = await select({
|
|
348
|
+
message: `${pending.length} pending step(s). What do you want to do?`,
|
|
349
|
+
choices: [
|
|
350
|
+
{ name: "Apply all pending", value: "all" },
|
|
351
|
+
{ name: "Pick which to apply", value: "pick" },
|
|
352
|
+
{ name: "Cancel", value: "cancel" }
|
|
353
|
+
]
|
|
354
|
+
});
|
|
355
|
+
if (action === "cancel") {
|
|
356
|
+
log.dim("Cancelled.");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let selected = pending;
|
|
360
|
+
if (action === "pick") {
|
|
361
|
+
const picked = await checkbox({
|
|
362
|
+
message: "Which steps to apply?",
|
|
363
|
+
choices: pending.map((s) => ({ name: s.label, value: s.name }))
|
|
364
|
+
});
|
|
365
|
+
selected = pending.filter((s) => picked.includes(s.name));
|
|
366
|
+
if (selected.length === 0) {
|
|
367
|
+
log.dim("Nothing selected.");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const config = {};
|
|
372
|
+
let learnOutputDir = "docs/learn";
|
|
373
|
+
let learnAutoCommit = false;
|
|
374
|
+
let isExternal = false;
|
|
375
|
+
for (const step of selected) {
|
|
376
|
+
console.log();
|
|
377
|
+
log.step(step.label);
|
|
378
|
+
switch (step.name) {
|
|
379
|
+
case "docs":
|
|
380
|
+
await setupDocsFolders();
|
|
381
|
+
break;
|
|
382
|
+
case "gitignore":
|
|
383
|
+
await setupGitignore();
|
|
384
|
+
break;
|
|
385
|
+
case "language": {
|
|
386
|
+
const lang = await setupLanguage();
|
|
387
|
+
config.language = lang;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case "learn": {
|
|
391
|
+
const learn = await setupLearnConfig(gitAvailable);
|
|
392
|
+
config.learn = {
|
|
393
|
+
outputDir: learn.outputDir,
|
|
394
|
+
categories: learn.categories,
|
|
395
|
+
autoCommit: learn.autoCommit,
|
|
396
|
+
readmeIndex: learn.readmeIndex
|
|
397
|
+
};
|
|
398
|
+
learnOutputDir = learn.outputDir;
|
|
399
|
+
learnAutoCommit = learn.autoCommit;
|
|
400
|
+
isExternal = learn.isExternal;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
case "completion":
|
|
404
|
+
ensureShellCompletion();
|
|
405
|
+
break;
|
|
406
|
+
case "permissions":
|
|
407
|
+
if (resolvedOutputDir) {
|
|
408
|
+
await setupClaudePermissions(
|
|
409
|
+
resolvedOutputDir,
|
|
410
|
+
learnAutoCommit
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (isExternal && !selected.some((s) => s.name === "permissions") && !checkClaudePermissions(resolvePath(learnOutputDir))) {
|
|
417
|
+
console.log();
|
|
418
|
+
log.step("Configure Claude permissions");
|
|
419
|
+
await setupClaudePermissions(learnOutputDir, learnAutoCommit);
|
|
420
|
+
}
|
|
421
|
+
if (Object.keys(config).length > 0) {
|
|
422
|
+
console.log();
|
|
423
|
+
await saveConfig(config);
|
|
424
|
+
}
|
|
425
|
+
console.log();
|
|
426
|
+
log.success("Setup complete!");
|
|
427
|
+
log.dim("Available commands: /cf-plan, /cf-commit, /cf-review, /cf-learn");
|
|
428
|
+
}
|
|
429
|
+
export {
|
|
430
|
+
initCommand
|
|
431
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getLibPath,
|
|
3
|
+
resolveDocsDir
|
|
4
|
+
} from "./chunk-KZT4AFDW.js";
|
|
5
|
+
import "./chunk-HRVSKMNA.js";
|
|
6
|
+
import {
|
|
7
|
+
run
|
|
8
|
+
} from "./chunk-6CGGT2FD.js";
|
|
9
|
+
import "./chunk-AQXTNLQD.js";
|
|
10
|
+
import {
|
|
11
|
+
log
|
|
12
|
+
} from "./chunk-6DUFTBTO.js";
|
|
13
|
+
import "./chunk-IUTXHCP7.js";
|
|
14
|
+
|
|
15
|
+
// src/commands/mcp.ts
|
|
16
|
+
import { existsSync, readdirSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
function countMdFiles(dir) {
|
|
20
|
+
let count = 0;
|
|
21
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
count += countMdFiles(join(dir, entry.name));
|
|
24
|
+
} else if (entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
25
|
+
count++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return count;
|
|
29
|
+
}
|
|
30
|
+
async function mcpCommand(path) {
|
|
31
|
+
const docsDir = resolveDocsDir(path);
|
|
32
|
+
const mcpDir = getLibPath("learn-mcp");
|
|
33
|
+
if (!existsSync(docsDir)) {
|
|
34
|
+
log.error(`Docs folder not found: ${docsDir}`);
|
|
35
|
+
log.dim("Run /cf-learn first to generate some docs.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const docCount = countMdFiles(docsDir);
|
|
39
|
+
if (docCount === 0) {
|
|
40
|
+
log.error(`No .md files found in ${docsDir}`);
|
|
41
|
+
log.dim("Run /cf-learn first to generate some docs.");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
console.log("=== \u{1F33F} Coding Friend MCP \u{1F33F} ===");
|
|
45
|
+
log.info(`Docs folder: ${chalk.cyan(docsDir)}`);
|
|
46
|
+
log.info(`Found: ${chalk.green(docCount)} docs`);
|
|
47
|
+
console.log();
|
|
48
|
+
if (!existsSync(join(mcpDir, "node_modules"))) {
|
|
49
|
+
log.step("Installing MCP server dependencies (one-time setup)...");
|
|
50
|
+
const result = run("npm", ["install", "--silent"], { cwd: mcpDir });
|
|
51
|
+
if (result === null) {
|
|
52
|
+
log.error("Failed to install dependencies");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
log.success("Done.");
|
|
56
|
+
}
|
|
57
|
+
if (!existsSync(join(mcpDir, "dist"))) {
|
|
58
|
+
log.step("Building MCP server...");
|
|
59
|
+
const result = run("npm", ["run", "build", "--silent"], { cwd: mcpDir });
|
|
60
|
+
if (result === null) {
|
|
61
|
+
log.error("Failed to build MCP server");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
log.success("Done.");
|
|
65
|
+
}
|
|
66
|
+
console.log();
|
|
67
|
+
const serverPath = join(mcpDir, "dist", "index.js");
|
|
68
|
+
console.log(`Add this to your MCP client config:
|
|
69
|
+
|
|
70
|
+
--- Claude Desktop / Claude Chat (claude_desktop_config.json) ---
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"coding-friend-learn": {
|
|
75
|
+
"command": "node",
|
|
76
|
+
"args": ["${serverPath}", "${docsDir}"]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
--- Generic MCP client ---
|
|
82
|
+
|
|
83
|
+
Server command: node ${serverPath} ${docsDir}
|
|
84
|
+
Transport: stdio
|
|
85
|
+
|
|
86
|
+
--- Available tools ---
|
|
87
|
+
|
|
88
|
+
Read:
|
|
89
|
+
list-categories List all categories with doc counts
|
|
90
|
+
list-docs List docs, filter by category/tag
|
|
91
|
+
read-doc Read full content of a doc
|
|
92
|
+
search-docs Full-text search across all docs
|
|
93
|
+
get-review-list Docs that need review
|
|
94
|
+
|
|
95
|
+
Write:
|
|
96
|
+
create-doc Create new learning doc
|
|
97
|
+
update-doc Append content or update tags
|
|
98
|
+
improve-doc Get improvement suggestions
|
|
99
|
+
track-knowledge Record understanding level (remembered/needs-review/new)
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
mcpCommand
|
|
104
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
claudeSettingsPath,
|
|
3
|
+
pluginCachePath
|
|
4
|
+
} from "./chunk-AQXTNLQD.js";
|
|
5
|
+
import {
|
|
6
|
+
log
|
|
7
|
+
} from "./chunk-6DUFTBTO.js";
|
|
8
|
+
import {
|
|
9
|
+
readJson,
|
|
10
|
+
writeJson
|
|
11
|
+
} from "./chunk-IUTXHCP7.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/statusline.ts
|
|
14
|
+
import { existsSync, readdirSync } from "fs";
|
|
15
|
+
import { confirm } from "@inquirer/prompts";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
function findLatestVersion(cachePath) {
|
|
18
|
+
if (!existsSync(cachePath)) return null;
|
|
19
|
+
const versions = readdirSync(cachePath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
20
|
+
return versions[0] ?? null;
|
|
21
|
+
}
|
|
22
|
+
async function statuslineCommand() {
|
|
23
|
+
console.log("=== \u{1F33F} Coding Friend Statusline \u{1F33F} ===");
|
|
24
|
+
console.log();
|
|
25
|
+
const cachePath = pluginCachePath();
|
|
26
|
+
const version = findLatestVersion(cachePath);
|
|
27
|
+
if (!version) {
|
|
28
|
+
log.error(
|
|
29
|
+
"coding-friend plugin not found in cache. Install it first via Claude Code."
|
|
30
|
+
);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const hookPath = `${cachePath}/${version}/hooks/statusline.sh`;
|
|
34
|
+
if (!existsSync(hookPath)) {
|
|
35
|
+
log.error(`Statusline hook not found: ${hookPath}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
log.info(`Found plugin ${chalk.green(`v${version}`)}`);
|
|
39
|
+
const settingsPath = claudeSettingsPath();
|
|
40
|
+
const settings = readJson(settingsPath) ?? {};
|
|
41
|
+
const existing = settings.statusLine;
|
|
42
|
+
if (existing?.command) {
|
|
43
|
+
log.warn(`Statusline already configured: ${existing.command}`);
|
|
44
|
+
const overwrite = await confirm({
|
|
45
|
+
message: "Overwrite existing statusline config?",
|
|
46
|
+
default: true
|
|
47
|
+
});
|
|
48
|
+
if (!overwrite) {
|
|
49
|
+
log.dim("Skipped.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
settings.statusLine = {
|
|
54
|
+
type: "command",
|
|
55
|
+
command: `bash ${hookPath}`
|
|
56
|
+
};
|
|
57
|
+
writeJson(settingsPath, settings);
|
|
58
|
+
log.success("Statusline configured!");
|
|
59
|
+
log.dim("Restart Claude Code (or start a new session) to see it.");
|
|
60
|
+
log.dim("Shows: plugin name, active model, and git branch.");
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
statuslineCommand
|
|
64
|
+
};
|