coding-friend-cli 1.15.0 → 1.17.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 (75) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-PYRGNY5P.js → chunk-C5LYVVEI.js} +9 -1
  3. package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
  4. package/dist/{chunk-ITL5TY3B.js → chunk-G6CEEMAR.js} +3 -3
  5. package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
  6. package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
  7. package/dist/{config-UQ742WPQ.js → config-LZFXXOI4.js} +276 -14
  8. package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
  9. package/dist/{disable-AOZ7FLZD.js → disable-R6K5YJN4.js} +2 -2
  10. package/dist/{enable-MJVTT3RU.js → enable-HF4PYVJN.js} +2 -2
  11. package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
  12. package/dist/index.js +78 -18
  13. package/dist/{init-AHIEQ27W.js → init-YK6YRTOT.js} +271 -23
  14. package/dist/{install-EIN7Z5V3.js → install-Q4PWEU43.js} +4 -4
  15. package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
  16. package/dist/memory-7RM67ZLS.js +668 -0
  17. package/dist/postinstall.js +1 -1
  18. package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
  19. package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
  20. package/dist/{uninstall-2IOZZERP.js → uninstall-3PSUDGI4.js} +3 -3
  21. package/dist/{update-IZ5UEKZN.js → update-WL6SFGGO.js} +4 -4
  22. package/lib/cf-memory/CHANGELOG.md +15 -0
  23. package/lib/cf-memory/README.md +284 -0
  24. package/lib/cf-memory/package-lock.json +2790 -0
  25. package/lib/cf-memory/package.json +31 -0
  26. package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
  27. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
  28. package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
  29. package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
  30. package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
  31. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
  32. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
  33. package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
  34. package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
  35. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
  36. package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
  37. package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
  38. package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
  39. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
  40. package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
  41. package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
  42. package/lib/cf-memory/src/backends/markdown.ts +318 -0
  43. package/lib/cf-memory/src/backends/minisearch.ts +203 -0
  44. package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
  45. package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
  46. package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
  47. package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
  48. package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
  49. package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
  50. package/lib/cf-memory/src/daemon/entry.ts +99 -0
  51. package/lib/cf-memory/src/daemon/process.ts +220 -0
  52. package/lib/cf-memory/src/daemon/server.ts +166 -0
  53. package/lib/cf-memory/src/daemon/watcher.ts +90 -0
  54. package/lib/cf-memory/src/index.ts +45 -0
  55. package/lib/cf-memory/src/lib/backend.ts +23 -0
  56. package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
  57. package/lib/cf-memory/src/lib/dedup.ts +80 -0
  58. package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
  59. package/lib/cf-memory/src/lib/ollama.ts +76 -0
  60. package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
  61. package/lib/cf-memory/src/lib/tier.ts +107 -0
  62. package/lib/cf-memory/src/lib/types.ts +109 -0
  63. package/lib/cf-memory/src/resources/index.ts +62 -0
  64. package/lib/cf-memory/src/server.ts +20 -0
  65. package/lib/cf-memory/src/tools/delete.ts +38 -0
  66. package/lib/cf-memory/src/tools/list.ts +38 -0
  67. package/lib/cf-memory/src/tools/retrieve.ts +52 -0
  68. package/lib/cf-memory/src/tools/search.ts +47 -0
  69. package/lib/cf-memory/src/tools/store.ts +70 -0
  70. package/lib/cf-memory/src/tools/update.ts +62 -0
  71. package/lib/cf-memory/tsconfig.json +15 -0
  72. package/lib/cf-memory/vitest.config.ts +7 -0
  73. package/lib/learn-host/CHANGELOG.md +4 -1
  74. package/lib/learn-host/package.json +1 -1
  75. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,35 +14,35 @@ program.name("cf").description(
14
14
  "coding-friend CLI \u2014 host learning docs, setup MCP, init projects"
15
15
  ).version(pkg.version, "-v, --version");
16
16
  program.command("install").description("Install the Coding Friend plugin into Claude Code").option("--user", "Install at user scope (all projects)").option("--global", "Install at user scope (all projects)").option("--project", "Install at project scope (shared via git)").option("--local", "Install at local scope (this machine only)").action(async (opts) => {
17
- const { installCommand } = await import("./install-EIN7Z5V3.js");
17
+ const { installCommand } = await import("./install-Q4PWEU43.js");
18
18
  await installCommand(opts);
19
19
  });
20
20
  program.command("uninstall").description("Uninstall the Coding Friend plugin from Claude Code").option("--user", "Uninstall from user scope (all projects)").option("--global", "Uninstall from user scope (all projects)").option("--project", "Uninstall from project scope").option("--local", "Uninstall from local scope").action(async (opts) => {
21
- const { uninstallCommand } = await import("./uninstall-2IOZZERP.js");
21
+ const { uninstallCommand } = await import("./uninstall-3PSUDGI4.js");
22
22
  await uninstallCommand(opts);
23
23
  });
24
24
  program.command("disable").description("Disable the Coding Friend plugin without uninstalling").option("--user", "Disable at user scope (all projects)").option("--global", "Disable at user scope (all projects)").option("--project", "Disable at project scope").option("--local", "Disable at local scope").action(async (opts) => {
25
- const { disableCommand } = await import("./disable-AOZ7FLZD.js");
25
+ const { disableCommand } = await import("./disable-R6K5YJN4.js");
26
26
  await disableCommand(opts);
27
27
  });
28
28
  program.command("enable").description("Re-enable the Coding Friend plugin").option("--user", "Enable at user scope (all projects)").option("--global", "Enable at user scope (all projects)").option("--project", "Enable at project scope").option("--local", "Enable at local scope").action(async (opts) => {
29
- const { enableCommand } = await import("./enable-MJVTT3RU.js");
29
+ const { enableCommand } = await import("./enable-HF4PYVJN.js");
30
30
  await enableCommand(opts);
31
31
  });
32
32
  program.command("init").description("Initialize coding-friend in current project").action(async () => {
33
- const { initCommand } = await import("./init-AHIEQ27W.js");
33
+ const { initCommand } = await import("./init-YK6YRTOT.js");
34
34
  await initCommand();
35
35
  });
36
36
  program.command("config").description("Manage Coding Friend configuration").action(async () => {
37
- const { configCommand } = await import("./config-UQ742WPQ.js");
37
+ const { configCommand } = await import("./config-LZFXXOI4.js");
38
38
  await configCommand();
39
39
  });
40
40
  program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
41
- const { hostCommand } = await import("./host-NA7LZ4HX.js");
41
+ const { hostCommand } = await import("./host-SYZH3FVC.js");
42
42
  await hostCommand(path, opts);
43
43
  });
44
44
  program.command("mcp").description("Setup MCP server for learning docs").argument("[path]", "path to docs folder").action(async (path) => {
45
- const { mcpCommand } = await import("./mcp-DLS3J6QJ.js");
45
+ const { mcpCommand } = await import("./mcp-TBEDYELW.js");
46
46
  await mcpCommand(path);
47
47
  });
48
48
  program.command("permission").description("Manage Claude Code permission rules for Coding Friend").option("--all", "Apply all recommended permissions without prompts").action(async (opts) => {
@@ -50,11 +50,11 @@ program.command("permission").description("Manage Claude Code permission rules f
50
50
  await permissionCommand(opts);
51
51
  });
52
52
  program.command("statusline").description("Setup coding-friend statusline in Claude Code").action(async () => {
53
- const { statuslineCommand } = await import("./statusline-6HQCDWBD.js");
53
+ const { statuslineCommand } = await import("./statusline-6Y2EBAFQ.js");
54
54
  await statuslineCommand();
55
55
  });
56
56
  program.command("update").description("Update coding-friend plugin, CLI, and statusline").option("--cli", "Update only the CLI (npm package)").option("--plugin", "Update only the Claude Code plugin").option("--statusline", "Update only the statusline").option("--user", "Update plugin at user scope (all projects)").option("--global", "Update plugin at user scope (all projects)").option("--project", "Update plugin at project scope").option("--local", "Update plugin at local scope").action(async (opts) => {
57
- const { updateCommand } = await import("./update-IZ5UEKZN.js");
57
+ const { updateCommand } = await import("./update-WL6SFGGO.js");
58
58
  await updateCommand(opts);
59
59
  });
60
60
  var session = program.command("session").description("Save and load Claude Code sessions across machines");
@@ -69,13 +69,73 @@ session.command("save").description("Save current Claude Code session to sync fo
69
69
  "-s, --session-id <id>",
70
70
  "session UUID to save (default: auto-detect newest)"
71
71
  ).option("-l, --label <label>", "label for this session").action(async (opts) => {
72
- const { sessionSaveCommand } = await import("./session-E3CZJJZQ.js");
72
+ const { sessionSaveCommand } = await import("./session-H4XW2WXH.js");
73
73
  await sessionSaveCommand(opts);
74
74
  });
75
75
  session.command("load").description("Load a saved session from sync folder").action(async () => {
76
- const { sessionLoadCommand } = await import("./session-E3CZJJZQ.js");
76
+ const { sessionLoadCommand } = await import("./session-H4XW2WXH.js");
77
77
  await sessionLoadCommand();
78
78
  });
79
+ var memory = program.command("memory").description("AI memory system \u2014 store and search project knowledge");
80
+ program.addHelpText(
81
+ "after",
82
+ `
83
+ Memory subcommands:
84
+ memory status Show memory system status (tier, doc count, daemon)
85
+ memory search Search memories by query
86
+ memory list List memories in current project (--projects for all DBs)
87
+ memory rm Remove a project database (--project-id <id>, --all, or --prune)
88
+ memory init Initialize Tier 1 (install SQLite deps, import existing memories)
89
+ memory start Start the memory daemon (Tier 2)
90
+ memory stop Stop the memory daemon
91
+ memory rebuild Rebuild the daemon search index
92
+ memory mcp Show MCP server setup instructions`
93
+ );
94
+ memory.command("status").description("Show memory system status").action(async () => {
95
+ const { memoryStatusCommand } = await import("./memory-7RM67ZLS.js");
96
+ await memoryStatusCommand();
97
+ });
98
+ memory.command("search").description("Search memories by query").argument("<query>", "search query").action(async (query) => {
99
+ const { memorySearchCommand } = await import("./memory-7RM67ZLS.js");
100
+ await memorySearchCommand(query);
101
+ });
102
+ memory.command("list").description(
103
+ "List memories in current project, or all projects with --projects"
104
+ ).option("--projects", "List all project databases with size and metadata").action(async (opts) => {
105
+ const { memoryListCommand } = await import("./memory-7RM67ZLS.js");
106
+ await memoryListCommand(opts);
107
+ });
108
+ memory.command("init").description(
109
+ "Initialize Tier 1 \u2014 install SQLite deps and import existing memories"
110
+ ).action(async () => {
111
+ const { memoryInitCommand } = await import("./memory-7RM67ZLS.js");
112
+ await memoryInitCommand();
113
+ });
114
+ memory.command("start").description("Start the memory daemon (Tier 2 \u2014 MiniSearch)").action(async () => {
115
+ const { memoryStartCommand } = await import("./memory-7RM67ZLS.js");
116
+ await memoryStartCommand();
117
+ });
118
+ memory.command("stop").description("Stop the memory daemon").action(async () => {
119
+ const { memoryStopCommand } = await import("./memory-7RM67ZLS.js");
120
+ await memoryStopCommand();
121
+ });
122
+ memory.command("rebuild").description("Rebuild the daemon search index").action(async () => {
123
+ const { memoryRebuildCommand } = await import("./memory-7RM67ZLS.js");
124
+ await memoryRebuildCommand();
125
+ });
126
+ memory.command("mcp").description("Show MCP server setup instructions").action(async () => {
127
+ const { memoryMcpCommand } = await import("./memory-7RM67ZLS.js");
128
+ await memoryMcpCommand();
129
+ });
130
+ memory.command("rm").description("Remove a project database").option("--project-id <id>", "Project ID to remove").option("--all", "Remove all project databases").option(
131
+ "--prune",
132
+ "Remove orphaned projects (source dir missing or 0 memories)"
133
+ ).action(
134
+ async (opts) => {
135
+ const { memoryRmCommand } = await import("./memory-7RM67ZLS.js");
136
+ await memoryRmCommand(opts);
137
+ }
138
+ );
79
139
  var dev = program.command("dev").description("Development mode commands");
80
140
  program.addHelpText(
81
141
  "after",
@@ -89,35 +149,35 @@ Dev subcommands:
89
149
  dev update [path] Update local dev plugin to latest version`
90
150
  );
91
151
  dev.command("on").description("Switch to local plugin source").argument("[path]", "path to local coding-friend repo (default: cwd)").action(async (path) => {
92
- const { devOnCommand } = await import("./dev-WJ5QQ35B.js");
152
+ const { devOnCommand } = await import("./dev-R3IYWZ3M.js");
93
153
  await devOnCommand(path);
94
154
  });
95
155
  dev.command("off").description("Switch back to remote marketplace").action(async () => {
96
- const { devOffCommand } = await import("./dev-WJ5QQ35B.js");
156
+ const { devOffCommand } = await import("./dev-R3IYWZ3M.js");
97
157
  await devOffCommand();
98
158
  });
99
159
  dev.command("status").description("Show current dev mode").action(async () => {
100
- const { devStatusCommand } = await import("./dev-WJ5QQ35B.js");
160
+ const { devStatusCommand } = await import("./dev-R3IYWZ3M.js");
101
161
  await devStatusCommand();
102
162
  });
103
163
  dev.command("sync").description(
104
164
  "Copy local source files to plugin cache (no version bump needed)"
105
165
  ).action(async () => {
106
- const { devSyncCommand } = await import("./dev-WJ5QQ35B.js");
166
+ const { devSyncCommand } = await import("./dev-R3IYWZ3M.js");
107
167
  await devSyncCommand();
108
168
  });
109
169
  dev.command("restart").description("Reinstall local dev plugin (off + on)").argument(
110
170
  "[path]",
111
171
  "path to local coding-friend repo (default: saved path or cwd)"
112
172
  ).action(async (path) => {
113
- const { devRestartCommand } = await import("./dev-WJ5QQ35B.js");
173
+ const { devRestartCommand } = await import("./dev-R3IYWZ3M.js");
114
174
  await devRestartCommand(path);
115
175
  });
116
176
  dev.command("update").description("Update local dev plugin to latest version (off + on)").argument(
117
177
  "[path]",
118
178
  "path to local coding-friend repo (default: saved path or cwd)"
119
179
  ).action(async (path) => {
120
- const { devUpdateCommand } = await import("./dev-WJ5QQ35B.js");
180
+ const { devUpdateCommand } = await import("./dev-R3IYWZ3M.js");
121
181
  await devUpdateCommand(path);
122
182
  });
123
183
  program.parse();
@@ -4,6 +4,9 @@ import {
4
4
  getExistingRules,
5
5
  getMissingRules
6
6
  } from "./chunk-56U7US6J.js";
7
+ import {
8
+ getLibPath
9
+ } from "./chunk-RZRT7NGT.js";
7
10
  import {
8
11
  findStatuslineHookPath,
9
12
  isStatuslineConfigured,
@@ -17,28 +20,30 @@ import {
17
20
  import {
18
21
  ensureShellCompletion,
19
22
  hasShellCompletion
20
- } from "./chunk-KJUGTLPQ.js";
23
+ } from "./chunk-YO6JKGR3.js";
21
24
  import {
22
25
  BACK,
23
26
  applyDocsDirChange,
24
27
  askScope,
28
+ ensureDocsFolders,
25
29
  formatScopeLabel,
26
30
  getMergedValue,
27
31
  getScopeLabel,
28
32
  injectBackChoice,
29
33
  showConfigHint
30
- } from "./chunk-PYRGNY5P.js";
34
+ } from "./chunk-C5LYVVEI.js";
31
35
  import {
32
36
  commandExists,
33
37
  run
34
- } from "./chunk-X5WEODUD.js";
38
+ } from "./chunk-CYQU33FY.js";
35
39
  import {
36
40
  claudeSettingsPath,
37
41
  globalConfigPath,
38
42
  localConfigPath,
39
43
  mergeJson,
40
44
  readJson,
41
- resolvePath
45
+ resolvePath,
46
+ writeJson
42
47
  } from "./chunk-RWUTFVRB.js";
43
48
  import {
44
49
  log
@@ -48,6 +53,7 @@ import {
48
53
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
49
54
  import { existsSync, readFileSync, writeFileSync } from "fs";
50
55
  import { homedir } from "os";
56
+ import { join } from "path";
51
57
  import chalk from "chalk";
52
58
  var GITIGNORE_START = "# >>> coding-friend managed";
53
59
  var GITIGNORE_END = "# <<< coding-friend managed";
@@ -84,6 +90,76 @@ function isGitRepo() {
84
90
  function escapeRegExp(str) {
85
91
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
86
92
  }
93
+ function hasGitignoreBlock() {
94
+ if (!existsSync(".gitignore")) return false;
95
+ const content = readFileSync(".gitignore", "utf-8");
96
+ return content.includes(GITIGNORE_START) || content.includes("# coding-friend");
97
+ }
98
+ function paddedScopeLabel(scope) {
99
+ const label = formatScopeLabel(scope);
100
+ const visibleLen = scope.length + 2;
101
+ return label + " ".repeat(Math.max(1, 9 - visibleLen));
102
+ }
103
+ function printSetupStatus(globalCfg, localCfg, gitAvailable) {
104
+ const docsDir = getDocsDir(globalCfg, localCfg);
105
+ const subfolders = ["plans", "memory", "research", "learn", "sessions"];
106
+ const folderStatus = subfolders.map((sub) => ({
107
+ name: `${docsDir}/${sub}`,
108
+ exists: existsSync(`${docsDir}/${sub}`)
109
+ }));
110
+ const foldersReady = folderStatus.filter((f) => f.exists).length;
111
+ const missingFolders = subfolders.length - foldersReady;
112
+ const allFoldersDone = missingFolders === 0;
113
+ const countColor = allFoldersDone ? chalk.green : chalk.yellow;
114
+ console.log(
115
+ ` ${chalk.bold("Folders")} ${countColor(`(${foldersReady}/${subfolders.length})`)}:`
116
+ );
117
+ for (const f of folderStatus) {
118
+ const icon = f.exists ? chalk.green("\u2713") : chalk.red("\u2717");
119
+ const name = f.exists ? chalk.dim(f.name) : f.name;
120
+ console.log(` ${icon} ${name}`);
121
+ }
122
+ console.log();
123
+ const configKeys = [
124
+ { key: "docsDir", label: "Docs folder" },
125
+ { key: "language", label: "Docs language" },
126
+ { key: "learn", label: "/cf-learn config" }
127
+ ];
128
+ let notLocalCount = 0;
129
+ let notConfiguredCount = 0;
130
+ console.log(` ${chalk.bold("Settings")}:`);
131
+ for (const s of configKeys) {
132
+ const scope = getScopeLabel(s.key, globalCfg, localCfg);
133
+ const value = getMergedValue(s.key, globalCfg, localCfg);
134
+ const valueStr = value && typeof value === "string" ? ` (${chalk.dim(value)})` : "";
135
+ console.log(` ${paddedScopeLabel(scope)}${s.label}${valueStr}`);
136
+ if (scope === "-") notConfiguredCount++;
137
+ if (scope === "-" || scope === "global") notLocalCount++;
138
+ }
139
+ const setupItems = [
140
+ {
141
+ label: ".gitignore",
142
+ done: !gitAvailable || hasGitignoreBlock(),
143
+ skipped: !gitAvailable
144
+ },
145
+ { label: "Shell completion", done: hasShellCompletion(), skipped: false },
146
+ { label: "Statusline", done: isStatuslineConfigured(), skipped: false },
147
+ { label: "CF Memory MCP", done: isMemoryMcpConfigured(), skipped: false }
148
+ ];
149
+ for (const item of setupItems) {
150
+ if (item.skipped) {
151
+ console.log(` ${paddedScopeLabel("skip")}${item.label}`);
152
+ } else if (item.done) {
153
+ console.log(` ${paddedScopeLabel("done")}${item.label}`);
154
+ } else {
155
+ console.log(` ${paddedScopeLabel("-")}${item.label}`);
156
+ notConfiguredCount++;
157
+ }
158
+ }
159
+ console.log();
160
+ const allDone = allFoldersDone && notConfiguredCount === 0;
161
+ return { allDone, notLocalCount, notConfiguredCount, missingFolders };
162
+ }
87
163
  function writeToScope(scope, data) {
88
164
  const targetPath = scope === "global" ? globalConfigPath() : localConfigPath();
89
165
  mergeJson(targetPath, data);
@@ -99,6 +175,27 @@ function handleBack(value) {
99
175
  function getDocsDir(globalCfg, localCfg) {
100
176
  return localCfg?.docsDir ?? globalCfg?.docsDir ?? DEFAULT_CONFIG.docsDir;
101
177
  }
178
+ async function offerGlobalShortcut(globalDisplay) {
179
+ const choice = await select({
180
+ message: "How to configure?",
181
+ choices: injectBackChoice(
182
+ [
183
+ {
184
+ name: `Use global setting (${globalDisplay})`,
185
+ value: "use_global"
186
+ },
187
+ { name: "Configure manually", value: "configure" }
188
+ ],
189
+ "Cancel init"
190
+ )
191
+ });
192
+ handleBack(choice);
193
+ if (choice === "use_global") {
194
+ log.dim("Using global setting.");
195
+ return true;
196
+ }
197
+ return false;
198
+ }
102
199
  async function stepDocsDir(globalCfg, localCfg) {
103
200
  const currentValue = getMergedValue("docsDir", globalCfg, localCfg);
104
201
  const scopeLabel = getScopeLabel("docsDir", globalCfg, localCfg);
@@ -106,6 +203,18 @@ async function stepDocsDir(globalCfg, localCfg) {
106
203
  `Docs folder name ${formatScopeLabel(scopeLabel)}${currentValue ? ` (${chalk.dim(currentValue)})` : ""}`,
107
204
  "Where plans, memory, research, and session docs are stored in your project."
108
205
  );
206
+ const globalValue = globalCfg?.docsDir;
207
+ if (globalValue && await offerGlobalShortcut(globalValue)) {
208
+ const DOCS_SUBFOLDERS2 = [
209
+ "plans",
210
+ "memory",
211
+ "research",
212
+ "learn",
213
+ "sessions"
214
+ ];
215
+ ensureDocsFolders(globalValue, DOCS_SUBFOLDERS2);
216
+ return;
217
+ }
109
218
  const value = await input({
110
219
  message: "Docs folder name:",
111
220
  default: currentValue ?? DEFAULT_CONFIG.docsDir,
@@ -126,11 +235,7 @@ async function stepDocsDir(globalCfg, localCfg) {
126
235
  writeToScope(scope, { docsDir: value });
127
236
  }
128
237
  async function stepGitignore(docsDir) {
129
- const hasBlock = (() => {
130
- if (!existsSync(".gitignore")) return false;
131
- const content = readFileSync(".gitignore", "utf-8");
132
- return content.includes(GITIGNORE_START) || content.includes("# coding-friend");
133
- })();
238
+ const hasBlock = hasGitignoreBlock();
134
239
  if (hasBlock) {
135
240
  printStepHeader(
136
241
  `Configure .gitignore ${chalk.green("[done]")}`,
@@ -206,6 +311,8 @@ async function stepDocsLanguage(globalCfg, localCfg) {
206
311
  `Docs language ${formatScopeLabel(scopeLabel)}${currentValue ? ` (${chalk.dim(currentValue)})` : ""}`,
207
312
  "Skills like /cf-plan, /cf-ask, /cf-remember will write docs in this language."
208
313
  );
314
+ const globalValue = globalCfg?.language;
315
+ if (globalValue && await offerGlobalShortcut(globalValue)) return;
209
316
  const lang = await selectLanguage(
210
317
  "What language should generated docs be written in?"
211
318
  );
@@ -243,6 +350,25 @@ async function stepLearnConfig(globalCfg, localCfg, gitAvailable) {
243
350
  `/cf-learn config ${formatScopeLabel(scopeLabel)}`,
244
351
  "Controls where and how /cf-learn saves your learning notes."
245
352
  );
353
+ const globalLearn = globalCfg?.learn;
354
+ if (globalLearn) {
355
+ const parts = [
356
+ globalLearn.language || "en",
357
+ globalLearn.outputDir || `${docsDir}/learn`
358
+ ];
359
+ if (globalLearn.categories) {
360
+ parts.push(`${globalLearn.categories.length} categories`);
361
+ }
362
+ if (await offerGlobalShortcut(parts.join(", "))) {
363
+ const gOutputDir = globalLearn.outputDir || `${docsDir}/learn`;
364
+ const gIsExternal = !gOutputDir.startsWith(`${docsDir}/`);
365
+ return {
366
+ outputDir: gOutputDir,
367
+ autoCommit: globalLearn.autoCommit || false,
368
+ isExternal: gIsExternal
369
+ };
370
+ }
371
+ }
246
372
  const language = await selectLanguage(
247
373
  "What language should /cf-learn notes be written in?"
248
374
  );
@@ -424,6 +550,95 @@ async function stepStatusline() {
424
550
  writeStatuslineSettings(hookResult.hookPath);
425
551
  log.success("Statusline configured!");
426
552
  }
553
+ function isMemoryMcpConfigured() {
554
+ const mcpPath = join(process.cwd(), ".mcp.json");
555
+ const config = readJson(mcpPath);
556
+ if (!config) return false;
557
+ const servers = config.mcpServers;
558
+ return servers != null && "coding-friend-memory" in servers;
559
+ }
560
+ async function stepMemory(docsDir) {
561
+ if (isMemoryMcpConfigured()) {
562
+ printStepHeader(
563
+ `CF Memory MCP ${chalk.green("[done]")}`,
564
+ "Connects the memory system to Claude Code via MCP."
565
+ );
566
+ log.dim("Memory MCP already configured in .mcp.json.");
567
+ return;
568
+ }
569
+ printStepHeader(
570
+ "CF Memory MCP",
571
+ "Connects the memory system to Claude Code via MCP so skills can store and search memories."
572
+ );
573
+ const choice = await select({
574
+ message: "Configure CF Memory MCP server?",
575
+ choices: injectBackChoice(
576
+ [
577
+ { name: "Yes, add to .mcp.json", value: "yes" },
578
+ {
579
+ name: "No, I'll configure it later (cf memory mcp)",
580
+ value: "no"
581
+ }
582
+ ],
583
+ "Cancel init"
584
+ )
585
+ });
586
+ handleBack(choice);
587
+ if (choice === "no") {
588
+ log.dim("Skipped. Run `cf memory mcp` anytime to get the config.");
589
+ return;
590
+ }
591
+ let serverPath;
592
+ try {
593
+ const mcpDir = getLibPath("cf-memory");
594
+ serverPath = join(mcpDir, "dist", "index.js");
595
+ if (!existsSync(serverPath)) {
596
+ log.warn(
597
+ "cf-memory not built yet. Run `cd cli/lib/cf-memory && npm install && npm run build` first."
598
+ );
599
+ return;
600
+ }
601
+ } catch {
602
+ log.warn(
603
+ "cf-memory package not found. Install the CLI first: npm i -g coding-friend-cli"
604
+ );
605
+ return;
606
+ }
607
+ const memoryDir = join(process.cwd(), docsDir, "memory");
608
+ const mcpPath = join(process.cwd(), ".mcp.json");
609
+ const existing = readJson(mcpPath) ?? {};
610
+ const servers = existing.mcpServers ?? {};
611
+ writeJson(mcpPath, {
612
+ ...existing,
613
+ mcpServers: {
614
+ ...servers,
615
+ "coding-friend-memory": {
616
+ command: "node",
617
+ args: [serverPath, memoryDir]
618
+ }
619
+ }
620
+ });
621
+ log.success(`Added coding-friend-memory to .mcp.json`);
622
+ const autoCapture = await confirm({
623
+ message: "Enable auto-capture? (saves session summaries to memory before context compaction)",
624
+ default: false
625
+ });
626
+ if (autoCapture) {
627
+ const scope = await askScope();
628
+ if (scope !== "back") {
629
+ const targetPath = scope === "global" ? globalConfigPath() : localConfigPath();
630
+ const existingConfig = readJson(targetPath);
631
+ const existingMemory = existingConfig?.memory ?? {};
632
+ mergeJson(targetPath, {
633
+ memory: { ...existingMemory, autoCapture: true }
634
+ });
635
+ log.success(`Saved to ${targetPath}`);
636
+ }
637
+ }
638
+ log.info(
639
+ `Tip: Run ${chalk.cyan("/cf-onboard")} in Claude Code to populate memory with project knowledge.`
640
+ );
641
+ }
427
642
  async function stepClaudePermissions(outputDir, autoCommit) {
428
643
  const resolved = resolvePath(outputDir);
429
644
  const homePath = resolved.startsWith(homedir()) ? resolved.replace(homedir(), "~") : resolved;
@@ -472,20 +687,52 @@ async function initCommand() {
472
687
  }
473
688
  const globalCfg = readJson(globalConfigPath());
474
689
  const localCfg = readJson(localConfigPath());
475
- console.log("Current configuration:");
476
- const docsDirScope = getScopeLabel("docsDir", globalCfg, localCfg);
477
- const docsDirVal = getMergedValue("docsDir", globalCfg, localCfg);
478
- console.log(
479
- ` ${formatScopeLabel(docsDirScope)} docsDir${docsDirVal ? ` (${chalk.dim(docsDirVal)})` : ""}`
480
- );
481
- const langScope = getScopeLabel("language", globalCfg, localCfg);
482
- const langVal = getMergedValue("language", globalCfg, localCfg);
483
- console.log(
484
- ` ${formatScopeLabel(langScope)} Docs language${langVal ? ` (${chalk.dim(langVal)})` : ""}`
485
- );
486
- const learnScope = getScopeLabel("learn", globalCfg, localCfg);
487
- console.log(` ${formatScopeLabel(learnScope)} /cf-learn config`);
690
+ console.log("Project status:");
488
691
  console.log();
692
+ const { allDone, notLocalCount, notConfiguredCount, missingFolders } = printSetupStatus(globalCfg, localCfg, gitAvailable);
693
+ if (allDone) {
694
+ if (notLocalCount > 0) {
695
+ console.log(
696
+ chalk.dim(
697
+ ` ${notLocalCount} setting(s) inherited from global config only.`
698
+ )
699
+ );
700
+ console.log();
701
+ }
702
+ log.success("All settings configured!");
703
+ console.log();
704
+ const proceed = await confirm({
705
+ message: "Modify settings?",
706
+ default: false
707
+ });
708
+ if (!proceed) {
709
+ log.dim("No changes. Run `cf init` anytime to reconfigure.");
710
+ return;
711
+ }
712
+ } else {
713
+ const parts = [];
714
+ if (missingFolders > 0) {
715
+ parts.push(`${missingFolders} folder(s) missing`);
716
+ }
717
+ if (notConfiguredCount > 0) {
718
+ parts.push(`${notConfiguredCount} setting(s) not configured`);
719
+ }
720
+ if (notLocalCount > 0) {
721
+ parts.push(`${notLocalCount} not set locally`);
722
+ }
723
+ if (parts.length > 0) {
724
+ console.log(` ${chalk.yellow("\u26A0")} ${parts.join(" \xB7 ")}`);
725
+ console.log();
726
+ }
727
+ const proceed = await confirm({
728
+ message: "Run setup wizard?",
729
+ default: true
730
+ });
731
+ if (!proceed) {
732
+ log.dim("Init cancelled. Run `cf init` anytime to resume.");
733
+ return;
734
+ }
735
+ }
489
736
  await stepDocsDir(globalCfg, localCfg);
490
737
  const updatedGlobal = readJson(globalConfigPath());
491
738
  const updatedLocal = readJson(localConfigPath());
@@ -507,6 +754,7 @@ async function initCommand() {
507
754
  );
508
755
  await stepShellCompletion();
509
756
  await stepStatusline();
757
+ await stepMemory(docsDir);
510
758
  if (isExternal) {
511
759
  printStepHeader(
512
760
  "Configure Claude permissions",
@@ -525,7 +773,7 @@ async function initCommand() {
525
773
  console.log();
526
774
  log.congrats("Setup complete!");
527
775
  log.dim(
528
- "Available commands: /cf-ask, /cf-plan, /cf-fix, /cf-commit, /cf-review, /cf-ship, /cf-optimize, /cf-remember, /cf-learn, /cf-research, /cf-session, /cf-help"
776
+ "Available commands: /cf-ask, /cf-plan, /cf-fix, /cf-commit, /cf-review, /cf-ship, /cf-optimize, /cf-onboard, /cf-remember, /cf-learn, /cf-research, /cf-session, /cf-help"
529
777
  );
530
778
  }
531
779
  export {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getLatestVersion,
3
3
  semverCompare
4
- } from "./chunk-ITL5TY3B.js";
4
+ } from "./chunk-G6CEEMAR.js";
5
5
  import {
6
6
  enableMarketplaceAutoUpdate,
7
7
  isMarketplaceRegistered,
@@ -13,14 +13,14 @@ import {
13
13
  import "./chunk-POC2WHU2.js";
14
14
  import {
15
15
  ensureShellCompletion
16
- } from "./chunk-KJUGTLPQ.js";
16
+ } from "./chunk-YO6JKGR3.js";
17
17
  import {
18
18
  resolveScope
19
- } from "./chunk-PYRGNY5P.js";
19
+ } from "./chunk-C5LYVVEI.js";
20
20
  import {
21
21
  commandExists,
22
22
  run
23
- } from "./chunk-X5WEODUD.js";
23
+ } from "./chunk-CYQU33FY.js";
24
24
  import {
25
25
  devStatePath
26
26
  } from "./chunk-RWUTFVRB.js";
@@ -1,13 +1,13 @@
1
+ import {
2
+ resolveDocsDir
3
+ } from "./chunk-KTX4MGMR.js";
1
4
  import {
2
5
  getLibPath
3
6
  } from "./chunk-RZRT7NGT.js";
4
- import {
5
- resolveDocsDir
6
- } from "./chunk-4DB4XTSL.js";
7
7
  import "./chunk-POC2WHU2.js";
8
8
  import {
9
9
  run
10
- } from "./chunk-X5WEODUD.js";
10
+ } from "./chunk-CYQU33FY.js";
11
11
  import "./chunk-RWUTFVRB.js";
12
12
  import {
13
13
  log