fuli-context 0.2.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 (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +178 -0
  3. package/compose.graphiti.yml +86 -0
  4. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
  6. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
  9. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
  11. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
  13. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
  14. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
  15. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
  16. package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
  17. package/dist/web/assets/console-tbvTUi6p.js +1 -0
  18. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  19. package/dist/web/assets/identity-B3s5CXhv.js +1 -0
  20. package/dist/web/assets/index-Devf7glZ.css +1 -0
  21. package/dist/web/assets/index-DwUDhQKc.js +6 -0
  22. package/dist/web/assets/model-DVT8sjy9.js +1 -0
  23. package/dist/web/assets/paths-B790iniO.js +1 -0
  24. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
  25. package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
  26. package/dist/web/index.html +22 -0
  27. package/graph-provider/Dockerfile +16 -0
  28. package/graph-provider/fuli_graph/__init__.py +5 -0
  29. package/graph-provider/fuli_graph/app.py +547 -0
  30. package/graph-provider/fuli_graph/auth.py +17 -0
  31. package/graph-provider/fuli_graph/collaboration_context.py +290 -0
  32. package/graph-provider/fuli_graph/config.py +34 -0
  33. package/graph-provider/fuli_graph/graph_models.py +123 -0
  34. package/graph-provider/fuli_graph/graph_projection.py +405 -0
  35. package/graph-provider/fuli_graph/graph_query.py +561 -0
  36. package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
  37. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
  38. package/graph-provider/fuli_graph/knowledge_management.py +816 -0
  39. package/graph-provider/fuli_graph/knowledge_records.py +65 -0
  40. package/graph-provider/fuli_graph/knowledge_search.py +723 -0
  41. package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
  42. package/graph-provider/fuli_graph/model_validation.py +58 -0
  43. package/graph-provider/fuli_graph/models.py +1179 -0
  44. package/graph-provider/fuli_graph/personal_project_access.py +29 -0
  45. package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
  46. package/graph-provider/fuli_graph/project_action_models.py +142 -0
  47. package/graph-provider/fuli_graph/project_knowledge.py +707 -0
  48. package/graph-provider/fuli_graph/project_management.py +251 -0
  49. package/graph-provider/fuli_graph/provider_values.py +40 -0
  50. package/graph-provider/fuli_graph/runtime.py +350 -0
  51. package/graph-provider/fuli_graph/search_projection.py +156 -0
  52. package/graph-provider/fuli_graph/store.py +24 -0
  53. package/graph-provider/fuli_graph/store_identity.py +237 -0
  54. package/graph-provider/fuli_graph/store_knowledge.py +447 -0
  55. package/graph-provider/fuli_graph/store_projects.py +242 -0
  56. package/graph-provider/fuli_graph/store_publication.py +370 -0
  57. package/graph-provider/fuli_graph/store_records.py +134 -0
  58. package/graph-provider/pyproject.toml +35 -0
  59. package/npm-shrinkwrap.json +6045 -0
  60. package/package.json +99 -0
  61. package/skills/capturing-session-knowledge/SKILL.md +226 -0
  62. package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
  63. package/skills/grilling-project/SKILL.md +129 -0
  64. package/skills/grilling-project/agents/openai.yaml +4 -0
  65. package/skills/grilling-project/references/project-profile.md +105 -0
  66. package/src/agent/candidate-read-service.js +53 -0
  67. package/src/agent/context-pack-service.js +83 -0
  68. package/src/agent/public-read-policy.js +39 -0
  69. package/src/agent/public-read-service.js +69 -0
  70. package/src/agent/read-projection.js +107 -0
  71. package/src/agent/search-service.js +61 -0
  72. package/src/agent/space-boundary.js +33 -0
  73. package/src/agent-tools/graph-definitions.js +479 -0
  74. package/src/agent-tools/graph-handlers.js +96 -0
  75. package/src/agent-tools/schema.js +37 -0
  76. package/src/agent-tools.js +16 -0
  77. package/src/app/application-error.js +13 -0
  78. package/src/app/bootstrap.js +8 -0
  79. package/src/app/create-application.js +143 -0
  80. package/src/app/state-service.js +38 -0
  81. package/src/candidates.js +78 -0
  82. package/src/classifier.js +63 -0
  83. package/src/cli/command-arguments.js +14 -0
  84. package/src/cli/command-registry.js +78 -0
  85. package/src/cli/commands/candidates.js +17 -0
  86. package/src/cli/commands/ingestion.js +32 -0
  87. package/src/cli/commands/reads.js +50 -0
  88. package/src/cli/commands/spaces.js +20 -0
  89. package/src/cli/invocation.js +47 -0
  90. package/src/cli/local-runtime-command.js +94 -0
  91. package/src/cli/local-runtime-options.js +64 -0
  92. package/src/cli/migrate-command.js +71 -0
  93. package/src/cli/migration-destination.js +117 -0
  94. package/src/cli/setup-agent-selection.js +195 -0
  95. package/src/cli/setup-command.js +85 -0
  96. package/src/cli/uninstall-command.js +67 -0
  97. package/src/cli.js +90 -0
  98. package/src/context-pack.js +206 -0
  99. package/src/defaults.js +1 -0
  100. package/src/extractor.js +77 -0
  101. package/src/fact-history.js +39 -0
  102. package/src/facts/fact-service.js +89 -0
  103. package/src/graphiti/agent-access-policy.js +88 -0
  104. package/src/graphiti/capture-policy.js +84 -0
  105. package/src/graphiti/federated-application.js +1292 -0
  106. package/src/graphiti/personal-context.js +29 -0
  107. package/src/graphiti/project-path-context.js +159 -0
  108. package/src/graphiti/provider-client.js +288 -0
  109. package/src/graphiti/runtime-config.js +85 -0
  110. package/src/graphiti/setup.js +43 -0
  111. package/src/graphiti/source-marker.js +142 -0
  112. package/src/http/api-router.js +133 -0
  113. package/src/http/bounded-json-array.js +19 -0
  114. package/src/http/error-mapping.js +36 -0
  115. package/src/http/graph-api-router.js +257 -0
  116. package/src/http/lens-route.js +50 -0
  117. package/src/http/request-policy.js +41 -0
  118. package/src/http/response.js +29 -0
  119. package/src/http/static-handler.js +55 -0
  120. package/src/ingestion.js +161 -0
  121. package/src/lens/lens-correction-service.js +81 -0
  122. package/src/lens/lens-fact-writer.js +85 -0
  123. package/src/lens/lens-input.js +62 -0
  124. package/src/lens/lens-query-projection.js +87 -0
  125. package/src/lens/lens-query-ranking.js +62 -0
  126. package/src/lens/lens-query.js +143 -0
  127. package/src/lens/lens-resource-projection.js +93 -0
  128. package/src/lens/lens-resource-selection.js +45 -0
  129. package/src/lens/lens-resource-service.js +103 -0
  130. package/src/lens/lens-search-query.js +122 -0
  131. package/src/lens/lens-service.js +183 -0
  132. package/src/local-runtime/lifecycle.js +254 -0
  133. package/src/mcp/create-mcp-server.js +85 -0
  134. package/src/mcp/instructions.js +10 -0
  135. package/src/mcp/project-action-preview-tokens.js +72 -0
  136. package/src/mcp/runtime.js +71 -0
  137. package/src/mcp/tool-annotations.js +63 -0
  138. package/src/mcp/tool-result.js +167 -0
  139. package/src/mcp/tool-schema.js +60 -0
  140. package/src/mcp-server.js +76 -0
  141. package/src/models.js +44 -0
  142. package/src/observer.js +83 -0
  143. package/src/package-metadata.js +6 -0
  144. package/src/project-rules.js +33 -0
  145. package/src/publication/canonical-json.js +39 -0
  146. package/src/publication/outbox-service.js +46 -0
  147. package/src/publication/publication-envelope-schema.js +75 -0
  148. package/src/publication/publication-envelope.js +37 -0
  149. package/src/publication/publication-policy.js +82 -0
  150. package/src/publication/publication-service.js +48 -0
  151. package/src/router.js +58 -0
  152. package/src/runtime-options.js +139 -0
  153. package/src/security/episode-visibility.js +14 -0
  154. package/src/security/fact-visibility.js +15 -0
  155. package/src/security/sensitive-content.js +18 -0
  156. package/src/security/source-metadata-policy.js +8 -0
  157. package/src/server/application-lifecycle.js +34 -0
  158. package/src/server/listen.js +24 -0
  159. package/src/server.js +135 -0
  160. package/src/setup/agent-installation-status.js +97 -0
  161. package/src/setup/agents.js +123 -0
  162. package/src/setup/codex-bootstrap.js +118 -0
  163. package/src/setup/codex-config.js +122 -0
  164. package/src/setup/config-backup.js +22 -0
  165. package/src/setup/container-runtime.js +388 -0
  166. package/src/setup/cursor-config.js +61 -0
  167. package/src/setup/graph-runtime.js +360 -0
  168. package/src/setup/node-runtime.js +24 -0
  169. package/src/setup/options.js +91 -0
  170. package/src/setup/paths.js +47 -0
  171. package/src/setup/session-skill.js +91 -0
  172. package/src/setup/setup.js +85 -0
  173. package/src/setup/uninstall.js +90 -0
  174. package/src/storage/episode-evidence-preview.js +100 -0
  175. package/src/storage/fact-search-order.js +18 -0
  176. package/src/storage/file/episode-evidence.js +37 -0
  177. package/src/storage/file/episode-index.js +59 -0
  178. package/src/storage/file/fact-search.js +60 -0
  179. package/src/storage/file-store.js +437 -0
  180. package/src/storage/import-json.js +70 -0
  181. package/src/storage/json-file.js +41 -0
  182. package/src/storage/legacy-record-validation.js +152 -0
  183. package/src/storage/legacy-snapshot-validation.js +32 -0
  184. package/src/storage/migrate.js +128 -0
  185. package/src/storage/migrations/001-initial.sql +83 -0
  186. package/src/storage/migrations/002-storage-integrity.sql +113 -0
  187. package/src/storage/record-validation.js +46 -0
  188. package/src/storage/snapshot-defaults.js +54 -0
  189. package/src/storage/sqlite/candidates-repository.js +84 -0
  190. package/src/storage/sqlite/episode-evidence-repository.js +154 -0
  191. package/src/storage/sqlite/episode-privacy-sql.js +33 -0
  192. package/src/storage/sqlite/episodes-repository.js +52 -0
  193. package/src/storage/sqlite/fact-search-repository.js +162 -0
  194. package/src/storage/sqlite/facts-repository.js +146 -0
  195. package/src/storage/sqlite/imports-repository.js +39 -0
  196. package/src/storage/sqlite/mapper.js +84 -0
  197. package/src/storage/sqlite/open-database.js +15 -0
  198. package/src/storage/sqlite/outbox-repository.js +103 -0
  199. package/src/storage/sqlite/snapshot-repository.js +69 -0
  200. package/src/storage/sqlite/spaces-repository.js +52 -0
  201. package/src/storage/sqlite/subscriptions-repository.js +54 -0
  202. package/src/storage/sqlite-store.js +159 -0
  203. package/src/storage/stable-json.js +24 -0
  204. package/src/storage/store-port.js +20 -0
  205. package/src/store.js +2 -0
  206. package/src/workspace-protocol/array-validation.js +19 -0
  207. package/src/workspace-protocol/base64url-contract.js +27 -0
  208. package/src/workspace-protocol/constants.js +37 -0
  209. package/src/workspace-protocol/error-contract.js +24 -0
  210. package/src/workspace-protocol/fact-contract.js +110 -0
  211. package/src/workspace-protocol/governance-contract.js +47 -0
  212. package/src/workspace-protocol/index.js +53 -0
  213. package/src/workspace-protocol/json-budget.js +19 -0
  214. package/src/workspace-protocol/manifest-contract.js +186 -0
  215. package/src/workspace-protocol/publication-contract.js +212 -0
  216. package/src/workspace-protocol/query-contract.js +149 -0
  217. package/src/workspace-protocol/scalar-contract.js +192 -0
  218. package/src/workspace-protocol/sync-contract.js +184 -0
  219. package/src/workspace-protocol/text-contract.js +39 -0
  220. package/src/workspace-protocol/validation.js +66 -0
@@ -0,0 +1,388 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { delimiter, dirname, join } from 'node:path';
5
+
6
+ const DOCKER_INFO_ARGS = ['info', '--format', '{{.ServerVersion}}'];
7
+ const RANCHER_DOWNLOAD_URL = 'https://rancherdesktop.io/';
8
+ const DOCKER_DOWNLOAD_URL = 'https://www.docker.com/products/docker-desktop/';
9
+
10
+ export function inspectContainerRuntime({
11
+ env = process.env,
12
+ platform = process.platform,
13
+ homeDir = homedir(),
14
+ pathExists = existsSync,
15
+ run = runProcess
16
+ } = {}) {
17
+ const desktop = detectDesktopRuntime({ env, platform, homeDir, pathExists });
18
+ const dockerCommand = resolveDockerCommand({ env, platform, homeDir, pathExists, run });
19
+ if (!dockerCommand) {
20
+ return {
21
+ status: desktop ? 'stopped' : 'missing',
22
+ label: desktop?.label ?? 'Docker',
23
+ desktop,
24
+ dockerCommand: null,
25
+ dockerEnvironment: null,
26
+ detail: 'docker command was not found'
27
+ };
28
+ }
29
+
30
+ const dockerEnvironment = selectDockerEnvironment({
31
+ env: dockerCommandEnvironment(dockerCommand, env, platform),
32
+ platform,
33
+ homeDir,
34
+ socketExists: pathExists,
35
+ dockerAvailable: (candidateEnvironment) => dockerInfoIndicatesDaemon(
36
+ run(dockerCommand, DOCKER_INFO_ARGS, candidateEnvironment)
37
+ )
38
+ });
39
+ const compose = run(dockerCommand, ['compose', 'version'], dockerEnvironment);
40
+ if (!processSucceeded(compose)) {
41
+ return {
42
+ status: 'missing-compose',
43
+ label: desktop?.label ?? 'Docker',
44
+ desktop,
45
+ dockerCommand,
46
+ dockerEnvironment,
47
+ detail: safeProcessDetail(compose, homeDir)
48
+ };
49
+ }
50
+
51
+ const info = run(dockerCommand, DOCKER_INFO_ARGS, dockerEnvironment);
52
+ if (dockerInfoIndicatesDaemon(info)) {
53
+ return {
54
+ status: 'ready',
55
+ label: desktop?.label ?? 'Docker',
56
+ desktop,
57
+ dockerCommand,
58
+ dockerEnvironment,
59
+ detail: null
60
+ };
61
+ }
62
+
63
+ const explicitTarget = nonEmpty(env.DOCKER_HOST) || nonEmpty(env.DOCKER_CONTEXT);
64
+ return {
65
+ status: desktop && !explicitTarget ? 'stopped' : 'unavailable',
66
+ label: desktop?.label ?? 'Docker',
67
+ desktop: explicitTarget ? null : desktop,
68
+ dockerCommand,
69
+ dockerEnvironment,
70
+ detail: safeProcessDetail(info, homeDir),
71
+ explicitTarget: Boolean(explicitTarget)
72
+ };
73
+ }
74
+
75
+ export async function ensureContainerRuntime(options = {}, dependencies = {}) {
76
+ const inspect = dependencies.inspect ?? inspectContainerRuntime;
77
+ const launch = dependencies.launch ?? launchDesktopRuntime;
78
+ const wait = dependencies.wait ?? delay;
79
+ const onProgress = options.onProgress ?? (() => {});
80
+ const pollIntervalMs = options.pollIntervalMs ?? 1000;
81
+ const maxWaitMs = options.maxWaitMs ?? 180_000;
82
+ let runtime = inspect(options);
83
+
84
+ if (runtime.status === 'ready') return runtime;
85
+ if (runtime.status !== 'stopped' || !runtime.desktop) {
86
+ throw containerRuntimeError(runtime, options.platform ?? process.platform);
87
+ }
88
+
89
+ onProgress(`正在启动 ${runtime.desktop.label},首次启动可能需要几分钟…`);
90
+ await launch(runtime.desktop, dependencies);
91
+
92
+ const attempts = Math.max(1, Math.ceil(maxWaitMs / pollIntervalMs));
93
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
94
+ runtime = inspect(options);
95
+ if (runtime.status === 'ready') return runtime;
96
+ if (runtime.status === 'missing-compose' || runtime.status === 'missing') {
97
+ throw containerRuntimeError(runtime, options.platform ?? process.platform);
98
+ }
99
+ if (attempt < attempts - 1) await wait(pollIntervalMs);
100
+ }
101
+
102
+ throw new Error(
103
+ `${runtime.desktop?.label ?? runtime.label} 已启动,但容器引擎未在 ` +
104
+ `${Math.ceil(maxWaitMs / 1000)} 秒内就绪。请打开该应用查看错误,确认 docker info ` +
105
+ `可以成功后重新运行 fl setup。${detailSuffix(runtime.detail)}`
106
+ );
107
+ }
108
+
109
+ export function runDockerCompose(args, runtime = null, {
110
+ platform = process.platform,
111
+ homeDir = homedir(),
112
+ run = runProcess
113
+ } = {}) {
114
+ const resolved = runtime ?? inspectContainerRuntime({ platform, homeDir, run });
115
+ if (resolved.status !== 'ready') {
116
+ throw containerRuntimeError(resolved, platform);
117
+ }
118
+ const result = run(
119
+ resolved.dockerCommand,
120
+ args,
121
+ resolved.dockerEnvironment
122
+ );
123
+ if (processSucceeded(result)) return;
124
+
125
+ const detail = safeProcessDetail(result, homeDir);
126
+ if (/port is already allocated|address already in use|bind.+failed/i.test(detail)) {
127
+ throw new Error(`Fuli 需要的本地端口已被占用。${detailSuffix(detail)}`);
128
+ }
129
+ if (/cannot connect|connection refused|error during connect|docker daemon|\bEOF\b/i.test(detail)) {
130
+ throw new Error(`容器运行时在启动 Fuli Provider 时失去连接。${detailSuffix(detail)}`);
131
+ }
132
+ throw new Error(`Docker Compose 无法启动 Fuli Provider。${detailSuffix(detail)}`);
133
+ }
134
+
135
+ export function selectDockerEnvironment({
136
+ env = process.env,
137
+ platform = process.platform,
138
+ homeDir = homedir(),
139
+ dockerAvailable = defaultDockerAvailable,
140
+ socketExists = existsSync
141
+ } = {}) {
142
+ const selected = { ...env };
143
+ if (nonEmpty(env.DOCKER_HOST) || nonEmpty(env.DOCKER_CONTEXT)) return selected;
144
+ if (dockerAvailable(selected)) return selected;
145
+ if (platform !== 'darwin') return selected;
146
+
147
+ const rancherSocket = join(homeDir, '.rd', 'docker.sock');
148
+ if (socketExists(rancherSocket)) {
149
+ selected.DOCKER_HOST = `unix://${rancherSocket}`;
150
+ }
151
+ return selected;
152
+ }
153
+
154
+ export function dockerInfoIndicatesDaemon(result) {
155
+ return processSucceeded(result) && nonEmpty(result.stdout);
156
+ }
157
+
158
+ export function containerRuntimeError(runtime, platform = process.platform) {
159
+ if (runtime.status === 'missing') {
160
+ return new Error(missingRuntimeMessage(platform));
161
+ }
162
+ if (runtime.status === 'missing-compose') {
163
+ return new Error(
164
+ `已找到 Docker,但缺少 Docker Compose v2。请安装 Compose v2 后重新运行 fl setup。` +
165
+ detailSuffix(runtime.detail)
166
+ );
167
+ }
168
+ if (runtime.explicitTarget) {
169
+ return new Error(
170
+ `DOCKER_HOST 或 DOCKER_CONTEXT 指向的容器引擎不可用。请修复该配置并确认 ` +
171
+ `docker info 可以成功后重新运行 fl setup。${detailSuffix(runtime.detail)}`
172
+ );
173
+ }
174
+ return new Error(
175
+ `已找到 Docker 和 Compose,但容器引擎没有运行。请启动 Docker Desktop、` +
176
+ `Rancher Desktop 或当前 Docker 服务,确认 docker info 可以成功后重新运行 fl setup。` +
177
+ detailSuffix(runtime.detail)
178
+ );
179
+ }
180
+
181
+ function resolveDockerCommand({ env, platform, homeDir, pathExists, run }) {
182
+ const candidates = ['docker', ...bundledDockerCandidates({ env, platform, homeDir })];
183
+ for (const command of candidates) {
184
+ if (command !== 'docker' && !pathExists(command)) continue;
185
+ if (processSucceeded(run(command, ['--version'], env))) return command;
186
+ }
187
+ return null;
188
+ }
189
+
190
+ function bundledDockerCandidates({ env, platform, homeDir }) {
191
+ if (platform === 'darwin') {
192
+ return [
193
+ join(homeDir, '.rd', 'bin', 'docker'),
194
+ '/Applications/Docker.app/Contents/Resources/bin/docker',
195
+ join(homeDir, 'Applications', 'Docker.app', 'Contents', 'Resources', 'bin', 'docker')
196
+ ];
197
+ }
198
+ if (platform === 'win32') {
199
+ return [
200
+ env.ProgramFiles && join(env.ProgramFiles, 'Docker', 'Docker', 'resources', 'bin', 'docker.exe'),
201
+ env.LOCALAPPDATA && join(env.LOCALAPPDATA, 'Programs', 'Rancher Desktop', 'resources',
202
+ 'resources', 'win32', 'bin', 'docker.exe')
203
+ ].filter(Boolean);
204
+ }
205
+ return ['/usr/bin/docker', '/usr/local/bin/docker'];
206
+ }
207
+
208
+ function dockerCommandEnvironment(command, env, platform) {
209
+ const selected = { ...env };
210
+ if (platform !== 'darwin' || !command.includes('/Docker.app/')) return selected;
211
+ const resources = command.slice(0, command.indexOf('/bin/docker'));
212
+ const pluginDirectory = join(resources, 'cli-plugins');
213
+ const current = nonEmpty(selected.DOCKER_CLI_PLUGIN_EXTRA_DIRS);
214
+ selected.DOCKER_CLI_PLUGIN_EXTRA_DIRS = current
215
+ ? `${current}${delimiter}${pluginDirectory}`
216
+ : pluginDirectory;
217
+ return selected;
218
+ }
219
+
220
+ function detectDesktopRuntime({ env, platform, homeDir, pathExists }) {
221
+ const candidates = desktopCandidates({ env, platform, homeDir });
222
+ const detected = candidates.find(({ applicationPath }) => pathExists(applicationPath));
223
+ if (!detected) return null;
224
+
225
+ if (detected.id === 'rancher-desktop') {
226
+ const rdctlPath = platform === 'win32'
227
+ ? join(dirname(detected.applicationPath), 'resources', 'resources', 'win32', 'bin',
228
+ 'rdctl.exe')
229
+ : join(homeDir, '.rd', 'bin', 'rdctl');
230
+ if (pathExists(rdctlPath)) {
231
+ return {
232
+ ...detected,
233
+ launchCommand: rdctlPath,
234
+ launchArgs: ['start']
235
+ };
236
+ }
237
+ }
238
+ if (platform === 'darwin') {
239
+ return {
240
+ ...detected,
241
+ launchCommand: 'open',
242
+ launchArgs: ['-a', detected.label]
243
+ };
244
+ }
245
+ return {
246
+ ...detected,
247
+ launchCommand: detected.applicationPath,
248
+ launchArgs: []
249
+ };
250
+ }
251
+
252
+ function desktopCandidates({ env, platform, homeDir }) {
253
+ if (platform === 'darwin') {
254
+ return [
255
+ {
256
+ id: 'rancher-desktop',
257
+ label: 'Rancher Desktop',
258
+ applicationPath: '/Applications/Rancher Desktop.app'
259
+ },
260
+ {
261
+ id: 'rancher-desktop',
262
+ label: 'Rancher Desktop',
263
+ applicationPath: join(homeDir, 'Applications', 'Rancher Desktop.app')
264
+ },
265
+ {
266
+ id: 'docker-desktop',
267
+ label: 'Docker Desktop',
268
+ applicationPath: '/Applications/Docker.app'
269
+ },
270
+ {
271
+ id: 'docker-desktop',
272
+ label: 'Docker Desktop',
273
+ applicationPath: join(homeDir, 'Applications', 'Docker.app')
274
+ }
275
+ ];
276
+ }
277
+ if (platform === 'win32') {
278
+ return [
279
+ env.LOCALAPPDATA && {
280
+ id: 'rancher-desktop',
281
+ label: 'Rancher Desktop',
282
+ applicationPath: join(env.LOCALAPPDATA, 'Programs', 'Rancher Desktop',
283
+ 'Rancher Desktop.exe')
284
+ },
285
+ env.ProgramFiles && {
286
+ id: 'rancher-desktop',
287
+ label: 'Rancher Desktop',
288
+ applicationPath: join(env.ProgramFiles, 'Rancher Desktop', 'Rancher Desktop.exe')
289
+ },
290
+ env.ProgramFiles && {
291
+ id: 'docker-desktop',
292
+ label: 'Docker Desktop',
293
+ applicationPath: join(env.ProgramFiles, 'Docker', 'Docker', 'Docker Desktop.exe')
294
+ }
295
+ ].filter(Boolean);
296
+ }
297
+ return [
298
+ {
299
+ id: 'rancher-desktop',
300
+ label: 'Rancher Desktop',
301
+ applicationPath: '/usr/bin/rancher-desktop'
302
+ },
303
+ {
304
+ id: 'rancher-desktop',
305
+ label: 'Rancher Desktop',
306
+ applicationPath: '/opt/rancher-desktop/rancher-desktop'
307
+ },
308
+ {
309
+ id: 'docker-desktop',
310
+ label: 'Docker Desktop',
311
+ applicationPath: '/usr/bin/docker-desktop'
312
+ }
313
+ ];
314
+ }
315
+
316
+ async function launchDesktopRuntime(desktop, { spawnProcess = spawn } = {}) {
317
+ await new Promise((resolve, reject) => {
318
+ const child = spawnProcess(desktop.launchCommand, desktop.launchArgs, {
319
+ detached: true,
320
+ windowsHide: true,
321
+ stdio: 'ignore'
322
+ });
323
+ child.once('error', reject);
324
+ child.once('spawn', () => {
325
+ child.unref();
326
+ resolve();
327
+ });
328
+ });
329
+ }
330
+
331
+ function missingRuntimeMessage(platform) {
332
+ if (platform === 'darwin') {
333
+ return `未检测到可用的 Docker Compose 容器运行时。请安装并首次打开 Rancher Desktop ` +
334
+ `(${RANCHER_DOWNLOAD_URL}) 或 Docker Desktop (${DOCKER_DOWNLOAD_URL}),` +
335
+ `然后重新运行 fl setup。`;
336
+ }
337
+ if (platform === 'win32') {
338
+ return `未检测到可用的 Docker Compose 容器运行时。请先启用 WSL 2,再安装并首次打开 ` +
339
+ `Rancher Desktop (${RANCHER_DOWNLOAD_URL}) 或 Docker Desktop ` +
340
+ `(${DOCKER_DOWNLOAD_URL}),然后重新运行 fl setup。`;
341
+ }
342
+ return `未检测到可用的 Docker Compose 容器运行时。请安装 Docker Engine + Compose v2,` +
343
+ `或 Rancher Desktop (${RANCHER_DOWNLOAD_URL}),然后重新运行 fl setup。`;
344
+ }
345
+
346
+ function defaultDockerAvailable(env) {
347
+ return dockerInfoIndicatesDaemon(runProcess('docker', DOCKER_INFO_ARGS, env));
348
+ }
349
+
350
+ function runProcess(command, args, env) {
351
+ return spawnSync(command, args, {
352
+ encoding: 'utf8',
353
+ env,
354
+ windowsHide: true,
355
+ stdio: ['ignore', 'pipe', 'pipe']
356
+ });
357
+ }
358
+
359
+ function safeProcessDetail(result, homeDir) {
360
+ const body = [result?.stderr, result?.stdout, result?.error?.message]
361
+ .filter(nonEmpty)
362
+ .join('\n')
363
+ .replaceAll(homeDir, '~')
364
+ .replace(/(authorization:\s*bearer\s+)\S+/gi, '$1[redacted]')
365
+ .replace(/((?:password|token|secret)[\w-]*\s*[=:]\s*)\S+/gi, '$1[redacted]')
366
+ .split(/\r?\n/)
367
+ .map((line) => line.trim())
368
+ .filter(Boolean)
369
+ .slice(-3)
370
+ .join(' ');
371
+ return body.slice(0, 600);
372
+ }
373
+
374
+ function detailSuffix(detail) {
375
+ return nonEmpty(detail) ? ` Docker 返回:${detail}` : '';
376
+ }
377
+
378
+ function processSucceeded(result) {
379
+ return result?.status === 0 && !result.error;
380
+ }
381
+
382
+ function nonEmpty(value) {
383
+ return typeof value === 'string' && value.trim().length > 0;
384
+ }
385
+
386
+ function delay(ms) {
387
+ return new Promise((resolve) => setTimeout(resolve, ms));
388
+ }
@@ -0,0 +1,61 @@
1
+ import { existsSync } from 'node:fs';
2
+
3
+ import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
4
+
5
+ export function connectCursor(agent, context, {
6
+ readConfig = readJsonFile,
7
+ writeConfig = writeJsonFileAtomic
8
+ } = {}) {
9
+ const current = readConfig(agent.configPath, {});
10
+ assertObject(current, 'Cursor MCP config');
11
+
12
+ const currentServers = current.mcpServers ?? {};
13
+ assertObject(currentServers, 'Cursor mcpServers');
14
+
15
+ writeConfig(agent.configPath, {
16
+ ...current,
17
+ mcpServers: {
18
+ ...currentServers,
19
+ fuli: {
20
+ command: context.nodePath,
21
+ args: [
22
+ context.mcpServerPath,
23
+ '--runtime-config',
24
+ context.runtimeConfigPath
25
+ ]
26
+ }
27
+ }
28
+ });
29
+
30
+ return { id: agent.id, label: agent.label, status: 'connected' };
31
+ }
32
+
33
+ export function disconnectCursor(agent, {
34
+ fileExists = existsSync,
35
+ readConfig = readJsonFile,
36
+ writeConfig = writeJsonFileAtomic
37
+ } = {}) {
38
+ if (!fileExists(agent.configPath)) {
39
+ return { id: agent.id, label: agent.label, status: 'not_connected' };
40
+ }
41
+ const current = readConfig(agent.configPath, {});
42
+ assertObject(current, 'Cursor MCP config');
43
+ const currentServers = current.mcpServers ?? {};
44
+ assertObject(currentServers, 'Cursor mcpServers');
45
+ if (!Object.hasOwn(currentServers, 'fuli')) {
46
+ return { id: agent.id, label: agent.label, status: 'not_connected' };
47
+ }
48
+
49
+ const { fuli: _removed, ...remainingServers } = currentServers;
50
+ writeConfig(agent.configPath, {
51
+ ...current,
52
+ mcpServers: remainingServers
53
+ });
54
+ return { id: agent.id, label: agent.label, status: 'disconnected' };
55
+ }
56
+
57
+ function assertObject(value, label) {
58
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
59
+ throw new TypeError(`${label} must be a JSON object`);
60
+ }
61
+ }