coding-friend-cli 1.19.0 → 1.20.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/README.md +20 -0
- package/dist/{chunk-IRPW2BMP.js → chunk-PHQK2MMO.js} +1 -1
- package/dist/index.js +13 -13
- package/dist/{install-HLCVBOXO.js → install-35IWHBIS.js} +1 -1
- package/dist/{memory-BL37DXPU.js → memory-47RXG7VL.js} +17 -5
- package/dist/{status-V324NM64.js → status-SENJZQ3G.js} +15 -12
- package/dist/{update-EVOGWLKX.js → update-NZ2HRWEN.js} +1 -1
- package/lib/cf-memory/CHANGELOG.md +4 -0
- package/lib/cf-memory/README.md +29 -1
- package/lib/cf-memory/package.json +1 -1
- package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +41 -1
- package/lib/cf-memory/src/backends/markdown.ts +28 -12
- package/lib/cf-memory/src/lib/types.ts +1 -0
- package/lib/cf-memory/src/tools/store.ts +23 -5
- package/lib/learn-host/CHANGELOG.md +5 -0
- package/lib/learn-host/package.json +1 -1
- package/lib/learn-host/src/app/[category]/[slug]/page.tsx +4 -3
- package/lib/learn-host/src/components/Breadcrumbs.tsx +1 -1
- package/lib/learn-host/src/components/DocCard.tsx +4 -1
- package/lib/learn-host/src/components/Sidebar.tsx +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,26 @@ CLI companion for the [coding-friend](https://github.com/dinhanhthi/coding-frien
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js >= 18
|
|
8
|
+
- npm (included with Node.js, but on some Linux distros you may need to install it separately)
|
|
8
9
|
- The [coding-friend plugin](https://github.com/dinhanhthi/coding-friend) installed in Claude Code
|
|
9
10
|
|
|
11
|
+
### Additional requirements for `cf memory init`
|
|
12
|
+
|
|
13
|
+
The memory system's Tier 1 (SQLite) uses native Node.js modules that require compilation. On **Linux** (Ubuntu/Debian), install the following system packages before running `cf memory init`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
sudo apt update
|
|
17
|
+
sudo apt install -y build-essential python3
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
On **macOS**, install Xcode Command Line Tools:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
xcode-select --install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Without these, `cf memory init` will fail when installing SQLite dependencies (`better-sqlite3`, `sqlite-vec`). If you don't need Tier 1, you can choose the **lite** (Tier 2) or **markdown** (Tier 3) tier during init — these have no native dependencies.
|
|
27
|
+
|
|
10
28
|
## Install
|
|
11
29
|
|
|
12
30
|
```bash
|
|
@@ -55,6 +73,8 @@ cf permission --project # Save to project-level settings (.claude/settings.lo
|
|
|
55
73
|
cf permission --all --user # Apply all recommended permissions to user settings
|
|
56
74
|
cf statusline # Setup coding-friend statusline
|
|
57
75
|
cf update # Update plugin + CLI + statusline
|
|
76
|
+
# 💡 If update fails, open Claude Code (`claude`) and run:
|
|
77
|
+
# /plugins → Installed → coding-friend → Update now → /reload-plugins
|
|
58
78
|
cf update --cli # Update only the CLI (npm package)
|
|
59
79
|
cf update --plugin # Update only the Claude Code plugin
|
|
60
80
|
cf update --statusline # Update only the statusline
|
|
@@ -198,7 +198,7 @@ async function updateCommand(opts) {
|
|
|
198
198
|
log.dim(`stderr: ${result.stderr}`);
|
|
199
199
|
}
|
|
200
200
|
log.dim(
|
|
201
|
-
"Try manually:
|
|
201
|
+
"Try manually in Claude Code: /plugins \u2192 Installed \u2192 coding-friend \u2192 Update now \u2192 /reload-plugins"
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ 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-
|
|
17
|
+
const { installCommand } = await import("./install-35IWHBIS.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) => {
|
|
@@ -57,11 +57,11 @@ program.command("statusline").description("Setup coding-friend statusline in Cla
|
|
|
57
57
|
await statuslineCommand();
|
|
58
58
|
});
|
|
59
59
|
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) => {
|
|
60
|
-
const { updateCommand } = await import("./update-
|
|
60
|
+
const { updateCommand } = await import("./update-NZ2HRWEN.js");
|
|
61
61
|
await updateCommand(opts);
|
|
62
62
|
});
|
|
63
63
|
program.command("status").description("Show comprehensive Coding Friend status").action(async () => {
|
|
64
|
-
const { statusCommand } = await import("./status-
|
|
64
|
+
const { statusCommand } = await import("./status-SENJZQ3G.js");
|
|
65
65
|
await statusCommand();
|
|
66
66
|
});
|
|
67
67
|
var session = program.command("session").description("Save and load Claude Code sessions across machines");
|
|
@@ -100,43 +100,43 @@ Memory subcommands:
|
|
|
100
100
|
memory mcp Show MCP server setup instructions`
|
|
101
101
|
);
|
|
102
102
|
memory.command("status").description("Show memory system status").action(async () => {
|
|
103
|
-
const { memoryStatusCommand } = await import("./memory-
|
|
103
|
+
const { memoryStatusCommand } = await import("./memory-47RXG7VL.js");
|
|
104
104
|
await memoryStatusCommand();
|
|
105
105
|
});
|
|
106
106
|
memory.command("search").description("Search memories by query").argument("<query>", "search query").action(async (query) => {
|
|
107
|
-
const { memorySearchCommand } = await import("./memory-
|
|
107
|
+
const { memorySearchCommand } = await import("./memory-47RXG7VL.js");
|
|
108
108
|
await memorySearchCommand(query);
|
|
109
109
|
});
|
|
110
110
|
memory.command("list").description(
|
|
111
111
|
"List memories in current project, or all projects with --projects"
|
|
112
112
|
).option("--projects", "List all project databases with size and metadata").action(async (opts) => {
|
|
113
|
-
const { memoryListCommand } = await import("./memory-
|
|
113
|
+
const { memoryListCommand } = await import("./memory-47RXG7VL.js");
|
|
114
114
|
await memoryListCommand(opts);
|
|
115
115
|
});
|
|
116
116
|
memory.command("init").description(
|
|
117
117
|
"Initialize memory system \u2014 interactive wizard (first time) or config menu"
|
|
118
118
|
).action(async () => {
|
|
119
|
-
const { memoryInitCommand } = await import("./memory-
|
|
119
|
+
const { memoryInitCommand } = await import("./memory-47RXG7VL.js");
|
|
120
120
|
await memoryInitCommand();
|
|
121
121
|
});
|
|
122
122
|
memory.command("config").description("Configure memory system settings").action(async () => {
|
|
123
|
-
const { memoryConfigCommand } = await import("./memory-
|
|
123
|
+
const { memoryConfigCommand } = await import("./memory-47RXG7VL.js");
|
|
124
124
|
await memoryConfigCommand();
|
|
125
125
|
});
|
|
126
126
|
memory.command("start-daemon").description("Start the memory daemon (Tier 2 \u2014 MiniSearch)").action(async () => {
|
|
127
|
-
const { memoryStartDaemonCommand } = await import("./memory-
|
|
127
|
+
const { memoryStartDaemonCommand } = await import("./memory-47RXG7VL.js");
|
|
128
128
|
await memoryStartDaemonCommand();
|
|
129
129
|
});
|
|
130
130
|
memory.command("stop-daemon").description("Stop the memory daemon").action(async () => {
|
|
131
|
-
const { memoryStopDaemonCommand } = await import("./memory-
|
|
131
|
+
const { memoryStopDaemonCommand } = await import("./memory-47RXG7VL.js");
|
|
132
132
|
await memoryStopDaemonCommand();
|
|
133
133
|
});
|
|
134
134
|
memory.command("rebuild").description("Rebuild the daemon search index").action(async () => {
|
|
135
|
-
const { memoryRebuildCommand } = await import("./memory-
|
|
135
|
+
const { memoryRebuildCommand } = await import("./memory-47RXG7VL.js");
|
|
136
136
|
await memoryRebuildCommand();
|
|
137
137
|
});
|
|
138
138
|
memory.command("mcp").description("Show MCP server setup instructions").action(async () => {
|
|
139
|
-
const { memoryMcpCommand } = await import("./memory-
|
|
139
|
+
const { memoryMcpCommand } = await import("./memory-47RXG7VL.js");
|
|
140
140
|
await memoryMcpCommand();
|
|
141
141
|
});
|
|
142
142
|
memory.command("rm").description("Remove a project database").option("--project-id <id>", "Project ID to remove").option("--all", "Remove all project databases").option(
|
|
@@ -144,7 +144,7 @@ memory.command("rm").description("Remove a project database").option("--project-
|
|
|
144
144
|
"Remove orphaned projects (source dir missing or 0 memories)"
|
|
145
145
|
).action(
|
|
146
146
|
async (opts) => {
|
|
147
|
-
const { memoryRmCommand } = await import("./memory-
|
|
147
|
+
const { memoryRmCommand } = await import("./memory-47RXG7VL.js");
|
|
148
148
|
await memoryRmCommand(opts);
|
|
149
149
|
}
|
|
150
150
|
);
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
showConfigHint
|
|
19
19
|
} from "./chunk-C5LYVVEI.js";
|
|
20
20
|
import {
|
|
21
|
-
run
|
|
21
|
+
run,
|
|
22
|
+
runWithStderr
|
|
22
23
|
} from "./chunk-CYQU33FY.js";
|
|
23
24
|
import {
|
|
24
25
|
globalConfigPath,
|
|
@@ -50,21 +51,32 @@ function countMdFiles(dir) {
|
|
|
50
51
|
function getMemoryDir(path) {
|
|
51
52
|
return resolveMemoryDir(path);
|
|
52
53
|
}
|
|
54
|
+
var MAX_ERROR_LINES = 30;
|
|
55
|
+
function truncateError(text) {
|
|
56
|
+
const lines = text.split("\n");
|
|
57
|
+
if (lines.length <= MAX_ERROR_LINES) return text;
|
|
58
|
+
const head = lines.slice(0, 20);
|
|
59
|
+
const tail = lines.slice(-8);
|
|
60
|
+
const skipped = lines.length - 28;
|
|
61
|
+
return [...head, ` ... (${skipped} lines omitted) ...`, ...tail].join("\n");
|
|
62
|
+
}
|
|
53
63
|
function ensureBuilt(mcpDir) {
|
|
54
64
|
if (!existsSync(join(mcpDir, "node_modules"))) {
|
|
55
65
|
log.step("Installing memory server dependencies (one-time setup)...");
|
|
56
|
-
const result =
|
|
57
|
-
if (result
|
|
66
|
+
const result = runWithStderr("npm", ["install"], { cwd: mcpDir });
|
|
67
|
+
if (result.exitCode !== 0) {
|
|
58
68
|
log.error("Failed to install dependencies");
|
|
69
|
+
if (result.stderr) log.error(truncateError(result.stderr));
|
|
59
70
|
process.exit(1);
|
|
60
71
|
}
|
|
61
72
|
log.success("Done.");
|
|
62
73
|
}
|
|
63
74
|
if (!existsSync(join(mcpDir, "dist"))) {
|
|
64
75
|
log.step("Building memory server...");
|
|
65
|
-
const result =
|
|
66
|
-
if (result
|
|
76
|
+
const result = runWithStderr("npm", ["run", "build"], { cwd: mcpDir });
|
|
77
|
+
if (result.exitCode !== 0) {
|
|
67
78
|
log.error("Failed to build memory server");
|
|
79
|
+
if (result.stderr) log.error(truncateError(result.stderr));
|
|
68
80
|
process.exit(1);
|
|
69
81
|
}
|
|
70
82
|
log.success("Done.");
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getLatestCliVersion,
|
|
7
7
|
getLatestVersion,
|
|
8
8
|
semverCompare
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-PHQK2MMO.js";
|
|
10
10
|
import {
|
|
11
11
|
detectPluginScope,
|
|
12
12
|
isPluginDisabled
|
|
@@ -63,19 +63,21 @@ function formatScalar(value) {
|
|
|
63
63
|
if (typeof value === "number") return String(value);
|
|
64
64
|
return String(value);
|
|
65
65
|
}
|
|
66
|
-
function pad(label, width) {
|
|
67
|
-
|
|
66
|
+
function pad(label, width, color) {
|
|
67
|
+
const displayed = color ? color(label) : label;
|
|
68
|
+
return ` ${displayed}${" ".repeat(Math.max(1, width - label.length))}`;
|
|
68
69
|
}
|
|
69
|
-
function subLine(key, value, overrides) {
|
|
70
|
+
function subLine(key, value, overrides, color) {
|
|
71
|
+
const displayed = color ? color(key) : key;
|
|
70
72
|
console.log(
|
|
71
|
-
` ${
|
|
73
|
+
` ${displayed}${" ".repeat(Math.max(1, CONFIG_SUB_COL - key.length))}${value}${overrides}`
|
|
72
74
|
);
|
|
73
75
|
}
|
|
74
76
|
function printConfig(obj, otherConfig) {
|
|
75
77
|
for (const [key, value] of Object.entries(obj)) {
|
|
76
78
|
const overrides = otherConfig && key in otherConfig ? ` ${chalk.yellow("(overrides global)")}` : "";
|
|
77
79
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
78
|
-
console.log(pad(key, CONFIG_KEY_COL) + chalk.dim("\u2500") + overrides);
|
|
80
|
+
console.log(pad(key, CONFIG_KEY_COL, chalk.cyan) + chalk.dim("\u2500") + overrides);
|
|
79
81
|
const nested = value;
|
|
80
82
|
const nestedOther = otherConfig && typeof otherConfig[key] === "object" ? otherConfig[key] : null;
|
|
81
83
|
for (const [subKey, subVal] of Object.entries(nested)) {
|
|
@@ -84,7 +86,7 @@ function printConfig(obj, otherConfig) {
|
|
|
84
86
|
const names = subVal.map(
|
|
85
87
|
(c) => typeof c === "object" && c !== null && "name" in c ? c.name : String(c)
|
|
86
88
|
).join(", ");
|
|
87
|
-
subLine(subKey, names, subOverrides);
|
|
89
|
+
subLine(subKey, names, subOverrides, chalk.cyan);
|
|
88
90
|
continue;
|
|
89
91
|
}
|
|
90
92
|
if (typeof subVal === "object" && subVal !== null && !Array.isArray(subVal)) {
|
|
@@ -92,29 +94,30 @@ function printConfig(obj, otherConfig) {
|
|
|
92
94
|
subVal
|
|
93
95
|
);
|
|
94
96
|
const inline = innerEntries.map(([k, v]) => `${k}: ${formatScalar(v)}`).join(", ");
|
|
95
|
-
subLine(subKey, inline, subOverrides);
|
|
97
|
+
subLine(subKey, inline, subOverrides, chalk.cyan);
|
|
96
98
|
continue;
|
|
97
99
|
}
|
|
98
100
|
if (Array.isArray(subVal)) {
|
|
99
101
|
subLine(
|
|
100
102
|
subKey,
|
|
101
103
|
subVal.map((v) => formatScalar(v)).join(", "),
|
|
102
|
-
subOverrides
|
|
104
|
+
subOverrides,
|
|
105
|
+
chalk.cyan
|
|
103
106
|
);
|
|
104
107
|
continue;
|
|
105
108
|
}
|
|
106
|
-
subLine(subKey, formatScalar(subVal), subOverrides);
|
|
109
|
+
subLine(subKey, formatScalar(subVal), subOverrides, chalk.cyan);
|
|
107
110
|
}
|
|
108
111
|
continue;
|
|
109
112
|
}
|
|
110
113
|
if (Array.isArray(value)) {
|
|
111
114
|
console.log(
|
|
112
|
-
`${pad(key, CONFIG_KEY_COL)}${value.map((v) => formatScalar(v)).join(", ")}${overrides}`
|
|
115
|
+
`${pad(key, CONFIG_KEY_COL, chalk.cyan)}${value.map((v) => formatScalar(v)).join(", ")}${overrides}`
|
|
113
116
|
);
|
|
114
117
|
continue;
|
|
115
118
|
}
|
|
116
119
|
console.log(
|
|
117
|
-
`${pad(key, CONFIG_KEY_COL)}${formatScalar(value)}${overrides}`
|
|
120
|
+
`${pad(key, CONFIG_KEY_COL, chalk.cyan)}${formatScalar(value)}${overrides}`
|
|
118
121
|
);
|
|
119
122
|
}
|
|
120
123
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# CF Memory Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.0 (2026-03-21)
|
|
4
|
+
|
|
5
|
+
- Add `index_only` option to `memory_store` MCP tool — skip file writing when file already exists on disk, enabling clean separation between file creation and indexing [#7f56711](https://github.com/dinhanhthi/coding-friend/commit/7f56711)
|
|
6
|
+
|
|
3
7
|
## v0.1.3 (2026-03-19)
|
|
4
8
|
|
|
5
9
|
- 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)
|
package/lib/cf-memory/README.md
CHANGED
|
@@ -265,9 +265,37 @@ The `cf` CLI exposes memory commands that use this package:
|
|
|
265
265
|
| `cf memory start-daemon` | Start the MiniSearch daemon (Tier 2) |
|
|
266
266
|
| `cf memory stop-daemon` | Stop the daemon |
|
|
267
267
|
| `cf memory rebuild` | Rebuild search index (Tier 1 direct or via daemon) |
|
|
268
|
-
| `cf memory init` | Install Tier 1 deps + import existing memories into SQLite
|
|
268
|
+
| `cf memory init` | Install Tier 1 deps + import existing memories into SQLite (see [prerequisites](#prerequisites-for-tier-1-on-linux)) |
|
|
269
269
|
| `cf memory mcp` | Print MCP server config for use in Claude Desktop / other clients |
|
|
270
270
|
|
|
271
|
+
## Prerequisites for Tier 1 on Linux
|
|
272
|
+
|
|
273
|
+
Tier 1 uses `better-sqlite3` and `sqlite-vec`, which are native Node.js modules requiring C++ compilation. On a fresh Linux install, you need build tools before running `cf memory init`:
|
|
274
|
+
|
|
275
|
+
**Ubuntu/Debian:**
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
sudo apt update
|
|
279
|
+
sudo apt install -y build-essential python3
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Fedora/RHEL:**
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
sudo dnf groupinstall "Development Tools"
|
|
286
|
+
sudo dnf install python3
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Arch Linux:**
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
sudo pacman -S base-devel python
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
If these are missing, `cf memory init` will fail at the "Installing SQLite dependencies" step. You can still use Tier 2 (lite) or Tier 3 (markdown) without native dependencies — choose them during the init wizard.
|
|
296
|
+
|
|
297
|
+
**macOS** users need Xcode Command Line Tools: `xcode-select --install`.
|
|
298
|
+
|
|
271
299
|
## Environment Variables
|
|
272
300
|
|
|
273
301
|
| Variable | Default | Description |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdirSync, rmSync, existsSync, readFileSync } from "fs";
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
5
|
import matter from "gray-matter";
|
|
@@ -85,6 +85,46 @@ describe("MarkdownBackend", () => {
|
|
|
85
85
|
expect(m2.slug).toContain("api-authentication-pattern-");
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it("index_only=true returns Memory without writing when file exists", async () => {
|
|
89
|
+
// Pre-create the file via normal store
|
|
90
|
+
const original = await backend.store(sampleInput);
|
|
91
|
+
const filePath = join(
|
|
92
|
+
testDir,
|
|
93
|
+
"features",
|
|
94
|
+
"api-authentication-pattern.md",
|
|
95
|
+
);
|
|
96
|
+
expect(existsSync(filePath)).toBe(true);
|
|
97
|
+
|
|
98
|
+
// Snapshot file content before index_only call
|
|
99
|
+
const contentBefore = readFileSync(filePath, "utf-8");
|
|
100
|
+
|
|
101
|
+
// Now store with index_only — should NOT create a second file
|
|
102
|
+
const indexed = await backend.store({ ...sampleInput, index_only: true });
|
|
103
|
+
|
|
104
|
+
// Should return clean slug (no timestamp suffix)
|
|
105
|
+
expect(indexed.slug).toBe("api-authentication-pattern");
|
|
106
|
+
expect(indexed.id).toBe("features/api-authentication-pattern");
|
|
107
|
+
expect(indexed.category).toBe("features");
|
|
108
|
+
expect(indexed.frontmatter.title).toBe(sampleInput.title);
|
|
109
|
+
expect(indexed.frontmatter.type).toBe(sampleInput.type);
|
|
110
|
+
expect(indexed.content).toBe(sampleInput.content);
|
|
111
|
+
|
|
112
|
+
// Verify no duplicate file was created (only one .md file in features/)
|
|
113
|
+
const files = readdirSync(join(testDir, "features")).filter((f: string) =>
|
|
114
|
+
f.endsWith(".md"),
|
|
115
|
+
);
|
|
116
|
+
expect(files.length).toBe(1);
|
|
117
|
+
|
|
118
|
+
// Verify existing file was not modified
|
|
119
|
+
expect(readFileSync(filePath, "utf-8")).toBe(contentBefore);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("index_only=true throws when file does not exist", async () => {
|
|
123
|
+
await expect(
|
|
124
|
+
backend.store({ ...sampleInput, index_only: true }),
|
|
125
|
+
).rejects.toThrow(/index_only.*file not found/i);
|
|
126
|
+
});
|
|
127
|
+
|
|
88
128
|
it("respects custom importance and source", async () => {
|
|
89
129
|
const memory = await backend.store({
|
|
90
130
|
...sampleInput,
|
|
@@ -112,18 +112,11 @@ export class MarkdownBackend implements MemoryBackend {
|
|
|
112
112
|
fs.mkdirSync(catDir, { recursive: true });
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
const slug = slugify(input.title);
|
|
116
116
|
const filePath = path.join(catDir, `${slug}.md`);
|
|
117
|
-
|
|
118
|
-
// Handle duplicate slugs
|
|
119
|
-
if (fs.existsSync(filePath)) {
|
|
120
|
-
slug = `${slug}-${Date.now()}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const finalPath = path.join(catDir, `${slug}.md`);
|
|
124
117
|
const now = today();
|
|
125
118
|
|
|
126
|
-
const
|
|
119
|
+
const buildFrontmatter = (): MemoryFrontmatter => ({
|
|
127
120
|
title: input.title,
|
|
128
121
|
description: input.description,
|
|
129
122
|
type: input.type,
|
|
@@ -132,7 +125,30 @@ export class MarkdownBackend implements MemoryBackend {
|
|
|
132
125
|
created: now,
|
|
133
126
|
updated: now,
|
|
134
127
|
source: input.source ?? "conversation",
|
|
135
|
-
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// index_only: verify file exists, return Memory from input without writing
|
|
131
|
+
if (input.index_only) {
|
|
132
|
+
if (!fs.existsSync(filePath)) {
|
|
133
|
+
throw new Error(`index_only: file not found for "${category}/${slug}"`);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
id: `${category}/${slug}`,
|
|
137
|
+
slug,
|
|
138
|
+
category,
|
|
139
|
+
frontmatter: buildFrontmatter(),
|
|
140
|
+
content: input.content,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle duplicate slugs
|
|
145
|
+
let finalSlug = slug;
|
|
146
|
+
if (fs.existsSync(filePath)) {
|
|
147
|
+
finalSlug = `${slug}-${Date.now()}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const finalPath = path.join(catDir, `${finalSlug}.md`);
|
|
151
|
+
const frontmatter = buildFrontmatter();
|
|
136
152
|
|
|
137
153
|
const doc = matter.stringify(
|
|
138
154
|
input.content,
|
|
@@ -141,8 +157,8 @@ export class MarkdownBackend implements MemoryBackend {
|
|
|
141
157
|
fs.writeFileSync(finalPath, doc, "utf-8");
|
|
142
158
|
|
|
143
159
|
return {
|
|
144
|
-
id: `${category}/${
|
|
145
|
-
slug,
|
|
160
|
+
id: `${category}/${finalSlug}`,
|
|
161
|
+
slug: finalSlug,
|
|
146
162
|
category,
|
|
147
163
|
frontmatter,
|
|
148
164
|
content: input.content,
|
|
@@ -26,8 +26,23 @@ export function registerStore(server: McpServer, backend: MemoryBackend): void {
|
|
|
26
26
|
.string()
|
|
27
27
|
.optional()
|
|
28
28
|
.describe("Source: conversation, auto-capture, manual"),
|
|
29
|
+
index_only: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
"When true, skip file creation and return a Memory object for indexing. File must already exist on disk.",
|
|
34
|
+
),
|
|
29
35
|
},
|
|
30
|
-
async ({
|
|
36
|
+
async ({
|
|
37
|
+
title,
|
|
38
|
+
description,
|
|
39
|
+
type,
|
|
40
|
+
tags,
|
|
41
|
+
content,
|
|
42
|
+
importance,
|
|
43
|
+
source,
|
|
44
|
+
index_only,
|
|
45
|
+
}) => {
|
|
31
46
|
const input = {
|
|
32
47
|
title,
|
|
33
48
|
description,
|
|
@@ -36,13 +51,16 @@ export function registerStore(server: McpServer, backend: MemoryBackend): void {
|
|
|
36
51
|
content,
|
|
37
52
|
importance,
|
|
38
53
|
source,
|
|
54
|
+
index_only,
|
|
39
55
|
};
|
|
40
56
|
|
|
41
57
|
let dedup: Awaited<ReturnType<typeof checkDuplicate>> | null = null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
if (!index_only) {
|
|
59
|
+
try {
|
|
60
|
+
dedup = await checkDuplicate(backend, input);
|
|
61
|
+
} catch {
|
|
62
|
+
// Dedup is best-effort — don't block store on search errors
|
|
63
|
+
}
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
const memory = await backend.store(input);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog (Learn Host)
|
|
2
2
|
|
|
3
|
+
## v0.3.0 (2026-03-21)
|
|
4
|
+
|
|
5
|
+
- Improve date display — show only updated date when different from created date, hide if same [#1fc2e65](https://github.com/dinhanhthi/coding-friend/commit/1fc2e65)
|
|
6
|
+
- Increase sidebar font size for better readability [#1fc2e65](https://github.com/dinhanhthi/coding-friend/commit/1fc2e65)
|
|
7
|
+
|
|
3
8
|
## v0.2.1 (2026-03-05)
|
|
4
9
|
|
|
5
10
|
- Fix TOC heading text stripping markdown links from slug generation ([#9a8fb5c](https://github.com/dinhanhthi/coding-friend/commit/9a8fb5c))
|
|
@@ -45,9 +45,10 @@ export default async function DocPage({
|
|
|
45
45
|
{doc.frontmatter.created && (
|
|
46
46
|
<span>Created: {doc.frontmatter.created}</span>
|
|
47
47
|
)}
|
|
48
|
-
{doc.frontmatter.updated &&
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
{doc.frontmatter.updated &&
|
|
49
|
+
doc.frontmatter.updated !== doc.frontmatter.created && (
|
|
50
|
+
<span>Updated: {doc.frontmatter.updated}</span>
|
|
51
|
+
)}
|
|
51
52
|
</div>
|
|
52
53
|
)}
|
|
53
54
|
{doc.frontmatter.tags.length > 0 && (
|
|
@@ -43,7 +43,10 @@ export default function DocCard({ doc }: { doc: DocMeta }) {
|
|
|
43
43
|
)}
|
|
44
44
|
</div>
|
|
45
45
|
<span className="text-xs text-slate-400">
|
|
46
|
-
{doc.frontmatter.updated
|
|
46
|
+
{doc.frontmatter.updated &&
|
|
47
|
+
doc.frontmatter.updated !== doc.frontmatter.created
|
|
48
|
+
? doc.frontmatter.updated
|
|
49
|
+
: doc.frontmatter.created}
|
|
47
50
|
</span>
|
|
48
51
|
</div>
|
|
49
52
|
</div>
|
|
@@ -23,7 +23,7 @@ export default function Sidebar({
|
|
|
23
23
|
<Link
|
|
24
24
|
key={cat.name}
|
|
25
25
|
href={`/${cat.name}/`}
|
|
26
|
-
className={`flex items-center justify-between rounded-full py-1.5 pr-2 pl-4 text-
|
|
26
|
+
className={`flex items-center justify-between rounded-full py-1.5 pr-2 pl-4 text-[0.938rem] capitalize transition-colors duration-200 ${
|
|
27
27
|
isActive
|
|
28
28
|
? "font-medium text-amber-700 dark:text-amber-400"
|
|
29
29
|
: "dark:hover:bg-navy-800/70 text-slate-600 hover:bg-slate-200/50 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|