fuli-context 0.2.0 → 0.3.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 CHANGED
@@ -19,10 +19,10 @@ Cursor 在会话之间复用个人偏好、项目约束、历史决策与 Runboo
19
19
 
20
20
  ```bash
21
21
  npm install --global fuli-context
22
- fl setup
22
+ fuli setup
23
23
  ```
24
24
 
25
- `fl setup` 会先展示即将进行的操作,再请求确认。它会检查容器运行时、初始化本机
25
+ `fuli setup` 会先展示即将进行的操作,再请求确认。它会检查容器运行时、初始化本机
26
26
  Graphiti / Neo4j、创建个人空间、安装配套 Agent Skills,并为检测到的 Agent 注册
27
27
  `fuli` MCP。Codex 接入还会在用户级 `AGENTS.md` 中合并一段很短的 Bootstrap:每个
28
28
  用户任务开始时先调用协作偏好工具,并把当前工作目录作为 `projectPath` 传给 Fuli。
@@ -32,59 +32,152 @@ Provider。
32
32
  初始化完成后:
33
33
 
34
34
  ```bash
35
- fl open
35
+ fuli open
36
36
  ```
37
37
 
38
38
  管理界面默认位于 `http://127.0.0.1:2727`。
39
39
 
40
- ## 常用命令
40
+ ## CLI 使用
41
+
42
+ 全局安装会提供两个等价命令:
43
+
44
+ - `fuli`:完整命令名,文档默认使用它;
45
+ - `fl`:短别名,例如 `fl status` 与 `fuli status` 完全相同。
46
+
47
+ 查看版本和内置帮助:
48
+
49
+ ```bash
50
+ fuli --version
51
+ fuli --help
52
+ ```
53
+
54
+ ### 本地服务与安装
55
+
56
+ | 命令 | 用途 |
57
+ | --- | --- |
58
+ | `fuli setup [选项]` | 初始化 Provider、管理界面、Agent 接入和 Skills;可重复执行 |
59
+ | `fuli start [选项]` | 启动本机服务 |
60
+ | `fuli stop [--data-dir DIR]` | 安全停止本机服务并保留数据 |
61
+ | `fuli restart [选项]` | 重启本机服务 |
62
+ | `fuli status [--json]` | 查看服务状态;`--json` 输出机器可读结果 |
63
+ | `fuli open` | 在浏览器中打开管理界面 |
64
+ | `fuli update [setup 选项]` | 更新全局 npm 包,并用新版 CLI 刷新本机接入 |
65
+ | `fuli uninstall [--yes]` | 清理 Agent 接入和服务,保留知识数据 |
66
+
67
+ 常用示例:
68
+
69
+ ```bash
70
+ fuli start
71
+ fuli start --open
72
+ fuli status
73
+ fuli status --json
74
+ fuli restart
75
+ fuli restart --rebuild
76
+ fuli stop
77
+ ```
78
+
79
+ `fuli stop` 只停止本机服务,不删除图谱数据。
80
+
81
+ `start` 和 `restart` 可使用 `--data-dir DIR`、`--personal-space NAME`、`--port PORT`;
82
+ `--open` 会在启动后打开管理界面,`--rebuild` 会重新构建本机 Provider 容器。
83
+
84
+ ### setup 选项
85
+
86
+ | 选项 | 用途 |
87
+ | --- | --- |
88
+ | `--yes` | 跳过确认,适合无人值守执行 |
89
+ | `--data-dir DIR` | 使用指定数据目录;后续命令应继续传入同一路径 |
90
+ | `--personal-space NAME` | 设置首次初始化时的个人空间名称,默认是 `我` |
91
+ | `--port PORT` | 设置管理界面端口,默认是 `2727` |
92
+ | `--codex-only` | 只接入 Codex |
93
+ | `--skip-agents` | 不修改 Agent 配置,也不刷新 Agent Skills |
94
+ | `--no-start` | 初始化 Provider,但不启动管理界面 |
95
+ | `--personal-only` | 只使用个人 Provider;这是默认模式 |
96
+ | `--with-dev-public` | 同时启动本机开发用公共 Provider,仅用于开发或联调 |
97
+
98
+ 例如,无人值守地完成默认初始化:
41
99
 
42
100
  ```bash
43
- fl --version
44
- fl --help
45
-
46
- fl start
47
- fl start --open
48
- fl status
49
- fl status --json
50
- fl open
51
- fl restart
52
- fl restart --rebuild
53
- fl stop
101
+ fuli setup --yes
54
102
  ```
55
103
 
56
- `fl stop` 只停止本机服务,不删除图谱数据。需要无人值守初始化时可以使用:
104
+ 使用自定义数据目录和管理界面端口:
57
105
 
58
106
  ```bash
59
- fl setup --yes
107
+ fuli setup --data-dir "./fuli-data" --port 3727
60
108
  ```
61
109
 
62
110
  只初始化 Provider、不启动管理界面:
63
111
 
64
112
  ```bash
65
- fl setup --no-start
113
+ fuli setup --no-start
66
114
  ```
67
115
 
68
116
  ## 更新
69
117
 
70
118
  ```bash
71
- fl stop
119
+ fuli update
120
+ ```
121
+
122
+ `fuli update` 默认先展示更新计划并请求确认。需要无人值守执行时:
123
+
124
+ ```bash
125
+ fuli update --yes
126
+ ```
127
+
128
+ 更新流程会先查询 npm 的 `latest` 版本;确认不会降级后,才安全停止旧版服务、安装当时
129
+ 的 `fuli-context@latest` 对应版本,再直接使用新安装版本的 CLI 执行 `setup --yes`。
130
+ 既有知识数据、配置备份和 Neo4j 数据卷不会删除;即使 npm 上已经是当前版本,也会刷新
131
+ Agent 接入和配套 Skills。`update` 支持上表中的全部 setup 选项;使用过自定义数据目录、
132
+ 端口或 Provider 模式时,应在更新时重复传入相同选项。
133
+
134
+ 如果当前 CLI 来自尚未发布的工作区版本,并且版本号高于 npm `latest`,更新会在停止服务
135
+ 和安装包之前退出,避免把开发版本降级。
136
+
137
+ 如果当前已安装版本尚不识别 `update`,需要先手动升级一次到包含该命令的版本:
138
+
139
+ ```bash
72
140
  npm install --global fuli-context@latest
73
- fl setup --yes
141
+ fuli setup --yes
142
+ ```
143
+
144
+ 如果自动更新在 npm 安装或新版 setup 阶段失败,知识数据仍会保留。检查命令输出后可用
145
+ 上面两条命令恢复;使用过自定义 setup 选项时也要一并带上。
146
+
147
+ ## 兼容的本地知识命令
148
+
149
+ 以下命令面向旧版 SQLite 工作流和开发调试。`--db`、`--personal-space` 是全局选项,
150
+ 必须写在子命令之前:
151
+
152
+ ```bash
153
+ fuli --db ./context.db --personal-space 我 search 我 "关键词"
74
154
  ```
75
155
 
76
- 重复执行 setup 会保留既有知识数据,并刷新 Agent 接入和配套 Skills。
156
+ | 命令 | 用途 |
157
+ | --- | --- |
158
+ | `space create NAME --kind personal\|public` | 创建个人或公共空间 |
159
+ | `subscribe PERSONAL_SPACE PUBLIC_SPACE` | 订阅公共空间 |
160
+ | `remember PERSONAL_SPACE --target SPACE --source-kind KIND --text TEXT` | 写入一段待归纳内容 |
161
+ | `observe PERSONAL_SPACE --target SPACE` | 观察当前 Git 改动 |
162
+ | `search PERSONAL_SPACE QUERY` | 搜索当前事实 |
163
+ | `timeline SPACE SUBJECT` | 查看主题时间线 |
164
+ | `rules SPACE` | 查看空间规则 |
165
+ | `history SPACE PREDICATE` | 查看某个事实的修订历史 |
166
+ | `context PERSONAL_SPACE SPACE QUERY` | 输出紧凑的 Agent 上下文包 |
167
+ | `candidates PERSONAL_SPACE` | 列出待处理候选知识 |
168
+ | `candidate ID sync\|personal_only\|ignore` | 处理候选知识 |
169
+ | `migrate --from LEGACY_JSON --to SQLITE_DB` | 将旧 JSON 数据迁移到 SQLite |
77
170
 
78
171
  ## 卸载
79
172
 
80
173
  先清理 Agent 接入,再移除全局 npm 包:
81
174
 
82
175
  ```bash
83
- fl uninstall
176
+ fuli uninstall
84
177
  npm uninstall --global fuli-context
85
178
  ```
86
179
 
87
- `fl uninstall` 会:
180
+ `fuli uninstall` 会:
88
181
 
89
182
  - 停止本机 Fuli 服务;
90
183
  - 从 Codex、Claude Code 和 Cursor 中移除 Fuli MCP 接入;
@@ -170,8 +263,8 @@ docker compose -f compose.graphiti.yml config --quiet
170
263
  ```
171
264
 
172
265
  `npm run test:package` 会构建前端、生成真实 npm tarball、安装到隔离的全局前缀,并验证
173
- `fl`、版本号、帮助信息和已发布 Web UI。测试源码、QA 截图与内部设计文档都不会进入
174
- npm 包。
266
+ `fuli` / `fl`、版本号、帮助信息和已发布 Web UI。测试源码、QA 截图与内部设计文档
267
+ 都不会进入 npm 包。
175
268
 
176
269
  ## License
177
270
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fuli-graph-provider"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Graphiti-backed personal and shared project provider for Fuli."
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "fuli-context",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "fuli-context",
9
- "version": "0.2.0",
9
+ "version": "0.3.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@hono/node-server": "2.0.12",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fuli-context",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Local-first context graph for AI agents.",
5
5
  "type": "module",
6
6
  "files": [
@@ -105,8 +105,9 @@ For the chosen marker, the final answer must begin with its `leadMarkdown` uncha
105
105
  before any heading or prose, and append its `markdown` unchanged after the answer. If
106
106
  any search supports the answer, choose `sourceMarker` from one supporting search. If
107
107
  none do, choose `noMatchSourceMarker`. Never show either marker when no search was
108
- called. Never fabricate, rewrite, omit, or wrap markers in HTML. A capture or write
109
- result is not a read citation.
108
+ called. For a no-match search, `noMatchSourceMarker.markdown` is intentionally empty:
109
+ show only its `leadMarkdown` and do not synthesize a footer. Never fabricate, rewrite,
110
+ omit, or wrap markers in HTML. A capture or write result is not a read citation.
110
111
 
111
112
  ## Decide What Persists
112
113
 
@@ -32,7 +32,8 @@ export function isRegisteredCommand(command) {
32
32
  }
33
33
 
34
34
  export function isCliCommand(command) {
35
- return command === 'setup' || command === 'uninstall' || command === 'migrate' ||
35
+ return command === 'setup' || command === 'update' ||
36
+ command === 'uninstall' || command === 'migrate' ||
36
37
  LOCAL_RUNTIME_COMMANDS.has(command) || isRegisteredCommand(command);
37
38
  }
38
39
 
@@ -41,25 +42,26 @@ export function isLocalRuntimeCommand(command) {
41
42
  }
42
43
 
43
44
  export function printHelp() {
44
- console.log(`fl <command>
45
+ console.log(`fuli <command> (short alias: fl)
45
46
 
46
47
  General:
47
48
  --help, -h
48
49
  --version, -v
49
50
 
50
51
  Local service:
51
- start [--port 2727] [--open] [--rebuild] [--data-dir DIR]
52
+ start [--port 2727] [--open] [--rebuild] [--data-dir DIR] [--personal-space NAME]
52
53
  stop [--data-dir DIR]
53
- restart [--port 2727] [--open] [--rebuild] [--data-dir DIR]
54
+ restart [--port 2727] [--open] [--rebuild] [--data-dir DIR] [--personal-space NAME]
54
55
  status [--json] [--data-dir DIR]
55
56
  open [--data-dir DIR]
56
57
 
57
58
  Install and Agent connection:
58
- setup [--yes] [--codex-only] [--data-dir DIR] [--personal-space NAME] [--port PORT] [--skip-agents] [--no-start] [--with-dev-public]
59
+ setup [--yes] [--codex-only] [--data-dir DIR] [--personal-space NAME] [--port PORT] [--skip-agents] [--no-start] [--personal-only|--with-dev-public]
60
+ update [setup options]
59
61
  uninstall [--yes] [--data-dir DIR]
60
62
 
61
63
  Legacy local knowledge commands:
62
- fl [--db SQLITE_DB] [--personal-space 我] <command>
64
+ fuli [--db SQLITE_DB] [--personal-space 我] <command>
63
65
  Global options must appear before the command.
64
66
 
65
67
  Commands:
@@ -0,0 +1,293 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { posix, win32 } from 'node:path';
4
+ import { createInterface } from 'node:readline/promises';
5
+
6
+ import {
7
+ FULI_PACKAGE_NAME,
8
+ FULI_VERSION
9
+ } from '../package-metadata.js';
10
+ import { quoteShellArgument } from '../runtime-options.js';
11
+ import { parseUpdateOptions } from '../setup/options.js';
12
+ import { runLocalRuntimeCommand } from './local-runtime-command.js';
13
+
14
+ const LATEST_PACKAGE = `${FULI_PACKAGE_NAME}@latest`;
15
+
16
+ export async function runUpdateCommand(args = [], dependencies = {}) {
17
+ const options = parseUpdateOptions(args);
18
+ const env = dependencies.env ?? process.env;
19
+ const platform = dependencies.platform ?? process.platform;
20
+ const nodePath = dependencies.nodePath ?? process.execPath;
21
+ const npmCommand = dependencies.npmCommand ??
22
+ (platform === 'win32' ? 'npm.cmd' : 'npm');
23
+ const confirm = dependencies.confirm ?? confirmInTerminal;
24
+ const write = dependencies.write ?? writeLine;
25
+ const stopRuntime = dependencies.stopRuntime ?? runLocalRuntimeCommand;
26
+ const runInherited = dependencies.runInherited ?? spawnInherited;
27
+ const runCaptured = dependencies.runCaptured ?? spawnCaptured;
28
+ const fileExists = dependencies.fileExists ?? existsSync;
29
+ const resolveLatestVersion = dependencies.resolveLatestVersion ??
30
+ (() => queryLatestVersion(npmCommand, runCaptured, env));
31
+
32
+ write(formatUpdatePreview(options));
33
+ if (!options.yes && !await confirm()) {
34
+ write('已取消,没有修改任何内容。');
35
+ return { status: 'cancelled' };
36
+ }
37
+
38
+ const latestVersion = resolveLatestVersion();
39
+ if (compareSemanticVersions(latestVersion, FULI_VERSION) < 0) {
40
+ const result = {
41
+ status: 'ahead',
42
+ previousVersion: FULI_VERSION,
43
+ version: FULI_VERSION,
44
+ latestVersion
45
+ };
46
+ write(formatUpdateResult(result));
47
+ return result;
48
+ }
49
+
50
+ const stopArgs = options.dataDir ? ['--data-dir', options.dataDir] : [];
51
+ const stopped = await stopRuntime('stop', stopArgs, { env, write });
52
+ if (stopped.status === 'partial') {
53
+ throw new Error([
54
+ '无法安全确认旧版界面进程已停止,更新尚未开始。',
55
+ '请先运行 fuli status 检查本地状态,再重试 fuli update。'
56
+ ].join('\n'));
57
+ }
58
+
59
+ const selectedPackage = `${FULI_PACKAGE_NAME}@${latestVersion}`;
60
+ write(`正在安装 ${selectedPackage}(npm latest)…`);
61
+ const installed = runInherited(npmCommand, [
62
+ 'install',
63
+ '--global',
64
+ selectedPackage,
65
+ '--no-audit',
66
+ '--no-fund'
67
+ ], { env });
68
+ if (!processSucceeded(installed)) {
69
+ throw new Error([
70
+ `全局包安装失败(${describeProcessFailure(installed)})。知识数据未删除。`,
71
+ `请先运行 npm install --global ${LATEST_PACKAGE},再运行 fuli setup --yes。`
72
+ ].join('\n'));
73
+ }
74
+
75
+ const globalRootResult = runCaptured(npmCommand, ['root', '--global'], { env });
76
+ if (!processSucceeded(globalRootResult)) {
77
+ throw postInstallError(
78
+ `无法定位 npm 全局安装目录(${describeProcessFailure(globalRootResult)})`,
79
+ args,
80
+ platform
81
+ );
82
+ }
83
+ const globalRoot = String(globalRootResult.stdout ?? '').trim();
84
+ const cliPath = resolveGlobalCliPath(globalRoot, platform);
85
+ if (!cliPath || !fileExists(cliPath)) {
86
+ throw postInstallError('新版 CLI 文件不存在,请重新安装全局包', args, platform);
87
+ }
88
+
89
+ const versionResult = runCaptured(nodePath, [cliPath, '--version'], { env });
90
+ if (!processSucceeded(versionResult)) {
91
+ throw postInstallError(
92
+ `新版 CLI 无法启动(${describeProcessFailure(versionResult)})`,
93
+ args,
94
+ platform
95
+ );
96
+ }
97
+ const version = String(versionResult.stdout ?? '').trim();
98
+ if (!version) throw postInstallError('新版 CLI 未返回版本号', args, platform);
99
+ if (compareSemanticVersions(version, latestVersion) !== 0) {
100
+ throw postInstallError(
101
+ `新版 CLI 版本 ${version} 与更新前确认的 npm latest ${latestVersion} 不一致`,
102
+ args,
103
+ platform
104
+ );
105
+ }
106
+
107
+ write('正在使用新版 CLI 刷新本机服务、Agent 接入和 Skills…');
108
+ const setupArgs = ['setup', '--yes', ...args.filter((arg) => arg !== '--yes')];
109
+ const setupResult = runInherited(nodePath, [cliPath, ...setupArgs], { env });
110
+ if (!processSucceeded(setupResult)) {
111
+ throw postInstallError(
112
+ `新版已安装,但 setup 未完成(${describeProcessFailure(setupResult)})`,
113
+ args,
114
+ platform
115
+ );
116
+ }
117
+
118
+ const result = {
119
+ status: version === FULI_VERSION ? 'current' : 'updated',
120
+ previousVersion: FULI_VERSION,
121
+ version
122
+ };
123
+ write(formatUpdateResult(result));
124
+ return result;
125
+ }
126
+
127
+ export function formatUpdatePreview(options) {
128
+ return [
129
+ '准备更新复利',
130
+ `npm 包:${LATEST_PACKAGE}`,
131
+ options.noStart
132
+ ? '本地界面:安全停止;更新后保持关闭'
133
+ : '本地服务:安全停止;更新后由新版重新启动',
134
+ options.dataDir ? `数据:保留 ${options.dataDir}` : '数据:保留当前默认数据目录',
135
+ `Agent 接入与 Skills:${options.skipAgents ? '按参数跳过刷新' : '由新版 setup 刷新'}`
136
+ ].join('\n');
137
+ }
138
+
139
+ export function formatUpdateResult(result) {
140
+ if (result.status === 'ahead') {
141
+ return [
142
+ `当前 CLI 版本 ${result.version} 高于 npm latest ${result.latestVersion};`,
143
+ '为避免降级,未停止服务、未安装包,也未修改 Agent 接入。'
144
+ ].join('');
145
+ }
146
+ if (result.status === 'current') {
147
+ return `复利已是最新版本 ${result.version};本机接入已刷新。`;
148
+ }
149
+ return `复利已从 ${result.previousVersion} 更新到 ${result.version}。`;
150
+ }
151
+
152
+ export function compareSemanticVersions(left, right) {
153
+ const parsedLeft = parseSemanticVersion(left);
154
+ const parsedRight = parseSemanticVersion(right);
155
+ for (let index = 0; index < 3; index += 1) {
156
+ if (parsedLeft.core[index] > parsedRight.core[index]) return 1;
157
+ if (parsedLeft.core[index] < parsedRight.core[index]) return -1;
158
+ }
159
+ if (!parsedLeft.prerelease.length && !parsedRight.prerelease.length) return 0;
160
+ if (!parsedLeft.prerelease.length) return 1;
161
+ if (!parsedRight.prerelease.length) return -1;
162
+ const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length);
163
+ for (let index = 0; index < length; index += 1) {
164
+ const leftPart = parsedLeft.prerelease[index];
165
+ const rightPart = parsedRight.prerelease[index];
166
+ if (leftPart === undefined) return -1;
167
+ if (rightPart === undefined) return 1;
168
+ if (leftPart === rightPart) continue;
169
+ const leftNumeric = /^\d+$/.test(leftPart);
170
+ const rightNumeric = /^\d+$/.test(rightPart);
171
+ if (leftNumeric && rightNumeric) {
172
+ return BigInt(leftPart) > BigInt(rightPart) ? 1 : -1;
173
+ }
174
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
175
+ return leftPart > rightPart ? 1 : -1;
176
+ }
177
+ return 0;
178
+ }
179
+
180
+ export function resolveGlobalCliPath(globalRoot, platform = process.platform) {
181
+ const path = platform === 'win32' ? win32 : posix;
182
+ if (typeof globalRoot !== 'string' || !path.isAbsolute(globalRoot.trim())) return null;
183
+ return path.join(globalRoot.trim(), FULI_PACKAGE_NAME, 'src', 'cli.js');
184
+ }
185
+
186
+ function postInstallError(message, args, platform) {
187
+ return new Error([
188
+ `${message}。知识数据未删除。`,
189
+ `请重新运行:${formatSetupRecoveryCommand(args, platform)}`
190
+ ].join('\n'));
191
+ }
192
+
193
+ function queryLatestVersion(npmCommand, runCaptured, env) {
194
+ const result = runCaptured(
195
+ npmCommand,
196
+ ['view', LATEST_PACKAGE, 'version', '--json'],
197
+ { env }
198
+ );
199
+ if (!processSucceeded(result)) {
200
+ throw new Error([
201
+ `无法检查 ${LATEST_PACKAGE}(${describeProcessFailure(result)})。`,
202
+ '本地服务尚未停止,也没有安装或修改任何包。'
203
+ ].join('\n'));
204
+ }
205
+ const raw = String(result.stdout ?? '').trim();
206
+ let version = raw;
207
+ try {
208
+ const parsed = JSON.parse(raw);
209
+ if (typeof parsed === 'string') version = parsed;
210
+ } catch {
211
+ // npm can also return one unquoted version line.
212
+ }
213
+ parseSemanticVersion(version);
214
+ return version;
215
+ }
216
+
217
+ function parseSemanticVersion(value) {
218
+ const text = String(value ?? '').trim();
219
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
220
+ .exec(text);
221
+ if (!match) throw new TypeError(`Invalid package version: ${text || '<empty>'}`);
222
+ const numeric = match.slice(1, 4);
223
+ if (numeric.some((part) => part.length > 1 && part.startsWith('0'))) {
224
+ throw new TypeError(`Invalid package version: ${text}`);
225
+ }
226
+ return {
227
+ core: numeric.map((part) => BigInt(part)),
228
+ prerelease: match[4]?.split('.') ?? []
229
+ };
230
+ }
231
+
232
+ function formatSetupRecoveryCommand(args, platform) {
233
+ const forwarded = args
234
+ .filter((arg) => arg !== '--yes')
235
+ .map((arg) => formatCommandArgument(arg, platform));
236
+ return ['fuli', 'setup', '--yes', ...forwarded].join(' ');
237
+ }
238
+
239
+ function formatCommandArgument(value, platform) {
240
+ const simple = platform === 'win32'
241
+ ? /^[A-Za-z0-9_./\\:@+-]+$/
242
+ : /^[A-Za-z0-9_./:@+-]+$/;
243
+ if (simple.test(value)) return value;
244
+ try {
245
+ return quoteShellArgument(value, platform);
246
+ } catch {
247
+ return '<复用原参数>';
248
+ }
249
+ }
250
+
251
+ function processSucceeded(result) {
252
+ return result?.status === 0 && !result.error && !result.signal;
253
+ }
254
+
255
+ function describeProcessFailure(result) {
256
+ if (result?.error?.message) return result.error.message;
257
+ if (result?.signal) return `signal ${result.signal}`;
258
+ if (Number.isInteger(result?.status)) return `exit code ${result.status}`;
259
+ return 'unknown process error';
260
+ }
261
+
262
+ function spawnInherited(command, args, { env }) {
263
+ return spawnSync(command, args, {
264
+ env,
265
+ stdio: 'inherit',
266
+ windowsHide: true
267
+ });
268
+ }
269
+
270
+ function spawnCaptured(command, args, { env }) {
271
+ return spawnSync(command, args, {
272
+ env,
273
+ encoding: 'utf8',
274
+ stdio: ['ignore', 'pipe', 'pipe'],
275
+ windowsHide: true
276
+ });
277
+ }
278
+
279
+ async function confirmInTerminal() {
280
+ const input = createInterface({ input: process.stdin, output: process.stdout });
281
+ try {
282
+ const answer = (await input.question('将修改全局 npm 安装,继续?[Y/n] '))
283
+ .trim()
284
+ .toLowerCase();
285
+ return answer === '' || answer === 'y' || answer === 'yes';
286
+ } finally {
287
+ input.close();
288
+ }
289
+ }
290
+
291
+ function writeLine(value) {
292
+ process.stdout.write(`${value}\n`);
293
+ }
package/src/cli.js CHANGED
@@ -13,6 +13,7 @@ import { runLocalRuntimeCommand } from './cli/local-runtime-command.js';
13
13
  import { migrateLegacyJson } from './cli/migrate-command.js';
14
14
  import { runSetupCommand } from './cli/setup-command.js';
15
15
  import { runUninstallCommand } from './cli/uninstall-command.js';
16
+ import { runUpdateCommand } from './cli/update-command.js';
16
17
  import { FULI_VERSION } from './package-metadata.js';
17
18
  import { assertSupportedNodeVersion } from './setup/node-runtime.js';
18
19
  import {
@@ -52,6 +53,13 @@ export async function main(argv = process.argv.slice(2), env = process.env) {
52
53
  await runUninstallCommand(commandArgs, { env });
53
54
  return;
54
55
  }
56
+ if (command === 'update') {
57
+ if (runtimeArgs.length) {
58
+ throw new TypeError('Update options must appear after the update command');
59
+ }
60
+ assertSupportedNodeVersion();
61
+ return runUpdateCommand(commandArgs, { env });
62
+ }
55
63
  if (isLocalRuntimeCommand(command)) {
56
64
  if (runtimeArgs.length) {
57
65
  throw new TypeError(`${command} options must appear after the command`);
@@ -90,13 +90,7 @@ function sourceLeadMarkdown({ consoleUrl, citations }) {
90
90
  }
91
91
 
92
92
  function sourceMarkerMarkdown({ consoleUrl, citations }) {
93
- if (!citations.length) {
94
- return [
95
- '**FULI 来源 · 未命中**',
96
- '',
97
- `- [打开 Fuli](${safeLoopbackOrigin(consoleUrl) ?? DEFAULT_CONSOLE_URL}/)`
98
- ].join('\n');
99
- }
93
+ if (!citations.length) return '';
100
94
  const shown = citations.slice(0, MAX_CITATIONS);
101
95
  const hidden = citations.length - shown.length;
102
96
  return [
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
5
  const PROJECT_ROOT = resolve(fileURLToPath(new URL('../..', import.meta.url)));
6
6
  export const WEB_ROOT = join(PROJECT_ROOT, 'dist', 'web');
7
+ const FULI_LOGO_PATTERN = /^fuli-logo(?:-[A-Za-z0-9_-]+)?\.png$/;
7
8
  const MIME_TYPES = {
8
9
  '.html': 'text/html; charset=utf-8',
9
10
  '.css': 'text/css; charset=utf-8',
@@ -18,8 +19,17 @@ const MIME_TYPES = {
18
19
 
19
20
  export function serveStatic(pathname, response) {
20
21
  if (pathname === '/favicon.ico') {
21
- response.writeHead(204);
22
- response.end();
22
+ const filePath = faviconFilePath();
23
+ if (!filePath) {
24
+ response.writeHead(204);
25
+ response.end();
26
+ return;
27
+ }
28
+ response.writeHead(200, {
29
+ 'cache-control': 'public, max-age=86400',
30
+ 'content-type': 'image/png'
31
+ });
32
+ response.end(readFileSync(filePath));
23
33
  return;
24
34
  }
25
35
 
@@ -50,6 +60,22 @@ export function serveStatic(pathname, response) {
50
60
  response.end(readFileSync(filePath));
51
61
  }
52
62
 
63
+ function faviconFilePath() {
64
+ const directPath = join(WEB_ROOT, 'favicon.png');
65
+ if (existsSync(directPath)) return directPath;
66
+
67
+ const assetsPath = join(WEB_ROOT, 'assets');
68
+ if (!existsSync(assetsPath)) return null;
69
+ try {
70
+ const filename = readdirSync(assetsPath)
71
+ .sort()
72
+ .find((candidate) => FULI_LOGO_PATTERN.test(candidate));
73
+ return filename ? join(assetsPath, filename) : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
53
79
  function shouldServeApplication(pathname) {
54
80
  return extname(pathname) === '' && !pathname.startsWith('/api/');
55
81
  }
@@ -6,5 +6,5 @@ export const MCP_INSTRUCTIONS = [
6
6
  'Changing/live: use monitoring or Git MCP',
7
7
  'Silently batch via capture_session_knowledge under personalProjectId. Never store secrets, transcripts, or guesses. Only human/source confirmed knowledge enters public review. Final checkpoint: flush or retain nothing.',
8
8
  'When personal knowledge affects the final answer/action, call record_knowledge_usage. Retrieval/injection never count; it creates at most agent_confirmed, never public eligibility.',
9
- 'Searches return terminal-safe Markdown. Final MUST begin with sourceMarker.leadMarkdown and append sourceMarker.markdown unchanged; without support use noMatchSourceMarker. Never wrap markers in HTML, rewrite them, or cite writes as reads.'
9
+ 'Terminal-safe Markdown. Final MUST begin with sourceMarker.leadMarkdown; append sourceMarker.markdown unchanged only on match. On miss use noMatchSourceMarker leadMarkdown only (empty markdown). Never wrap markers in HTML or cite writes.'
10
10
  ].join(' ');
@@ -16,6 +16,14 @@ const BOOLEAN_OPTIONS = Object.freeze({
16
16
  const PROVIDER_MODE_OPTIONS = new Set(['--personal-only', '--with-dev-public']);
17
17
 
18
18
  export function parseSetupOptions(args = []) {
19
+ return parseSetupLikeOptions(args, 'setup');
20
+ }
21
+
22
+ export function parseUpdateOptions(args = []) {
23
+ return parseSetupLikeOptions(args, 'update');
24
+ }
25
+
26
+ function parseSetupLikeOptions(args, command) {
19
27
  const result = {
20
28
  dataDir: null,
21
29
  personalSpaceName: '我',
@@ -47,7 +55,7 @@ export function parseSetupOptions(args = []) {
47
55
  continue;
48
56
  }
49
57
  const valueKey = VALUE_OPTIONS[flag];
50
- if (!valueKey) throw new TypeError(`Unknown setup option: ${flag}`);
58
+ if (!valueKey) throw new TypeError(`Unknown ${command} option: ${flag}`);
51
59
  const value = args[index + 1];
52
60
  if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
53
61
  throw new TypeError(`Missing value for ${flag}`);