remnote-bridge 0.1.3 → 0.1.5

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.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * clean 命令
3
+ *
4
+ * 清理 remnote-bridge 在本机产生的所有残留文件:
5
+ * - 项目根:.remnote-bridge.pid / .remnote-bridge.log / .remnote-bridge.json
6
+ * - 用户目录:~/.claude/skills/remnote-bridge/
7
+ *
8
+ * 如果守护进程仍在运行,先停止再清理。
9
+ */
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import { findProjectRoot, pidFilePath, logFilePath, configFilePath } from '../config.js';
14
+ import { checkDaemon } from '../daemon/pid.js';
15
+ import { jsonOutput } from '../utils/output.js';
16
+ export async function cleanCommand(options = {}) {
17
+ const { json } = options;
18
+ const projectRoot = findProjectRoot();
19
+ const removed = [];
20
+ const errors = [];
21
+ // 1. 如果守护进程在运行,先停止
22
+ const pidPath = pidFilePath(projectRoot);
23
+ const status = checkDaemon(pidPath);
24
+ if (status.running) {
25
+ if (!json) {
26
+ console.log(`守护进程运行中(PID: ${status.pid}),正在停止...`);
27
+ }
28
+ try {
29
+ process.kill(status.pid, 'SIGTERM');
30
+ // 等待最多 5 秒
31
+ const start = Date.now();
32
+ while (Date.now() - start < 5000) {
33
+ try {
34
+ process.kill(status.pid, 0);
35
+ await new Promise(r => setTimeout(r, 200));
36
+ }
37
+ catch {
38
+ break; // 进程已退出
39
+ }
40
+ }
41
+ // 如果还没退出,强制终止
42
+ try {
43
+ process.kill(status.pid, 0);
44
+ process.kill(status.pid, 'SIGKILL');
45
+ }
46
+ catch {
47
+ // 已退出
48
+ }
49
+ }
50
+ catch {
51
+ // 进程可能已退出
52
+ }
53
+ }
54
+ // 2. 清理项目根下的文件
55
+ const projectFiles = [
56
+ pidFilePath(projectRoot),
57
+ logFilePath(projectRoot),
58
+ configFilePath(projectRoot),
59
+ ];
60
+ for (const filePath of projectFiles) {
61
+ if (fs.existsSync(filePath)) {
62
+ try {
63
+ fs.unlinkSync(filePath);
64
+ removed.push(filePath);
65
+ if (!json) {
66
+ console.log(` 已删除: ${filePath}`);
67
+ }
68
+ }
69
+ catch (err) {
70
+ errors.push(`${filePath}: ${err.message}`);
71
+ if (!json) {
72
+ console.error(` 删除失败: ${filePath} — ${err.message}`);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ // 3. 清理 ~/.claude/skills/remnote-bridge/
78
+ const skillDir = path.join(os.homedir(), '.claude', 'skills', 'remnote-bridge');
79
+ if (fs.existsSync(skillDir)) {
80
+ try {
81
+ fs.rmSync(skillDir, { recursive: true, force: true });
82
+ removed.push(skillDir);
83
+ if (!json) {
84
+ console.log(` 已删除: ${skillDir}`);
85
+ }
86
+ }
87
+ catch (err) {
88
+ errors.push(`${skillDir}: ${err.message}`);
89
+ if (!json) {
90
+ console.error(` 删除失败: ${skillDir} — ${err.message}`);
91
+ }
92
+ }
93
+ }
94
+ // 4. 输出结果
95
+ if (json) {
96
+ jsonOutput({
97
+ ok: errors.length === 0,
98
+ command: 'clean',
99
+ removed,
100
+ ...(errors.length > 0 ? { errors } : {}),
101
+ });
102
+ }
103
+ else {
104
+ if (removed.length === 0 && errors.length === 0) {
105
+ console.log('没有发现需要清理的文件');
106
+ }
107
+ else if (errors.length === 0) {
108
+ console.log(`\n清理完成,共删除 ${removed.length} 项`);
109
+ }
110
+ else {
111
+ console.log(`\n清理完成,删除 ${removed.length} 项,${errors.length} 项失败`);
112
+ }
113
+ }
114
+ }
@@ -2,6 +2,7 @@
2
2
  * webpack-dev-server 子进程管理
3
3
  *
4
4
  * 在 remnote-plugin 目录下启动 npm run dev。
5
+ * 具备崩溃重试和依赖自动修复能力。
5
6
  */
6
7
  import { spawn, execSync } from 'child_process';
7
8
  import path from 'path';
@@ -9,29 +10,60 @@ import fs from 'fs';
9
10
  export class DevServerManager {
10
11
  child = null;
11
12
  options;
13
+ retryCount = 0;
14
+ maxRetries;
15
+ stopping = false;
12
16
  constructor(options) {
13
17
  this.options = options;
18
+ this.maxRetries = options.maxRetries ?? 2;
14
19
  }
15
20
  /**
16
21
  * 启动 webpack-dev-server。
17
22
  * 如果 remnote-plugin 目录不存在,抛出错误。
18
23
  */
19
24
  start() {
20
- const { pluginDir, port, onLog, onExit } = this.options;
25
+ const { pluginDir, port, onLog } = this.options;
21
26
  if (!fs.existsSync(path.join(pluginDir, 'package.json'))) {
22
27
  throw new Error(`Plugin 目录不存在或缺少 package.json: ${pluginDir}`);
23
28
  }
24
- // 首次使用时自动安装 plugin 依赖
25
- if (!fs.existsSync(path.join(pluginDir, 'node_modules'))) {
26
- onLog?.('[dev-server] remnote-plugin/node_modules 不存在,正在安装依赖...', 'info');
29
+ this.ensureDependencies(false);
30
+ this.spawnDevServer();
31
+ }
32
+ /**
33
+ * 确保依赖完整。
34
+ * @param cleanInstall true = 删除 node_modules 后重装(修复损坏)
35
+ */
36
+ ensureDependencies(cleanInstall) {
37
+ const { pluginDir, onLog } = this.options;
38
+ const nodeModulesDir = path.join(pluginDir, 'node_modules');
39
+ const hasCompleteDeps = fs.existsSync(nodeModulesDir) &&
40
+ fs.existsSync(path.join(nodeModulesDir, '.package-lock.json'));
41
+ if (cleanInstall && fs.existsSync(nodeModulesDir)) {
42
+ onLog?.('[dev-server] 检测到依赖损坏,正在清洁重装...', 'warn');
43
+ // 删除 node_modules 和 package-lock.json 以彻底修复
44
+ fs.rmSync(nodeModulesDir, { recursive: true, force: true });
45
+ const lockFile = path.join(pluginDir, 'package-lock.json');
46
+ if (fs.existsSync(lockFile)) {
47
+ fs.unlinkSync(lockFile);
48
+ }
49
+ }
50
+ if (cleanInstall || !hasCompleteDeps) {
51
+ onLog?.('[dev-server] remnote-plugin 依赖缺失或不完整,正在安装...', 'info');
27
52
  execSync('npm install', { cwd: pluginDir, stdio: 'pipe' });
28
53
  onLog?.('[dev-server] 依赖安装完成', 'info');
29
54
  }
30
- // 通过环境变量传递端口
55
+ }
56
+ /**
57
+ * 启动 dev-server 子进程并挂载事件监听。
58
+ */
59
+ spawnDevServer() {
60
+ const { pluginDir, port, onLog, onExit } = this.options;
61
+ // shell: true 确保 Windows 上能找到 npm.cmd
31
62
  this.child = spawn('npm', ['run', 'dev'], {
32
63
  cwd: pluginDir,
33
64
  env: { ...process.env, PORT: String(port) },
34
65
  stdio: 'pipe',
66
+ shell: true,
35
67
  });
36
68
  this.child.stdout?.on('data', (data) => {
37
69
  onLog?.(`[dev-server] ${data.toString().trim()}`, 'info');
@@ -40,9 +72,35 @@ export class DevServerManager {
40
72
  onLog?.(`[dev-server] ${data.toString().trim()}`, 'warn');
41
73
  });
42
74
  this.child.on('exit', (code) => {
43
- onLog?.(`webpack-dev-server 退出 (code: ${code})`, code === 0 ? 'info' : 'error');
44
75
  this.child = null;
45
- onExit?.(code);
76
+ // 正常退出或正在停止中,不重试
77
+ if (code === 0 || this.stopping) {
78
+ onLog?.(`webpack-dev-server 退出 (code: ${code})`, 'info');
79
+ onExit?.(code);
80
+ return;
81
+ }
82
+ onLog?.(`webpack-dev-server 异常退出 (code: ${code})`, 'error');
83
+ // 尝试重试
84
+ if (this.retryCount < this.maxRetries) {
85
+ this.retryCount++;
86
+ const isCleanRetry = this.retryCount === 1;
87
+ onLog?.(`[dev-server] 第 ${this.retryCount}/${this.maxRetries} 次重试` +
88
+ (isCleanRetry ? '(清洁重装依赖)' : '') + '...', 'warn');
89
+ try {
90
+ // 第一次重试:清洁重装依赖(修复损坏的 node_modules)
91
+ // 后续重试:直接重启
92
+ this.ensureDependencies(isCleanRetry);
93
+ this.spawnDevServer();
94
+ }
95
+ catch (err) {
96
+ onLog?.(`[dev-server] 重试失败: ${err.message}`, 'error');
97
+ onExit?.(code);
98
+ }
99
+ }
100
+ else {
101
+ onLog?.(`[dev-server] 已达最大重试次数 (${this.maxRetries}),放弃`, 'error');
102
+ onExit?.(code);
103
+ }
46
104
  });
47
105
  this.child.on('error', (err) => {
48
106
  onLog?.(`webpack-dev-server 启动失败: ${err.message}`, 'error');
@@ -54,6 +112,7 @@ export class DevServerManager {
54
112
  * 停止 webpack-dev-server。
55
113
  */
56
114
  stop() {
115
+ this.stopping = true;
57
116
  return new Promise((resolve) => {
58
117
  if (!this.child) {
59
118
  resolve();
package/dist/cli/main.js CHANGED
@@ -16,6 +16,7 @@ import { readGlobeCommand } from './commands/read-globe.js';
16
16
  import { readContextCommand } from './commands/read-context.js';
17
17
  import { searchCommand } from './commands/search.js';
18
18
  import { installSkillCommand, installSkillCopyCommand } from './commands/install-skill.js';
19
+ import { cleanCommand } from './commands/clean.js';
19
20
  const program = new Command();
20
21
  /**
21
22
  * --json 模式下解析 JSON 输入参数。
@@ -53,7 +54,7 @@ function parseJsonInput(command, jsonStr, requiredFields = []) {
53
54
  program
54
55
  .name('remnote-bridge')
55
56
  .description('RemNote Bridge — CLI + MCP Server + Plugin')
56
- .version('0.1.3')
57
+ .version('0.1.5')
57
58
  .option('--json', '以 JSON 格式输出(适用于程序化调用)');
58
59
  program
59
60
  .command('connect')
@@ -305,4 +306,11 @@ installCmd.command('skill')
305
306
  await installSkillCommand();
306
307
  }
307
308
  });
309
+ program
310
+ .command('clean')
311
+ .description('清理所有残留文件(.pid / .log / .json / skill 目录)')
312
+ .action(async () => {
313
+ const { json } = program.opts();
314
+ await cleanCommand({ json });
315
+ });
308
316
  program.parse();
@@ -130,10 +130,18 @@ disconnect → 关闭 daemon,清空所有缓存
130
130
 
131
131
  **注意**:中文等无空格语言搜索效果较差。如果完整词搜索无结果,尝试用单个最具区分度的字搜索;如果仍然无果,改用 \\\`read_globe\\\` → \\\`read_tree\\\` 手动定位。
132
132
 
133
- ### 场景 C:了解当前上下文
133
+ ### 场景 C:了解当前上下文(⚠️ 高优先级)
134
134
 
135
135
  > 用户说:"我现在在看什么"、"当前页面是什么"
136
136
 
137
+ **重要:用户正在看的页面对你来说是不可见的。** 用户在跟你沟通时,往往默认你也能看到他正在浏览的 RemNote 页面,但实际上你们的信息是不对等的。当你发现以下情况时,**必须主动调用 \\\`read_context\\\` 来对齐信息**:
138
+ - 用户提到了你没有上下文的内容(如"这个"、"当前页面"、"这里")
139
+ - 用户的描述与你已知的信息对不上
140
+ - 你搜索不到用户提到的某些内容
141
+ - 用户似乎在引用他正在查看的界面
142
+
143
+ 先用 \\\`read_context\\\` 看到用户所看到的,再做决策,沟通才能顺畅。
144
+
137
145
  使用 \\\`read_context\\\`:
138
146
  - **focus 模式**(默认):以用户当前光标所在的 Rem 为中心,构建鱼眼视图——焦点处完全展开,周围递减。焦点行以 \\\`* \\\` 前缀标记。需要用户在 RemNote 中已点击某个 Rem。
139
147
  - **page 模式**:以当前打开的页面为根,均匀展开子树。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remnote-bridge",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "RemNote 自动化桥接工具集:CLI + MCP Server + Plugin",
5
5
  "type": "module",
6
6
  "bin": {
@@ -191,6 +191,16 @@ RemNote 格式设置(fontSize、highlightColor 等)底层通过 Powerup 机
191
191
 
192
192
  ## 2. 命令决策
193
193
 
194
+ ### ⚠️ 用户正在看的页面对你不可见
195
+
196
+ 用户在跟你沟通时,往往默认你也能看到他正在浏览的 RemNote 页面,但实际上你们的信息是不对等的。当你发现以下情况时,**必须主动执行 `read-context` 来对齐信息**:
197
+ - 用户提到了你没有上下文的内容(如"这个"、"当前页面"、"这里")
198
+ - 用户的描述与你已知的信息对不上
199
+ - 你搜索不到用户提到的某些内容
200
+ - 用户似乎在引用他正在查看的界面
201
+
202
+ 先用 `read-context` 看到用户所看到的,再做决策,沟通才能顺畅。
203
+
194
204
  ### 读取:用户想了解什么?
195
205
 
196
206
  ```