coding-friend-cli 1.17.2 → 1.18.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.
@@ -1,11 +1,15 @@
1
1
  import {
2
- PERMISSION_RULES,
2
+ STATIC_RULES,
3
3
  applyPermissions,
4
+ cleanupStalePluginRules,
5
+ getAllRules,
4
6
  getExistingRules,
5
- groupByCategory
6
- } from "./chunk-56U7US6J.js";
7
+ groupByCategory,
8
+ logPluginScriptWarning
9
+ } from "./chunk-7CAIGH2Y.js";
7
10
  import {
8
- claudeLocalSettingsPath
11
+ claudeLocalSettingsPath,
12
+ claudeSettingsPath
9
13
  } from "./chunk-RWUTFVRB.js";
10
14
  import {
11
15
  log
@@ -34,6 +38,39 @@ function colorDescription(desc) {
34
38
  }
35
39
  return `${main} ${usedBy}`;
36
40
  }
41
+ async function resolveSettingsPath(opts) {
42
+ if (opts.user && opts.project) {
43
+ log.error("Cannot use both --user and --project. Pick one.");
44
+ process.exit(1);
45
+ }
46
+ if (opts.user) {
47
+ const p2 = claudeSettingsPath();
48
+ return { path: p2, label: p2.replace(homedir(), "~"), scope: "user" };
49
+ }
50
+ if (opts.project) {
51
+ const p2 = claudeLocalSettingsPath();
52
+ return { path: p2, label: p2.replace(homedir(), "~"), scope: "project" };
53
+ }
54
+ if (!process.stdin.isTTY) {
55
+ const p2 = claudeLocalSettingsPath();
56
+ return { path: p2, label: p2.replace(homedir(), "~"), scope: "project" };
57
+ }
58
+ const scope = await select({
59
+ message: "Where should permissions be saved?",
60
+ choices: [
61
+ {
62
+ name: "Project \u2014 .claude/settings.local.json (this project only, gitignored)",
63
+ value: "project"
64
+ },
65
+ {
66
+ name: "User \u2014 ~/.claude/settings.json (all projects)",
67
+ value: "user"
68
+ }
69
+ ]
70
+ });
71
+ const p = scope === "user" ? claudeSettingsPath() : claudeLocalSettingsPath();
72
+ return { path: p, label: p.replace(homedir(), "~"), scope };
73
+ }
37
74
  async function interactiveFlow(allRules, existing) {
38
75
  const groups = groupByCategory(allRules);
39
76
  const allRuleStrings = allRules.map((r) => r.rule);
@@ -98,14 +135,17 @@ async function interactiveFlow(allRules, existing) {
98
135
  async function permissionCommand(opts) {
99
136
  console.log("=== \u{1F33F} Coding Friend Permissions \u{1F33F} ===");
100
137
  console.log();
101
- const settingsPath = claudeLocalSettingsPath();
102
- const settingsLabel = settingsPath.replace(homedir(), "~");
138
+ const {
139
+ path: settingsPath,
140
+ label: settingsLabel,
141
+ scope
142
+ } = await resolveSettingsPath(opts);
103
143
  const existing = getExistingRules(settingsPath);
104
- const allRules = PERMISSION_RULES;
144
+ const allRules = getAllRules();
105
145
  const allRuleStrings = allRules.map((r) => r.rule);
106
146
  const managedExisting = existing.filter((r) => allRuleStrings.includes(r));
107
147
  const unmanagedExisting = existing.filter((r) => !allRuleStrings.includes(r));
108
- log.dim(`Settings: ${settingsLabel} (project-local)`);
148
+ log.dim(`Settings: ${settingsLabel} (${scope})`);
109
149
  log.dim(
110
150
  `Current: ${managedExisting.length}/${allRules.length} Coding Friend rules configured`
111
151
  );
@@ -122,13 +162,34 @@ async function permissionCommand(opts) {
122
162
  log.success("All recommended permissions already configured.");
123
163
  return;
124
164
  }
125
- console.log("Adding recommended permissions:");
165
+ const staticSet = new Set(STATIC_RULES.map((r) => r.rule));
166
+ const staticCount = toAdd2.filter((r) => staticSet.has(r)).length;
167
+ const pluginCount = toAdd2.length - staticCount;
168
+ console.log(
169
+ `Adding ${chalk.bold(toAdd2.length)} recommended permissions${pluginCount > 0 ? ` (${staticCount} static + ${pluginCount} plugin scripts)` : ""}:`
170
+ );
126
171
  for (const r of toAdd2) {
127
172
  console.log(` ${chalk.green("+")} ${r}`);
128
173
  }
174
+ if (pluginCount > 0) {
175
+ console.log();
176
+ logPluginScriptWarning(log, chalk);
177
+ }
129
178
  console.log();
179
+ const ok2 = !process.stdin.isTTY || await confirm({
180
+ message: `Apply ${toAdd2.length} permission rules to ${settingsLabel}?`,
181
+ default: true
182
+ });
183
+ if (!ok2) {
184
+ log.dim("Skipped.");
185
+ return;
186
+ }
130
187
  applyPermissions(settingsPath, toAdd2, []);
131
- log.success(`Added ${toAdd2.length} permission rules.`);
188
+ const cleaned = cleanupStalePluginRules(settingsPath);
189
+ if (cleaned > 0) {
190
+ log.dim(`Removed ${cleaned} stale old-format plugin rules.`);
191
+ }
192
+ log.success(`Added ${toAdd2.length} permission rules to ${settingsLabel}.`);
132
193
  return;
133
194
  }
134
195
  const { toAdd, toRemove } = await interactiveFlow(allRules, existing);
@@ -160,7 +221,7 @@ async function permissionCommand(opts) {
160
221
  }
161
222
  applyPermissions(settingsPath, toAdd, toRemove);
162
223
  log.success(
163
- `Done \u2014 added ${toAdd.length}, removed ${toRemove.length} rules.`
224
+ `Done \u2014 added ${toAdd.length}, removed ${toRemove.length} rules in ${settingsLabel}.`
164
225
  );
165
226
  }
166
227
  export {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureShellCompletion
4
- } from "./chunk-YO6JKGR3.js";
4
+ } from "./chunk-VUAUAO2R.js";
5
5
  import "./chunk-W5CD7WTX.js";
6
6
 
7
7
  // src/postinstall.ts
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  hasShellCompletion,
7
7
  removeShellCompletion
8
- } from "./chunk-YO6JKGR3.js";
8
+ } from "./chunk-VUAUAO2R.js";
9
9
  import {
10
10
  resolveScope
11
11
  } from "./chunk-C5LYVVEI.js";
@@ -2,10 +2,10 @@ import {
2
2
  getLatestVersion,
3
3
  semverCompare,
4
4
  updateCommand
5
- } from "./chunk-G6CEEMAR.js";
5
+ } from "./chunk-A427XMWE.js";
6
6
  import "./chunk-ORACWEDN.js";
7
7
  import "./chunk-POC2WHU2.js";
8
- import "./chunk-YO6JKGR3.js";
8
+ import "./chunk-VUAUAO2R.js";
9
9
  import "./chunk-C5LYVVEI.js";
10
10
  import "./chunk-CYQU33FY.js";
11
11
  import "./chunk-RWUTFVRB.js";
@@ -1,5 +1,10 @@
1
1
  # CF Memory Changelog
2
2
 
3
+ ## v0.1.3 (2026-03-19)
4
+
5
+ - Use path-based project IDs instead of SHA256 hashes for human-readable project directories (e.g. `-Users-thi-git-foo` instead of `a1b2c3d4e5f6`) [#9c4cac0](https://github.com/dinhanhthi/coding-friend/commit/9c4cac0)
6
+ - Rename `cf memory start`/`stop` to `cf memory start-daemon`/`stop-daemon` in documentation [#acbe789](https://github.com/dinhanhthi/coding-friend/commit/acbe789)
7
+
3
8
  ## v0.1.1 (2026-03-17)
4
9
 
5
10
  - Fix `today()` to capture full timestamp (`YYYY-MM-DD HH:MM`) instead of date-only for memory created/updated fields [#31e0824](https://github.com/dinhanhthi/coding-friend/commit/31e0824)
@@ -262,8 +262,8 @@ The `cf` CLI exposes memory commands that use this package:
262
262
  | `cf memory status` | Show current tier, daemon status, memory count |
263
263
  | `cf memory search <query>` | Search memories from the terminal |
264
264
  | `cf memory list` | List all stored memories |
265
- | `cf memory start` | Start the MiniSearch daemon (Tier 2) |
266
- | `cf memory stop` | Stop the daemon |
265
+ | `cf memory start-daemon` | Start the MiniSearch daemon (Tier 2) |
266
+ | `cf memory stop-daemon` | Stop the daemon |
267
267
  | `cf memory rebuild` | Rebuild search index (Tier 1 direct or via daemon) |
268
268
  | `cf memory init` | Install Tier 1 deps + import existing memories into SQLite |
269
269
  | `cf memory mcp` | Print MCP server config for use in Claude Desktop / other clients |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cf-memory",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { projectId } from "../backends/sqlite/index.js";
3
+
4
+ describe("projectId", () => {
5
+ it("strips /docs/memory suffix and encodes path", () => {
6
+ expect(projectId("/Users/thi/git/coding-friend/docs/memory")).toBe(
7
+ "-Users-thi-git-coding-friend",
8
+ );
9
+ });
10
+
11
+ it("strips /memory suffix when docsDir is custom", () => {
12
+ expect(projectId("/Users/thi/git/foo/memory")).toBe("-Users-thi-git-foo");
13
+ });
14
+
15
+ it("handles path without known suffix", () => {
16
+ expect(projectId("/Users/thi/git/foo/custom-docs")).toBe(
17
+ "-Users-thi-git-foo-custom-docs",
18
+ );
19
+ });
20
+
21
+ it("strips trailing slash before encoding", () => {
22
+ expect(projectId("/Users/thi/git/foo/docs/memory/")).toBe(
23
+ projectId("/Users/thi/git/foo/docs/memory"),
24
+ );
25
+ });
26
+
27
+ it("strips multiple trailing slashes", () => {
28
+ expect(projectId("/Users/thi/git/foo/docs/memory///")).toBe(
29
+ "-Users-thi-git-foo",
30
+ );
31
+ });
32
+
33
+ it("produces same result with and without trailing slash", () => {
34
+ const withSlash = projectId("/a/b/c/docs/memory/");
35
+ const withoutSlash = projectId("/a/b/c/docs/memory");
36
+ expect(withSlash).toBe(withoutSlash);
37
+ expect(withSlash).toBe("-a-b-c");
38
+ });
39
+
40
+ it("handles deeply nested paths", () => {
41
+ expect(projectId("/a/b/c/d/e/f/g")).toBe("-a-b-c-d-e-f-g");
42
+ });
43
+ });
@@ -7,11 +7,10 @@
7
7
  * Markdown files remain the source of truth. SQLite is a derived index
8
8
  * that can be rebuilt from markdown at any time.
9
9
  *
10
- * DB path: ~/.coding-friend/memory/projects/{12-char-sha256}/db.sqlite
10
+ * DB path: ~/.coding-friend/memory/projects/{encoded-path}/db.sqlite
11
11
  */
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
- import crypto from "node:crypto";
15
14
  import type { MemoryBackend } from "../../lib/backend.js";
16
15
  import { MarkdownBackend } from "../markdown.js";
17
16
  import {
@@ -46,14 +45,17 @@ import {
46
45
  import { hybridSearch } from "./search.js";
47
46
 
48
47
  /**
49
- * Compute a short hash of the docsDir path for project isolation.
48
+ * Derive the project root from a docsDir path by stripping known suffixes.
49
+ * Then encode it into a filesystem-safe directory name for project isolation.
50
+ * Uses the same encoding as Claude Code's project directories:
51
+ * resolve + strip known suffixes + strip trailing slashes + replace all "/" with "-"
52
+ * e.g. /Users/thi/git/foo/docs/memory → -Users-thi-git-foo
50
53
  */
51
- function projectHash(docsDir: string): string {
52
- return crypto
53
- .createHash("sha256")
54
- .update(path.resolve(docsDir))
55
- .digest("hex")
56
- .slice(0, 12);
54
+ export function projectId(docsDir: string): string {
55
+ let resolved = path.resolve(docsDir).replace(/\/+$/, "");
56
+ // Strip known memory directory suffixes to get the project root
57
+ resolved = resolved.replace(/\/docs\/memory$/, "").replace(/\/memory$/, "");
58
+ return resolved.replace(/\//g, "-");
57
59
  }
58
60
 
59
61
  export interface SqliteBackendOptions {
@@ -83,8 +85,8 @@ export class SqliteBackend implements MemoryBackend {
83
85
  this.dbPath = opts.dbPath;
84
86
  } else {
85
87
  const depsDir = opts?.depsDir ?? this.getDefaultDepsDir();
86
- const hash = projectHash(docsDir);
87
- const projectDir = path.join(depsDir, "projects", hash);
88
+ const id = projectId(docsDir);
89
+ const projectDir = path.join(depsDir, "projects", id);
88
90
  this.dbPath = path.join(projectDir, "db.sqlite");
89
91
  }
90
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.17.2",
3
+ "version": "1.18.0",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,198 +0,0 @@
1
- import {
2
- readJson,
3
- writeJson
4
- } from "./chunk-RWUTFVRB.js";
5
-
6
- // src/lib/permissions.ts
7
- var PERMISSION_RULES = [
8
- // Core (hooks & infrastructure)
9
- {
10
- rule: "Bash(cat:*)",
11
- description: "[read-only] Read file contents (\u26A0 system-wide scope, project boundary enforced by Claude Code) \xB7 Used by: session-init hook",
12
- category: "Core",
13
- recommended: true
14
- },
15
- {
16
- rule: "Bash(grep:*)",
17
- description: "[read-only] Search file contents (\u26A0 system-wide scope, project boundary enforced by Claude Code) \xB7 Used by: session-init hook, skills",
18
- category: "Core",
19
- recommended: true
20
- },
21
- {
22
- rule: "Bash(sed:*)",
23
- description: "[modify] Text transformation \xB7 Used by: session-init hook",
24
- category: "Core",
25
- recommended: true
26
- },
27
- {
28
- rule: "Bash(tr:*)",
29
- description: "[read-only] Character translation \xB7 Used by: session-init hook",
30
- category: "Core",
31
- recommended: true
32
- },
33
- {
34
- rule: "Bash(wc:*)",
35
- description: "[read-only] Count lines/words \xB7 Used by: cf-verification, skills",
36
- category: "Core",
37
- recommended: true
38
- },
39
- {
40
- rule: "Bash(mkdir:*)",
41
- description: "[write] Create directories \xB7 Used by: docs folder setup",
42
- category: "Core",
43
- recommended: true
44
- },
45
- // Git Operations
46
- {
47
- rule: "Bash(git add:*)",
48
- description: "[modify] Stage files for commit \xB7 Used by: /cf-commit, /cf-ship",
49
- category: "Git",
50
- recommended: true
51
- },
52
- {
53
- rule: "Bash(git commit:*)",
54
- description: "[modify] Create commits \xB7 Used by: /cf-commit, /cf-ship",
55
- category: "Git",
56
- recommended: true
57
- },
58
- {
59
- rule: "Bash(git status:*)",
60
- description: "[read-only] Check working tree status \xB7 Used by: /cf-commit, /cf-review, cf-verification",
61
- category: "Git",
62
- recommended: true
63
- },
64
- {
65
- rule: "Bash(git diff:*)",
66
- description: "[read-only] View file changes \xB7 Used by: /cf-commit, /cf-review, cf-verification",
67
- category: "Git",
68
- recommended: true
69
- },
70
- {
71
- rule: "Bash(git log:*)",
72
- description: "[read-only] View commit history \xB7 Used by: /cf-commit, /cf-review, cf-sys-debug",
73
- category: "Git",
74
- recommended: true
75
- },
76
- {
77
- rule: "Bash(git push:*)",
78
- description: "[remote] Push commits to remote \xB7 Used by: /cf-ship",
79
- category: "Git",
80
- recommended: true
81
- },
82
- {
83
- rule: "Bash(git pull:*)",
84
- description: "[remote] Pull changes from remote \xB7 Used by: /cf-ship",
85
- category: "Git",
86
- recommended: true
87
- },
88
- {
89
- rule: "Bash(gh pr create:*)",
90
- description: "[remote] Create GitHub pull requests \xB7 Used by: /cf-ship",
91
- category: "Git",
92
- recommended: true
93
- },
94
- // Testing & Build
95
- {
96
- rule: "Bash(npm test:*)",
97
- description: "[execute] Run test suites \xB7 Used by: cf-verification, /cf-fix, cf-tdd",
98
- category: "Testing & Build",
99
- recommended: true
100
- },
101
- {
102
- rule: "Bash(npm run:*)",
103
- description: "[execute] Run npm scripts (build, lint, format) \xB7 Used by: cf-verification",
104
- category: "Testing & Build",
105
- recommended: true
106
- },
107
- // Web & Research
108
- {
109
- rule: "WebSearch",
110
- description: "[network] Perform web searches \xB7 Used by: /cf-research",
111
- category: "Web & Research",
112
- recommended: false
113
- },
114
- {
115
- rule: "WebFetch(domain:*)",
116
- description: "[network] Fetch content from web pages \xB7 Used by: /cf-research",
117
- category: "Web & Research",
118
- recommended: false
119
- }
120
- ];
121
- function getExistingRules(settingsPath) {
122
- const settings = readJson(settingsPath);
123
- if (!settings) return [];
124
- const permissions = settings.permissions;
125
- return permissions?.allow ?? [];
126
- }
127
- function getMissingRules(existing, rules) {
128
- return rules.filter((r) => !existing.includes(r.rule));
129
- }
130
- function buildLearnDirRules(learnPath, autoCommit) {
131
- const rules = [
132
- {
133
- rule: `Read(${learnPath}/**)`,
134
- description: "[read-only] Read learning docs \xB7 Used by: /cf-learn",
135
- category: "External Learn Directory",
136
- recommended: true
137
- },
138
- {
139
- rule: `Edit(${learnPath}/**)`,
140
- description: "[modify] Edit learning docs \xB7 Used by: /cf-learn",
141
- category: "External Learn Directory",
142
- recommended: true
143
- },
144
- {
145
- rule: `Write(${learnPath}/**)`,
146
- description: "[write] Write learning docs \xB7 Used by: /cf-learn",
147
- category: "External Learn Directory",
148
- recommended: true
149
- }
150
- ];
151
- if (autoCommit) {
152
- const quoted = learnPath.includes(" ") ? `"${learnPath}"` : learnPath;
153
- rules.push({
154
- rule: `Bash(cd ${quoted} && git add:*)`,
155
- description: "[modify] Stage learning docs for commit \xB7 Used by: /cf-learn auto-commit",
156
- category: "External Learn Directory",
157
- recommended: true
158
- });
159
- rules.push({
160
- rule: `Bash(cd ${quoted} && git commit:*)`,
161
- description: "[modify] Commit learning docs \xB7 Used by: /cf-learn auto-commit",
162
- category: "External Learn Directory",
163
- recommended: true
164
- });
165
- }
166
- return rules;
167
- }
168
- function applyPermissions(settingsPath, toAdd, toRemove) {
169
- const settings = readJson(settingsPath) ?? {};
170
- const permissions = settings.permissions ?? {};
171
- const existing = permissions.allow ?? [];
172
- const afterRemove = existing.filter((r) => !toRemove.includes(r));
173
- const afterAdd = [
174
- ...afterRemove,
175
- ...toAdd.filter((r) => !afterRemove.includes(r))
176
- ];
177
- permissions.allow = afterAdd;
178
- settings.permissions = permissions;
179
- writeJson(settingsPath, settings);
180
- }
181
- function groupByCategory(rules) {
182
- const groups = /* @__PURE__ */ new Map();
183
- for (const rule of rules) {
184
- const list = groups.get(rule.category) ?? [];
185
- list.push(rule);
186
- groups.set(rule.category, list);
187
- }
188
- return groups;
189
- }
190
-
191
- export {
192
- PERMISSION_RULES,
193
- getExistingRules,
194
- getMissingRules,
195
- buildLearnDirRules,
196
- applyPermissions,
197
- groupByCategory
198
- };