coding-friend-cli 1.6.0 → 1.8.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.
Files changed (35) hide show
  1. package/README.md +2 -0
  2. package/dist/{chunk-FWHEMJS3.js → chunk-BPLN4LDL.js} +3 -5
  3. package/dist/{chunk-4PLV2ENL.js → chunk-FYGACWU6.js} +3 -2
  4. package/dist/chunk-FYHHNX7K.js +35 -0
  5. package/dist/{chunk-MRTR7TJ4.js → chunk-HFLBFX6J.js} +2 -4
  6. package/dist/{chunk-ESIIWKPD.js → chunk-JS75SVQA.js} +6 -8
  7. package/dist/{chunk-JWAJ4XPK.js → chunk-PGLUEN7D.js} +1 -1
  8. package/dist/chunk-QQ5SVZET.js +135 -0
  9. package/dist/chunk-RZRT7NGT.js +18 -0
  10. package/dist/{chunk-WHCJT7E2.js → chunk-TPRZHSFS.js} +38 -1
  11. package/dist/{chunk-6DUFTBTO.js → chunk-W5CD7WTX.js} +1 -0
  12. package/dist/config-JZEFZIPY.js +406 -0
  13. package/dist/{dev-EWSTIVM7.js → dev-U7LPXAHR.js} +9 -11
  14. package/dist/{host-SQEDE3NN.js → host-LOG5RPZ7.js} +7 -6
  15. package/dist/index.js +36 -13
  16. package/dist/init-JJATBCHC.js +512 -0
  17. package/dist/{install-RZFSIPFD.js → install-7MSZ7B5O.js} +7 -8
  18. package/dist/{mcp-QRPBL4ML.js → mcp-ORMYETXQ.js} +7 -6
  19. package/dist/postinstall.js +2 -2
  20. package/dist/session-3MWYAKKY.js +235 -0
  21. package/dist/{statusline-WGPSURDC.js → statusline-5HWRTSVL.js} +4 -5
  22. package/dist/{uninstall-KOAJFPD6.js → uninstall-HDLTWPXG.js} +8 -10
  23. package/dist/update-E4MQDRFC.js +16 -0
  24. package/lib/learn-host/CHANGELOG.md +6 -0
  25. package/lib/learn-host/package.json +1 -1
  26. package/lib/learn-host/src/app/[category]/[slug]/page.tsx +3 -1
  27. package/lib/learn-host/src/app/globals.css +20 -0
  28. package/lib/learn-host/src/components/TableOfContents.tsx +15 -1
  29. package/lib/learn-host/src/lib/docs.ts +2 -1
  30. package/package.json +1 -1
  31. package/dist/chunk-IUTXHCP7.js +0 -28
  32. package/dist/chunk-WK5YYHXM.js +0 -44
  33. package/dist/init-HX5T5DBV.js +0 -468
  34. package/dist/json-2XS56OJY.js +0 -10
  35. package/dist/update-VAFEWOLA.js +0 -17
@@ -0,0 +1,235 @@
1
+ import {
2
+ loadConfig
3
+ } from "./chunk-FYHHNX7K.js";
4
+ import "./chunk-PGLUEN7D.js";
5
+ import {
6
+ claudeSessionDir,
7
+ encodeProjectPath,
8
+ globalConfigPath,
9
+ mergeJson,
10
+ readJson,
11
+ writeJson
12
+ } from "./chunk-TPRZHSFS.js";
13
+ import {
14
+ log
15
+ } from "./chunk-W5CD7WTX.js";
16
+
17
+ // src/commands/session.ts
18
+ import { input, select } from "@inquirer/prompts";
19
+ import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
20
+ import { homedir as homedir2 } from "os";
21
+ import { join as join2, basename } from "path";
22
+
23
+ // src/lib/session.ts
24
+ import {
25
+ readdirSync,
26
+ statSync,
27
+ copyFileSync,
28
+ existsSync,
29
+ readFileSync
30
+ } from "fs";
31
+ import { join } from "path";
32
+ import { hostname as osHostname } from "os";
33
+ function buildPreviewText(jsonlPath, maxChars = 200) {
34
+ try {
35
+ const content = readFileSync(jsonlPath, "utf-8");
36
+ const lines = content.split("\n").filter(Boolean);
37
+ for (const line of lines) {
38
+ try {
39
+ const entry = JSON.parse(line);
40
+ if (entry.type === "user") {
41
+ const msg = entry.message;
42
+ const msgContent = msg?.content;
43
+ if (typeof msgContent === "string" && msgContent.trim()) {
44
+ return msgContent.trim().slice(0, maxChars);
45
+ }
46
+ }
47
+ } catch {
48
+ }
49
+ }
50
+ return "(preview unavailable)";
51
+ } catch {
52
+ return "(preview unavailable)";
53
+ }
54
+ }
55
+ function listSyncedSessions(syncDir) {
56
+ const sessionsDir = join(syncDir, "sessions");
57
+ if (!existsSync(sessionsDir)) return [];
58
+ const entries = readdirSync(sessionsDir).filter((entry) => {
59
+ const entryPath = join(sessionsDir, entry);
60
+ return statSync(entryPath).isDirectory();
61
+ });
62
+ const metas = [];
63
+ for (const entry of entries) {
64
+ const metaPath = join(sessionsDir, entry, "meta.json");
65
+ const meta = readJson(metaPath);
66
+ if (meta) metas.push(meta);
67
+ }
68
+ return metas.sort(
69
+ (a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime()
70
+ );
71
+ }
72
+ function remapProjectPath(originalPath, currentHome) {
73
+ const homePattern = /^(\/(?:Users|home)\/[^/]+)(\/.*)?$/;
74
+ const match = originalPath.match(homePattern);
75
+ if (!match) return originalPath;
76
+ const origHomeDir = match[1];
77
+ const rest = match[2] ?? "";
78
+ if (origHomeDir === currentHome) return originalPath;
79
+ return currentHome + rest;
80
+ }
81
+ function saveSession(opts) {
82
+ const { jsonlPath, sessionId, label, projectPath, syncDir, previewText } = opts;
83
+ const destDir = join(syncDir, "sessions", sessionId);
84
+ const destJsonl = join(destDir, "session.jsonl");
85
+ const destMeta = join(destDir, "meta.json");
86
+ copyFileSync(jsonlPath, destJsonl);
87
+ const meta = {
88
+ sessionId,
89
+ label,
90
+ projectPath,
91
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
92
+ machine: hostname(),
93
+ previewText
94
+ };
95
+ writeJson(destMeta, meta);
96
+ }
97
+ function loadSession(meta, localProjectPath, syncDir) {
98
+ const encodedPath = encodeProjectPath(localProjectPath);
99
+ const destDir = claudeSessionDir(encodedPath);
100
+ const destPath = join(destDir, `${meta.sessionId}.jsonl`);
101
+ const srcPath = join(syncDir, "sessions", meta.sessionId, "session.jsonl");
102
+ copyFileSync(srcPath, destPath);
103
+ }
104
+ function hostname() {
105
+ try {
106
+ return osHostname();
107
+ } catch {
108
+ return "unknown";
109
+ }
110
+ }
111
+
112
+ // src/commands/session.ts
113
+ async function resolveSyncDir() {
114
+ const config = loadConfig();
115
+ if (config.sessionSyncDir) return config.sessionSyncDir;
116
+ log.warn("No session sync folder configured.");
117
+ const syncDir = await input({
118
+ message: "Enter path to your sync folder (e.g. ~/Dropbox/cf-sessions or a git repo path):",
119
+ validate: (v) => v.trim().length > 0 || "Path cannot be empty"
120
+ });
121
+ const resolved = syncDir.startsWith("~/") ? join2(homedir2(), syncDir.slice(2)) : syncDir;
122
+ mergeJson(globalConfigPath(), { sessionSyncDir: resolved });
123
+ log.success(`Sync folder saved to global config: ${resolved}`);
124
+ log.warn(
125
+ "Session files contain your full conversation history. Make sure this folder is private."
126
+ );
127
+ return resolved;
128
+ }
129
+ function formatSessionChoice(meta) {
130
+ const date = new Date(meta.savedAt).toLocaleString();
131
+ const preview = meta.previewText.slice(0, 60).replace(/\n/g, " ");
132
+ return `[${meta.label}] ${date} @${meta.machine} \u2014 ${preview}`;
133
+ }
134
+ async function sessionSaveCommand(opts = {}) {
135
+ const syncDir = await resolveSyncDir();
136
+ const cwd = process.cwd();
137
+ let jsonlPath = null;
138
+ if (opts.sessionId) {
139
+ const candidate = join2(
140
+ claudeSessionDir(encodeProjectPath(cwd)),
141
+ `${opts.sessionId}.jsonl`
142
+ );
143
+ if (!existsSync2(candidate)) {
144
+ log.error(`Session file not found: ${candidate}`);
145
+ process.exit(1);
146
+ }
147
+ jsonlPath = candidate;
148
+ } else {
149
+ const sessionDir = claudeSessionDir(encodeProjectPath(cwd));
150
+ if (!existsSync2(sessionDir)) {
151
+ log.error(
152
+ `No sessions found for current directory. Run this inside a project that has Claude Code sessions.`
153
+ );
154
+ process.exit(1);
155
+ }
156
+ const files = readdirSync2(sessionDir).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-")).map((f) => ({ name: f, mtime: statSync2(join2(sessionDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
157
+ if (files.length === 0) {
158
+ log.error("No session files found in current project directory.");
159
+ process.exit(1);
160
+ }
161
+ const recentThreshold = 6e4;
162
+ const recentFiles = files.filter(
163
+ (f) => files[0].mtime - f.mtime < recentThreshold
164
+ );
165
+ if (recentFiles.length > 1) {
166
+ const chosen = await select({
167
+ message: "Multiple recent sessions found. Which one to save?",
168
+ choices: recentFiles.map((f) => ({
169
+ name: `${f.name} (modified ${new Date(f.mtime).toLocaleString()})`,
170
+ value: join2(sessionDir, f.name)
171
+ }))
172
+ });
173
+ jsonlPath = chosen;
174
+ } else {
175
+ jsonlPath = join2(sessionDir, files[0].name);
176
+ }
177
+ }
178
+ const sessionId = basename(jsonlPath, ".jsonl");
179
+ const label = opts.label ?? await input({
180
+ message: "Give this session a label:",
181
+ default: `session-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`
182
+ });
183
+ const previewText = buildPreviewText(jsonlPath);
184
+ saveSession({
185
+ jsonlPath,
186
+ sessionId,
187
+ label,
188
+ projectPath: cwd,
189
+ syncDir,
190
+ previewText
191
+ });
192
+ log.success(`Session saved: "${label}"`);
193
+ log.dim(` \u2192 ${join2(syncDir, "sessions", sessionId)}`);
194
+ }
195
+ async function sessionLoadCommand() {
196
+ const syncDir = await resolveSyncDir();
197
+ const sessions = listSyncedSessions(syncDir);
198
+ if (sessions.length === 0) {
199
+ log.warn("No saved sessions found in sync folder.");
200
+ log.dim(` Sync folder: ${syncDir}`);
201
+ log.dim(" Run /cf-session inside a Claude Code conversation to save one.");
202
+ return;
203
+ }
204
+ const chosen = await select({
205
+ message: "Choose a session to load:",
206
+ choices: sessions.map((s) => ({
207
+ name: formatSessionChoice(s),
208
+ value: s
209
+ }))
210
+ });
211
+ const currentHome = homedir2();
212
+ const remapped = remapProjectPath(chosen.projectPath, currentHome);
213
+ let localProjectPath = remapped;
214
+ if (remapped !== chosen.projectPath) {
215
+ log.step(
216
+ `Original path: ${chosen.projectPath}
217
+ Remapped to: ${remapped}`
218
+ );
219
+ const confirmed = await input({
220
+ message: "Local project path (press Enter to accept or edit):",
221
+ default: remapped
222
+ });
223
+ localProjectPath = confirmed.trim() || remapped;
224
+ }
225
+ loadSession(chosen, localProjectPath, syncDir);
226
+ log.success(`Session "${chosen.label}" loaded.`);
227
+ log.info(`To resume, run:`);
228
+ console.log(`
229
+ claude --resume ${chosen.sessionId}
230
+ `);
231
+ }
232
+ export {
233
+ sessionLoadCommand,
234
+ sessionSaveCommand
235
+ };
@@ -4,15 +4,14 @@ import {
4
4
  saveStatuslineConfig,
5
5
  selectStatuslineComponents,
6
6
  writeStatuslineSettings
7
- } from "./chunk-FWHEMJS3.js";
7
+ } from "./chunk-BPLN4LDL.js";
8
8
  import {
9
9
  ALL_COMPONENT_IDS
10
- } from "./chunk-JWAJ4XPK.js";
11
- import "./chunk-WHCJT7E2.js";
10
+ } from "./chunk-PGLUEN7D.js";
11
+ import "./chunk-TPRZHSFS.js";
12
12
  import {
13
13
  log
14
- } from "./chunk-6DUFTBTO.js";
15
- import "./chunk-IUTXHCP7.js";
14
+ } from "./chunk-W5CD7WTX.js";
16
15
 
17
16
  // src/commands/statusline.ts
18
17
  import { confirm } from "@inquirer/prompts";
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  isMarketplaceRegistered,
3
3
  isPluginInstalled
4
- } from "./chunk-MRTR7TJ4.js";
4
+ } from "./chunk-HFLBFX6J.js";
5
5
  import {
6
6
  hasShellCompletion,
7
7
  removeShellCompletion
8
- } from "./chunk-4PLV2ENL.js";
8
+ } from "./chunk-FYGACWU6.js";
9
9
  import {
10
10
  commandExists,
11
11
  run
@@ -15,15 +15,13 @@ import {
15
15
  devStatePath,
16
16
  globalConfigDir,
17
17
  marketplaceCachePath,
18
- marketplaceClonePath
19
- } from "./chunk-WHCJT7E2.js";
20
- import {
21
- log
22
- } from "./chunk-6DUFTBTO.js";
23
- import {
18
+ marketplaceClonePath,
24
19
  readJson,
25
20
  writeJson
26
- } from "./chunk-IUTXHCP7.js";
21
+ } from "./chunk-TPRZHSFS.js";
22
+ import {
23
+ log
24
+ } from "./chunk-W5CD7WTX.js";
27
25
 
28
26
  // src/commands/uninstall.ts
29
27
  import { existsSync, rmSync } from "fs";
@@ -80,7 +78,7 @@ function nothingToRemove(d) {
80
78
  }
81
79
  async function uninstallCommand() {
82
80
  console.log(`
83
- === ${chalk.red("Coding Friend Uninstall")} ===`);
81
+ === \u{1F44B} ${chalk.red("Coding Friend Uninstall")} \u{1F44B} ===`);
84
82
  if (!commandExists("claude")) {
85
83
  log.error("Claude CLI not found. Cannot uninstall plugin without it.");
86
84
  log.dim(
@@ -0,0 +1,16 @@
1
+ import {
2
+ getLatestVersion,
3
+ semverCompare,
4
+ updateCommand
5
+ } from "./chunk-JS75SVQA.js";
6
+ import "./chunk-BPLN4LDL.js";
7
+ import "./chunk-FYGACWU6.js";
8
+ import "./chunk-PGLUEN7D.js";
9
+ import "./chunk-UFGNO6CW.js";
10
+ import "./chunk-TPRZHSFS.js";
11
+ import "./chunk-W5CD7WTX.js";
12
+ export {
13
+ getLatestVersion,
14
+ semverCompare,
15
+ updateCommand
16
+ };
@@ -1,5 +1,11 @@
1
1
  # Changelog (Learn Host)
2
2
 
3
+ ## v0.2.1 (2026-03-05)
4
+
5
+ - Add package manager tabs (npm, yarn, pnpm) to website ([#72e9e05](https://github.com/dinhanhthi/coding-friend/commit/72e9e05))
6
+ - Fix TOC heading text stripping markdown links from slug generation ([#9a8fb5c](https://github.com/dinhanhthi/coding-friend/commit/9a8fb5c))
7
+ - Decorate inline codes for TOC ([#573d7b0](https://github.com/dinhanhthi/coding-friend/commit/573d7b0))
8
+
3
9
  ## v0.2.0 (2026-03-03)
4
10
 
5
11
  - Add dedicated tag pages for filtering docs by tag ([#06f5847](https://github.com/dinhanhthi/coding-friend/commit/06f5847))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-learn-host",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev -p 3333",
@@ -37,7 +37,9 @@ export default async function DocPage({
37
37
  />
38
38
 
39
39
  <header className="mb-8">
40
- <h1 className="mb-2 text-3xl font-bold">{doc.frontmatter.title}</h1>
40
+ <h1 className="text-accent mb-2 text-3xl font-bold">
41
+ {doc.frontmatter.title}
42
+ </h1>
41
43
  {(doc.frontmatter.created || doc.frontmatter.updated) && (
42
44
  <div className="flex flex-wrap items-center gap-3 text-sm text-slate-500 dark:text-slate-400">
43
45
  {doc.frontmatter.created && (
@@ -119,3 +119,23 @@ pre {
119
119
  padding: 1px 5px;
120
120
  border-radius: 4px;
121
121
  }
122
+
123
+ /* Tighter prose list spacing */
124
+ .prose
125
+ :where(ul, ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
126
+ margin-top: 0.5em;
127
+ margin-bottom: 0.5em;
128
+ }
129
+
130
+ .prose :where(li):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
131
+ margin-top: 0.25em;
132
+ margin-bottom: 0.25em;
133
+ }
134
+
135
+ .prose
136
+ :where(li > p, li > ul, li > ol, li > pre, li > blockquote, li > div):not(
137
+ :where([class~="not-prose"], [class~="not-prose"] *)
138
+ ) {
139
+ margin-top: 0.35em;
140
+ margin-bottom: 0.35em;
141
+ }
@@ -3,6 +3,20 @@
3
3
  import { useEffect, useState } from "react";
4
4
  import type { TocItem } from "@/lib/types";
5
5
 
6
+ function renderText(text: string) {
7
+ const stripped = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
8
+ const parts = stripped.split(/(`[^`]+`)/g);
9
+ return parts.map((part, i) =>
10
+ part.startsWith("`") && part.endsWith("`") ? (
11
+ <code key={i} className="rounded bg-slate-700/60 px-1 py-0.5 text-xs">
12
+ {part.slice(1, -1)}
13
+ </code>
14
+ ) : (
15
+ part
16
+ ),
17
+ );
18
+ }
19
+
6
20
  interface Props {
7
21
  headings: TocItem[];
8
22
  }
@@ -51,7 +65,7 @@ export default function TableOfContents({ headings }: Props) {
51
65
  : "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
52
66
  }`}
53
67
  >
54
- {h.text}
68
+ {renderText(h.text)}
55
69
  </a>
56
70
  </li>
57
71
  ))}
@@ -189,7 +189,8 @@ export function extractHeadings(content: string): TocItem[] {
189
189
  const regex = /^(#{2,3})\s+(.+)$/gm;
190
190
  let match;
191
191
  while ((match = regex.exec(content)) !== null) {
192
- const text = match[2].trim();
192
+ const raw = match[2].trim();
193
+ const text = raw.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
193
194
  const id = text
194
195
  .toLowerCase()
195
196
  .replace(/[^a-z0-9]+/g, "-")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,28 +0,0 @@
1
- // src/lib/json.ts
2
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
3
- import { dirname } from "path";
4
- function readJson(filePath) {
5
- try {
6
- const content = readFileSync(filePath, "utf-8");
7
- return JSON.parse(content);
8
- } catch {
9
- return null;
10
- }
11
- }
12
- function writeJson(filePath, data) {
13
- const dir = dirname(filePath);
14
- if (!existsSync(dir)) {
15
- mkdirSync(dir, { recursive: true });
16
- }
17
- writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
18
- }
19
- function mergeJson(filePath, data) {
20
- const existing = readJson(filePath) ?? {};
21
- writeJson(filePath, { ...existing, ...data });
22
- }
23
-
24
- export {
25
- readJson,
26
- writeJson,
27
- mergeJson
28
- };
@@ -1,44 +0,0 @@
1
- import {
2
- globalConfigPath,
3
- localConfigPath,
4
- resolvePath
5
- } from "./chunk-WHCJT7E2.js";
6
- import {
7
- readJson
8
- } from "./chunk-IUTXHCP7.js";
9
-
10
- // src/lib/config.ts
11
- function resolveDocsDir(explicitPath) {
12
- if (explicitPath) {
13
- return resolvePath(explicitPath);
14
- }
15
- const local = readJson(localConfigPath());
16
- if (local?.learn?.outputDir) {
17
- return resolvePath(local.learn.outputDir);
18
- }
19
- const global = readJson(globalConfigPath());
20
- if (global?.learn?.outputDir) {
21
- return resolvePath(global.learn.outputDir);
22
- }
23
- return resolvePath("docs/learn");
24
- }
25
-
26
- // src/lib/lib-path.ts
27
- import { existsSync } from "fs";
28
- import { dirname, join } from "path";
29
- import { fileURLToPath } from "url";
30
- var __dirname = dirname(fileURLToPath(import.meta.url));
31
- function getLibPath(name) {
32
- const bundled = join(__dirname, "..", "lib", name);
33
- if (existsSync(bundled)) return bundled;
34
- const dev = join(__dirname, "..", "..", "lib", name);
35
- if (existsSync(dev)) return dev;
36
- throw new Error(
37
- `Could not find lib/${name}. Ensure it exists in the CLI package.`
38
- );
39
- }
40
-
41
- export {
42
- resolveDocsDir,
43
- getLibPath
44
- };