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,254 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, unlinkSync } from 'node:fs';
3
+
4
+ import {
5
+ checkLocalConsoleHealth,
6
+ ensureGraphRuntime,
7
+ MANAGED_PERSONAL_PROVIDER_URL,
8
+ MANAGED_WORKSPACE_PROVIDER_URL,
9
+ stopGraphProviders
10
+ } from '../setup/graph-runtime.js';
11
+ import { readJsonFile } from '../storage/json-file.js';
12
+
13
+ export async function startLocalRuntime(input, dependencies = {}) {
14
+ const deps = lifecycleDependencies(dependencies);
15
+ const config = deps.readConfig(input.paths.graphRuntimeConfigPath);
16
+ const managesDevelopmentWorkspace = hasManagedDevelopmentWorkspace(config);
17
+ const buildProviders = input.rebuild === true || !config ||
18
+ !deps.fileExists(input.paths.graphEnvPath);
19
+ const runtime = await deps.ensureRuntime({
20
+ paths: input.paths,
21
+ personalSpaceName: input.personalSpaceName,
22
+ port: input.port,
23
+ personalOnly: !managesDevelopmentWorkspace,
24
+ buildProviders,
25
+ noStart: false,
26
+ env: input.env,
27
+ onProgress: input.onProgress
28
+ });
29
+
30
+ if (input.open === true) await deps.openExternal(runtime.url);
31
+ return {
32
+ ...runtime,
33
+ managesDevelopmentWorkspace
34
+ };
35
+ }
36
+
37
+ export async function stopLocalRuntime(input, dependencies = {}) {
38
+ const deps = lifecycleDependencies(dependencies);
39
+ const config = deps.readConfig(input.paths.graphRuntimeConfigPath);
40
+ const state = deps.readState(input.paths.graphRuntimeStatePath);
41
+ const consoleResult = await stopRecordedConsole(state, input.paths, deps);
42
+ const managesDevelopmentWorkspace = state?.managedProviders?.includes(
43
+ 'development-workspace'
44
+ ) || hasManagedDevelopmentWorkspace(config);
45
+
46
+ let providers = 'not_initialized';
47
+ if (deps.fileExists(input.paths.graphEnvPath)) {
48
+ deps.stopProviders(input.paths, input.paths.graphEnvPath, {
49
+ personalOnly: !managesDevelopmentWorkspace
50
+ });
51
+ providers = 'stopped';
52
+ }
53
+
54
+ const partial = consoleResult === 'unverified' || consoleResult === 'still_running';
55
+ return {
56
+ status: partial ? 'partial' : 'stopped',
57
+ console: consoleResult,
58
+ providers,
59
+ managesDevelopmentWorkspace
60
+ };
61
+ }
62
+
63
+ export async function restartLocalRuntime(input, dependencies = {}) {
64
+ const stopped = await stopLocalRuntime(input, dependencies);
65
+ if (stopped.status === 'partial') {
66
+ throw new Error('无法安全确认原本地界面的进程身份,已拒绝重启。');
67
+ }
68
+ const started = await startLocalRuntime(input, dependencies);
69
+ return { stopped, ...started, status: 'restarted' };
70
+ }
71
+
72
+ export async function inspectLocalRuntime(input, dependencies = {}) {
73
+ const deps = lifecycleDependencies(dependencies);
74
+ const config = deps.readConfig(input.paths.graphRuntimeConfigPath);
75
+ const state = deps.readState(input.paths.graphRuntimeStatePath);
76
+ const stateLooksOwned = isRuntimeState(state) && isLoopbackConsoleUrl(state.url);
77
+ const processAlive = stateLooksOwned && deps.isProcessAlive(state.pid);
78
+ const consoleReady = processAlive && await deps.consoleHealth(
79
+ state.url,
80
+ state.pid,
81
+ state.version
82
+ );
83
+ const personalUrl = config?.personal?.providerUrl ?? MANAGED_PERSONAL_PROVIDER_URL;
84
+ const personal = config
85
+ ? await deps.providerHealth(personalUrl)
86
+ : { url: personalUrl, status: 'not_configured' };
87
+ const workspaces = await Promise.all((config?.workspaces ?? []).map(async (workspace) => ({
88
+ ...(await deps.providerHealth(workspace.providerUrl)),
89
+ managedDevelopment: isManagedDevelopmentUrl(workspace.providerUrl)
90
+ })));
91
+
92
+ let status = 'stopped';
93
+ if (!config && !state) status = 'not_configured';
94
+ else if (consoleReady && personal.status === 'ready') status = 'running';
95
+ else if (processAlive || personal.status === 'ready') status = 'degraded';
96
+
97
+ return {
98
+ status,
99
+ console: {
100
+ status: consoleReady ? 'ready' : processAlive ? 'unverified' : 'stopped',
101
+ url: stateLooksOwned ? state.url : `http://127.0.0.1:${input.port}`,
102
+ pid: processAlive ? state.pid : null
103
+ },
104
+ personal,
105
+ public: {
106
+ configured: workspaces.length > 0,
107
+ status: publicProviderStatus(workspaces),
108
+ providers: workspaces
109
+ }
110
+ };
111
+ }
112
+
113
+ export async function openLocalConsole(input, dependencies = {}) {
114
+ const deps = lifecycleDependencies(dependencies);
115
+ const state = deps.readState(input.paths.graphRuntimeStatePath);
116
+ if (!isRuntimeState(state) || !isLoopbackConsoleUrl(state.url) ||
117
+ !deps.isProcessAlive(state.pid) ||
118
+ !await deps.consoleHealth(state.url, state.pid, state.version)) {
119
+ throw new Error('本地界面尚未运行,请先执行 fl start。');
120
+ }
121
+ await deps.openExternal(state.url);
122
+ return { status: 'opened', url: state.url };
123
+ }
124
+
125
+ async function stopRecordedConsole(state, paths, deps) {
126
+ if (!isRuntimeState(state)) return 'not_running';
127
+ if (!deps.isProcessAlive(state.pid)) {
128
+ deps.removeState(paths.graphRuntimeStatePath);
129
+ return 'not_running';
130
+ }
131
+ if (!isLoopbackConsoleUrl(state.url) ||
132
+ !await deps.consoleHealth(state.url, state.pid, state.version)) {
133
+ return 'unverified';
134
+ }
135
+
136
+ deps.stopProcess(state.pid);
137
+ if (!await deps.waitForExit(state.pid)) return 'still_running';
138
+ deps.removeState(paths.graphRuntimeStatePath);
139
+ return 'stopped';
140
+ }
141
+
142
+ function lifecycleDependencies(overrides) {
143
+ return {
144
+ readConfig: (path) => readJsonFile(path, null),
145
+ readState: safeReadState,
146
+ fileExists: existsSync,
147
+ ensureRuntime: ensureGraphRuntime,
148
+ stopProviders: stopGraphProviders,
149
+ consoleHealth: checkLocalConsoleHealth,
150
+ providerHealth,
151
+ isProcessAlive,
152
+ stopProcess,
153
+ waitForExit,
154
+ removeState: (path) => {
155
+ if (existsSync(path)) unlinkSync(path);
156
+ },
157
+ openExternal,
158
+ ...overrides
159
+ };
160
+ }
161
+
162
+ function safeReadState(path) {
163
+ try {
164
+ return readJsonFile(path, null);
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ async function providerHealth(url) {
171
+ try {
172
+ const response = await fetch(`${String(url).replace(/\/$/, '')}/health`, {
173
+ signal: AbortSignal.timeout(2500)
174
+ });
175
+ if (!response.ok) return { url, status: 'unavailable' };
176
+ const body = await response.json();
177
+ return { url, status: body?.status === 'ready' ? 'ready' : 'unavailable' };
178
+ } catch {
179
+ return { url, status: 'unavailable' };
180
+ }
181
+ }
182
+
183
+ async function openExternal(url) {
184
+ const invocation = externalOpenInvocation(url);
185
+ const child = spawn(invocation.command, invocation.args, {
186
+ detached: true,
187
+ windowsHide: true,
188
+ stdio: 'ignore'
189
+ });
190
+ child.unref();
191
+ }
192
+
193
+ export function externalOpenInvocation(url, platform = process.platform) {
194
+ if (!isLoopbackConsoleUrl(url)) throw new TypeError('Only the local Fuli console can be opened');
195
+ if (platform === 'darwin') return { command: 'open', args: [url] };
196
+ if (platform === 'win32') return { command: 'cmd', args: ['/c', 'start', '', url] };
197
+ return { command: 'xdg-open', args: [url] };
198
+ }
199
+
200
+ function hasManagedDevelopmentWorkspace(config) {
201
+ return (config?.workspaces ?? []).some(({ providerUrl }) =>
202
+ isManagedDevelopmentUrl(providerUrl));
203
+ }
204
+
205
+ function isManagedDevelopmentUrl(value) {
206
+ try {
207
+ const url = new URL(value);
208
+ return url.protocol === 'http:' &&
209
+ ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname) &&
210
+ url.port === new URL(MANAGED_WORKSPACE_PROVIDER_URL).port;
211
+ } catch {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ function isLoopbackConsoleUrl(value) {
217
+ try {
218
+ const url = new URL(value);
219
+ return url.protocol === 'http:' &&
220
+ ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname) &&
221
+ url.username === '' && url.password === '' &&
222
+ (url.pathname === '/' || url.pathname === '') && !url.search && !url.hash;
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+
228
+ function publicProviderStatus(workspaces) {
229
+ if (!workspaces.length) return 'not_connected';
230
+ const ready = workspaces.filter(({ status }) => status === 'ready').length;
231
+ if (ready === workspaces.length) return 'ready';
232
+ return ready > 0 ? 'degraded' : 'unavailable';
233
+ }
234
+
235
+ function isRuntimeState(state) {
236
+ return (state?.version === 2 || state?.version === 3) &&
237
+ Number.isInteger(state.pid) && typeof state.url === 'string';
238
+ }
239
+
240
+ function isProcessAlive(pid) {
241
+ try { process.kill(pid, 0); return true; } catch { return false; }
242
+ }
243
+
244
+ function stopProcess(pid) {
245
+ try { process.kill(pid, 'SIGTERM'); } catch { /* already stopped */ }
246
+ }
247
+
248
+ async function waitForExit(pid) {
249
+ for (let attempt = 0; attempt < 50; attempt += 1) {
250
+ if (!isProcessAlive(pid)) return true;
251
+ await new Promise((resolve) => setTimeout(resolve, 100));
252
+ }
253
+ return false;
254
+ }
@@ -0,0 +1,85 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
+
4
+ import { callAgentTool, listAgentTools } from '../agent-tools.js';
5
+ import { FULI_VERSION } from '../package-metadata.js';
6
+ import { MCP_INSTRUCTIONS } from './instructions.js';
7
+ import { createProjectActionPreviewTokens } from './project-action-preview-tokens.js';
8
+ import { annotationsFor } from './tool-annotations.js';
9
+ import { errorToolResult, protocolErrorResult, successToolResult } from './tool-result.js';
10
+ import { jsonSchemaToZod, openObjectSchema } from './tool-schema.js';
11
+
12
+ const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
13
+ get_collaboration_preferences: 16 * 1024,
14
+ search_knowledge_graph: 32 * 1024
15
+ });
16
+
17
+ export function createMcpServer(app) {
18
+ const server = new McpServer(
19
+ { name: 'fuli', version: FULI_VERSION },
20
+ { instructions: MCP_INSTRUCTIONS }
21
+ );
22
+
23
+ const tools = createToolMap(app, createProjectActionPreviewTokens());
24
+ for (const tool of tools.values()) registerTool(server, tool);
25
+ registerCallHandler(server, tools);
26
+ return server;
27
+ }
28
+
29
+ function createToolMap(app, projectActionPreviews) {
30
+ return new Map(listAgentTools().map((definition) => [definition.name, {
31
+ definition,
32
+ schema: jsonSchemaToZod(definition.inputSchema),
33
+ invoke: (input) => invokeAgentTool(
34
+ app,
35
+ definition.name,
36
+ input,
37
+ projectActionPreviews
38
+ )
39
+ }]));
40
+ }
41
+
42
+ function registerTool(server, tool) {
43
+ const { definition, schema } = tool;
44
+ server.registerTool(definition.name, {
45
+ title: definition.title,
46
+ description: definition.description,
47
+ inputSchema: schema,
48
+ outputSchema: openObjectSchema(),
49
+ annotations: annotationsFor(definition.name)
50
+ }, (input) => invokeTool(tool, input));
51
+ }
52
+
53
+ function registerCallHandler(server, tools) {
54
+ server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
55
+ const tool = tools.get(request.params.name);
56
+ if (!tool) return errorToolResult(new Error('Unknown tool'));
57
+ const parsed = await tool.schema.safeParseAsync(request.params.arguments ?? {});
58
+ if (!parsed.success) return protocolErrorResult('Input validation error');
59
+ return invokeTool(tool, parsed.data);
60
+ });
61
+ }
62
+
63
+ async function invokeTool(tool, input) {
64
+ try {
65
+ return successToolResult(await tool.invoke(input), {
66
+ limitBytes: TOOL_RESULT_LIMIT_BYTES[tool.definition.name]
67
+ });
68
+ } catch (error) {
69
+ return errorToolResult(error);
70
+ }
71
+ }
72
+
73
+ async function invokeAgentTool(app, name, input, projectActionPreviews) {
74
+ if (name === 'preview_personal_project_action') {
75
+ const result = await callAgentTool(app, name, input);
76
+ return {
77
+ ...result,
78
+ previewToken: projectActionPreviews.issue(input)
79
+ };
80
+ }
81
+ if (name === 'apply_personal_project_action') {
82
+ projectActionPreviews.consume(input.previewToken, input);
83
+ }
84
+ return callAgentTool(app, name, input);
85
+ }
@@ -0,0 +1,10 @@
1
+ export const MCP_INSTRUCTIONS = [
2
+ "Each user task: call exactly get_collaboration_preferences before other tools/answer with projectPath=cwd; never substitute a project action or Fuli tool. Apply all effective_preferences: personal-global everywhere plus only Fuli's matched project. Resolve relevant deferred_conflict via resolve_deferred_preference_conflict; ignore others. Pending is search-only. Never guess personalProjectId. Writes must enforce preferences in the actual payload; final text is not compliance.",
3
+ 'Before claiming URLs, routes, requirements, prior decisions, or runbooks are unknown, call search_knowledge_graph; resolve IDs via list_knowledge_spaces/list_personal_projects.',
4
+ 'Search personal-global profile, exact personalProjectId, inheritable knowledge reached via PART_OF or USES_KNOWLEDGE_FROM, named contextPersonalProjectIds, and relevant subscribed public project IDs. Never inherit project preferences or traverse RELATED_TO. Pending results stay marked. On a location miss, ask to widen to all registered local personal projects; use all_local_confirmed only after explicit confirmation. It never expands public projects.',
5
+ 'After a confirmed all-local miss, search current repository or workspace files with Grep or rg. Never scan user home, filesystem root, outside the workspace, credentials, browser data, or unrelated repositories. Require deployment evidence for live URLs.',
6
+ 'Changing/live: use monitoring or Git MCP',
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
+ '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.'
10
+ ].join(' ');
@@ -0,0 +1,72 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { ApplicationError } from '../app/application-error.js';
4
+
5
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
6
+
7
+ export function createProjectActionPreviewTokens({
8
+ now = Date.now,
9
+ createToken = randomUUID,
10
+ ttlMs = DEFAULT_TTL_MS
11
+ } = {}) {
12
+ const previews = new Map();
13
+
14
+ return Object.freeze({
15
+ issue(input) {
16
+ pruneExpired(previews, now());
17
+ const token = createToken();
18
+ previews.set(token, {
19
+ expiresAt: now() + ttlMs,
20
+ intent: projectActionIntent(input)
21
+ });
22
+ return token;
23
+ },
24
+
25
+ consume(token, input) {
26
+ if (!token) {
27
+ throw new ApplicationError(
28
+ 'preview_required',
29
+ 'Preview this exact project action before applying it'
30
+ );
31
+ }
32
+ const preview = previews.get(token);
33
+ if (!preview || preview.expiresAt <= now()) {
34
+ previews.delete(token);
35
+ throw new ApplicationError(
36
+ 'preview_expired',
37
+ 'Project action preview authorization is missing or expired'
38
+ );
39
+ }
40
+ if (preview.intent !== projectActionIntent(input)) {
41
+ throw new ApplicationError(
42
+ 'preview_mismatch',
43
+ 'Project action no longer matches its preview'
44
+ );
45
+ }
46
+ previews.delete(token);
47
+ }
48
+ });
49
+ }
50
+
51
+ function projectActionIntent(input) {
52
+ return JSON.stringify({
53
+ personalSpaceId: input.personalSpaceId,
54
+ itemKind: input.itemKind,
55
+ itemId: input.itemId,
56
+ mode: input.mode,
57
+ targetProjectId: input.targetProjectId ?? null,
58
+ newProjectId: input.newProjectId ?? null,
59
+ newProjectName: input.newProjectName ?? null,
60
+ newProjectPurpose: input.newProjectPurpose ?? null,
61
+ keepSourceRelation: input.keepSourceRelation ?? true,
62
+ relationType: input.relationType ?? 'RELATED_TO',
63
+ conflictResolution: input.conflictResolution ?? 'defer',
64
+ reason: input.reason
65
+ });
66
+ }
67
+
68
+ function pruneExpired(previews, currentTime) {
69
+ for (const [token, preview] of previews) {
70
+ if (preview.expiresAt <= currentTime) previews.delete(token);
71
+ }
72
+ }
@@ -0,0 +1,71 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+
3
+ import { openFederatedGraphApplication } from '../graphiti/federated-application.js';
4
+ import { createMcpServer } from './create-mcp-server.js';
5
+
6
+ export async function runStdio({ runtimeConfigPath }) {
7
+ const app = openFederatedGraphApplication({ runtimeConfigPath });
8
+ const server = createServerOrClose(app);
9
+ const transport = new StdioServerTransport();
10
+ const signalHandlers = new Map();
11
+ const close = createCloseOnce({
12
+ closeServer: () => server.close(),
13
+ closeApplication: () => app.close(),
14
+ afterClose: () => removeHandlers(signalHandlers)
15
+ });
16
+ const quietClose = createQuietCloser(close, () => { process.exitCode = 1; });
17
+
18
+ server.server.onclose = quietClose;
19
+ for (const signal of ['SIGINT', 'SIGTERM']) {
20
+ const handler = quietClose;
21
+ signalHandlers.set(signal, handler);
22
+ process.once(signal, handler);
23
+ }
24
+ process.stdin.once('end', quietClose);
25
+
26
+ try {
27
+ await server.connect(transport);
28
+ } catch (error) {
29
+ await close();
30
+ throw error;
31
+ }
32
+ }
33
+
34
+ export function createServerOrClose(app, factory = createMcpServer) {
35
+ try {
36
+ return factory(app);
37
+ } catch (error) {
38
+ void app.close();
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ export function createCloseOnce({ closeServer, closeApplication, afterClose = () => {} }) {
44
+ let closing;
45
+ return () => {
46
+ if (!closing) {
47
+ closing = Promise.resolve().then(async () => {
48
+ try {
49
+ await closeServer();
50
+ } finally {
51
+ try {
52
+ await closeApplication();
53
+ } finally {
54
+ afterClose();
55
+ }
56
+ }
57
+ });
58
+ }
59
+ return closing;
60
+ };
61
+ }
62
+
63
+ export function createQuietCloser(close, onFailure = () => {}) {
64
+ return () => {
65
+ close().catch(onFailure);
66
+ };
67
+ }
68
+
69
+ function removeHandlers(signalHandlers) {
70
+ for (const [signal, handler] of signalHandlers) process.off(signal, handler);
71
+ }
@@ -0,0 +1,63 @@
1
+ const READ_TOOLS = new Set([
2
+ 'list_knowledge_spaces',
3
+ 'list_personal_projects',
4
+ 'preview_personal_project_action',
5
+ 'list_project_releases',
6
+ 'list_project_relations',
7
+ 'list_personal_review_queue',
8
+ 'list_project_review_queue',
9
+ 'get_graphiti_status'
10
+ ]);
11
+
12
+ const WRITE_TOOLS = new Set([
13
+ 'get_collaboration_preferences',
14
+ 'resolve_deferred_preference_conflict',
15
+ 'capture_session_knowledge',
16
+ 'search_knowledge_graph',
17
+ 'record_knowledge_usage',
18
+ 'get_knowledge_graph',
19
+ 'search_human_knowledge_changes',
20
+ 'review_human_knowledge_change',
21
+ 'upsert_personal_project',
22
+ 'revise_personal_knowledge',
23
+ 'reassign_personal_knowledge',
24
+ 'set_personal_preference_scope',
25
+ 'apply_personal_project_action',
26
+ 'publish_personal_project',
27
+ 'create_project_relation',
28
+ 'review_project_relation',
29
+ 'review_personal_draft',
30
+ 'subscribe_public_project',
31
+ 'unsubscribe_public_project',
32
+ 'review_project_proposal'
33
+ ]);
34
+
35
+ const DESTRUCTIVE_TOOLS = new Set([
36
+ 'resolve_deferred_preference_conflict',
37
+ 'review_project_proposal',
38
+ 'review_project_relation',
39
+ 'review_personal_draft',
40
+ 'unsubscribe_public_project'
41
+ ]);
42
+
43
+ export function annotationsFor(name) {
44
+ if (!READ_TOOLS.has(name) && !WRITE_TOOLS.has(name)) {
45
+ throw new TypeError(`Missing MCP annotations for tool: ${name}`);
46
+ }
47
+ const readOnly = READ_TOOLS.has(name);
48
+ return {
49
+ readOnlyHint: readOnly,
50
+ destructiveHint: DESTRUCTIVE_TOOLS.has(name),
51
+ idempotentHint: readOnly || name === 'upsert_personal_project' ||
52
+ name === 'publish_personal_project' || name === 'unsubscribe_public_project' ||
53
+ name === 'record_knowledge_usage',
54
+ openWorldHint: name === 'capture_session_knowledge' ||
55
+ name === 'publish_personal_project' ||
56
+ name === 'create_project_relation' ||
57
+ name === 'review_project_relation' ||
58
+ name === 'review_personal_draft' ||
59
+ name === 'subscribe_public_project' ||
60
+ name === 'unsubscribe_public_project' ||
61
+ name === 'review_project_proposal'
62
+ };
63
+ }