fuli-context 0.4.0 → 0.7.2

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 (207) hide show
  1. package/README.en.md +5 -0
  2. package/README.md +411 -173
  3. package/README.zh-CN.md +448 -0
  4. package/compose.graphiti.yml +6 -6
  5. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  6. package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
  7. package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
  8. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  10. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
  13. package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
  14. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
  15. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
  18. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
  19. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
  20. package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
  21. package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
  22. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  23. package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
  24. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  25. package/dist/web/assets/console-CbDsKrsO.js +1 -0
  26. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  27. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  28. package/dist/web/assets/i18n-BQcns1PH.js +5 -0
  29. package/dist/web/assets/identity-B8K4VYg6.js +1 -0
  30. package/dist/web/assets/index-CB8Xmsc2.js +2 -0
  31. package/dist/web/assets/index-CZeuPh-U.css +1 -0
  32. package/dist/web/assets/model-eF52GgPF.js +1 -0
  33. package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  35. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
  36. package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
  37. package/dist/web/index.html +9 -6
  38. package/docs/external-knowledge-architecture.md +279 -0
  39. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  40. package/graph-provider/fuli_graph/app.py +57 -1
  41. package/graph-provider/fuli_graph/graph_models.py +3 -0
  42. package/graph-provider/fuli_graph/graph_query.py +62 -16
  43. package/graph-provider/fuli_graph/knowledge_review.py +612 -0
  44. package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
  45. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  46. package/graph-provider/fuli_graph/models.py +19 -0
  47. package/graph-provider/fuli_graph/runtime.py +15 -0
  48. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  49. package/graph-provider/pyproject.toml +1 -1
  50. package/npm-shrinkwrap.json +5 -407
  51. package/package.json +7 -4
  52. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  53. package/skills/flreview/SKILL.md +73 -0
  54. package/skills/flreview/agents/openai.yaml +6 -0
  55. package/src/agent-tools/graph-definitions.js +79 -4
  56. package/src/agent-tools/graph-handlers.js +12 -0
  57. package/src/cli/command-registry.js +3 -3
  58. package/src/cli/local-runtime-command.js +60 -23
  59. package/src/cli/local-runtime-options.js +16 -6
  60. package/src/cli/setup-agent-selection.js +12 -12
  61. package/src/cli/setup-command.js +41 -18
  62. package/src/cli/shell-argument.js +10 -0
  63. package/src/cli/uninstall-command.js +17 -15
  64. package/src/cli/update-command.js +38 -30
  65. package/src/external-knowledge/catalog.js +61 -0
  66. package/src/external-knowledge/conflict-policy.js +58 -0
  67. package/src/external-knowledge/connected-search.js +166 -0
  68. package/src/external-knowledge/connectors/custom.js +133 -0
  69. package/src/external-knowledge/connectors/feishu.js +461 -0
  70. package/src/external-knowledge/connectors/mcp.js +438 -0
  71. package/src/external-knowledge/connectors/notion.js +307 -0
  72. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  73. package/src/external-knowledge/document-mapping.js +151 -0
  74. package/src/external-knowledge/graph-projection.js +17 -0
  75. package/src/external-knowledge/index.js +8 -0
  76. package/src/external-knowledge/registry.js +91 -0
  77. package/src/external-knowledge/runtime.js +68 -0
  78. package/src/external-knowledge/safe-diagnostic.js +14 -0
  79. package/src/external-knowledge/service.js +811 -0
  80. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  81. package/src/graphiti/federated-application.js +66 -41
  82. package/src/graphiti/knowledge-item-role.js +21 -0
  83. package/src/graphiti/knowledge-review.js +43 -0
  84. package/src/graphiti/project-profile-mapping.js +32 -0
  85. package/src/graphiti/provider-client.js +22 -1
  86. package/src/graphiti/source-marker.js +54 -3
  87. package/src/graphiti/task-knowledge-recall.js +250 -0
  88. package/src/http/api-router.js +23 -108
  89. package/src/http/external-knowledge-api-router.js +104 -0
  90. package/src/http/graph-api-router.js +12 -1
  91. package/src/http/request-policy.js +38 -3
  92. package/src/local-runtime/lifecycle.js +37 -16
  93. package/src/mcp/create-mcp-server.js +1 -0
  94. package/src/mcp/instructions.js +7 -7
  95. package/src/mcp/tool-annotations.js +6 -0
  96. package/src/mcp/tool-result.js +1 -0
  97. package/src/server/application-lifecycle.js +2 -11
  98. package/src/server/blocked-ports.js +11 -0
  99. package/src/server/lan-access.js +52 -0
  100. package/src/server.js +103 -25
  101. package/src/setup/agent-installation-status.js +2 -1
  102. package/src/setup/agents.js +2 -1
  103. package/src/setup/claude-code-config.js +2 -1
  104. package/src/setup/codex-bootstrap.js +12 -7
  105. package/src/setup/container-runtime.js +25 -21
  106. package/src/setup/graph-runtime.js +219 -58
  107. package/src/setup/node-runtime.js +3 -3
  108. package/src/setup/options.js +2 -4
  109. package/src/setup/paths.js +18 -3
  110. package/src/setup/review-skill-adapters.js +49 -0
  111. package/src/setup/setup.js +26 -2
  112. package/src/setup/uninstall.js +4 -2
  113. package/src/system/resource-monitor.js +317 -0
  114. package/src/system/runtime-settings.js +131 -0
  115. package/src/system/system-service.js +119 -0
  116. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  118. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +0 -1
  120. package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +0 -1
  121. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  122. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  123. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  124. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  125. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  126. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  127. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  128. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  129. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  130. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  131. package/dist/web/assets/index-BQRkOwPx.js +0 -2
  132. package/dist/web/assets/index-CpLDx3of.css +0 -1
  133. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  134. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  135. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  136. package/src/agent/candidate-read-service.js +0 -53
  137. package/src/agent/context-pack-service.js +0 -83
  138. package/src/agent/public-read-policy.js +0 -39
  139. package/src/agent/public-read-service.js +0 -69
  140. package/src/agent/read-projection.js +0 -107
  141. package/src/agent/search-service.js +0 -61
  142. package/src/agent/space-boundary.js +0 -33
  143. package/src/app/bootstrap.js +0 -8
  144. package/src/app/create-application.js +0 -143
  145. package/src/app/state-service.js +0 -38
  146. package/src/candidates.js +0 -78
  147. package/src/classifier.js +0 -63
  148. package/src/cli/command-arguments.js +0 -14
  149. package/src/context-pack.js +0 -206
  150. package/src/extractor.js +0 -77
  151. package/src/fact-history.js +0 -39
  152. package/src/facts/fact-service.js +0 -89
  153. package/src/http/bounded-json-array.js +0 -19
  154. package/src/http/lens-route.js +0 -50
  155. package/src/ingestion.js +0 -161
  156. package/src/lens/lens-correction-service.js +0 -81
  157. package/src/lens/lens-fact-writer.js +0 -85
  158. package/src/lens/lens-input.js +0 -62
  159. package/src/lens/lens-query-projection.js +0 -87
  160. package/src/lens/lens-query-ranking.js +0 -62
  161. package/src/lens/lens-query.js +0 -143
  162. package/src/lens/lens-resource-projection.js +0 -93
  163. package/src/lens/lens-resource-selection.js +0 -45
  164. package/src/lens/lens-resource-service.js +0 -103
  165. package/src/lens/lens-search-query.js +0 -122
  166. package/src/lens/lens-service.js +0 -183
  167. package/src/models.js +0 -44
  168. package/src/observer.js +0 -83
  169. package/src/project-rules.js +0 -33
  170. package/src/publication/outbox-service.js +0 -46
  171. package/src/publication/publication-envelope-schema.js +0 -75
  172. package/src/publication/publication-envelope.js +0 -37
  173. package/src/publication/publication-policy.js +0 -82
  174. package/src/publication/publication-service.js +0 -48
  175. package/src/router.js +0 -58
  176. package/src/runtime-options.js +0 -138
  177. package/src/security/episode-visibility.js +0 -14
  178. package/src/security/fact-visibility.js +0 -15
  179. package/src/security/source-metadata-policy.js +0 -8
  180. package/src/storage/episode-evidence-preview.js +0 -100
  181. package/src/storage/fact-search-order.js +0 -18
  182. package/src/storage/file/episode-evidence.js +0 -37
  183. package/src/storage/file/episode-index.js +0 -59
  184. package/src/storage/file/fact-search.js +0 -60
  185. package/src/storage/file-store.js +0 -437
  186. package/src/storage/migrate.js +0 -128
  187. package/src/storage/migrations/001-initial.sql +0 -83
  188. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  189. package/src/storage/record-validation.js +0 -46
  190. package/src/storage/snapshot-defaults.js +0 -54
  191. package/src/storage/sqlite/candidates-repository.js +0 -84
  192. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  193. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  194. package/src/storage/sqlite/episodes-repository.js +0 -52
  195. package/src/storage/sqlite/fact-search-repository.js +0 -162
  196. package/src/storage/sqlite/facts-repository.js +0 -146
  197. package/src/storage/sqlite/imports-repository.js +0 -39
  198. package/src/storage/sqlite/mapper.js +0 -84
  199. package/src/storage/sqlite/open-database.js +0 -15
  200. package/src/storage/sqlite/outbox-repository.js +0 -103
  201. package/src/storage/sqlite/snapshot-repository.js +0 -69
  202. package/src/storage/sqlite/spaces-repository.js +0 -52
  203. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  204. package/src/storage/sqlite-store.js +0 -159
  205. package/src/storage/stable-json.js +0 -24
  206. package/src/storage/store-port.js +0 -20
  207. package/src/store.js +0 -2
@@ -4,22 +4,30 @@ import { existsSync, unlinkSync } from 'node:fs';
4
4
  import {
5
5
  checkLocalConsoleHealth,
6
6
  ensureGraphRuntime,
7
- MANAGED_PERSONAL_PROVIDER_URL,
8
- MANAGED_WORKSPACE_PROVIDER_URL,
9
7
  stopGraphProviders
10
8
  } from '../setup/graph-runtime.js';
11
9
  import { readJsonFile } from '../storage/json-file.js';
10
+ import {
11
+ DEFAULT_RUNTIME_SETTINGS,
12
+ managedProviderUrls
13
+ } from '../system/runtime-settings.js';
12
14
 
13
15
  export async function startLocalRuntime(input, dependencies = {}) {
14
16
  const deps = lifecycleDependencies(dependencies);
17
+ const runtimeSettings = input.runtimeSettings ?? DEFAULT_RUNTIME_SETTINGS;
15
18
  const config = deps.readConfig(input.paths.graphRuntimeConfigPath);
16
- const managesDevelopmentWorkspace = hasManagedDevelopmentWorkspace(config);
19
+ const managesDevelopmentWorkspace = hasManagedDevelopmentWorkspace(
20
+ config,
21
+ runtimeSettings
22
+ );
17
23
  const buildProviders = input.rebuild === true || !config ||
18
24
  !deps.fileExists(input.paths.graphEnvPath);
19
25
  const runtime = await deps.ensureRuntime({
20
26
  paths: input.paths,
21
27
  personalSpaceName: input.personalSpaceName,
22
28
  port: input.port,
29
+ lan: input.lan === true,
30
+ runtimeSettings,
23
31
  personalOnly: !managesDevelopmentWorkspace,
24
32
  buildProviders,
25
33
  noStart: false,
@@ -36,12 +44,13 @@ export async function startLocalRuntime(input, dependencies = {}) {
36
44
 
37
45
  export async function stopLocalRuntime(input, dependencies = {}) {
38
46
  const deps = lifecycleDependencies(dependencies);
47
+ const runtimeSettings = input.runtimeSettings ?? DEFAULT_RUNTIME_SETTINGS;
39
48
  const config = deps.readConfig(input.paths.graphRuntimeConfigPath);
40
49
  const state = deps.readState(input.paths.graphRuntimeStatePath);
41
50
  const consoleResult = await stopRecordedConsole(state, input.paths, deps);
42
51
  const managesDevelopmentWorkspace = state?.managedProviders?.includes(
43
52
  'development-workspace'
44
- ) || hasManagedDevelopmentWorkspace(config);
53
+ ) || hasManagedDevelopmentWorkspace(config, runtimeSettings);
45
54
 
46
55
  let providers = 'not_initialized';
47
56
  if (deps.fileExists(input.paths.graphEnvPath)) {
@@ -63,7 +72,7 @@ export async function stopLocalRuntime(input, dependencies = {}) {
63
72
  export async function restartLocalRuntime(input, dependencies = {}) {
64
73
  const stopped = await stopLocalRuntime(input, dependencies);
65
74
  if (stopped.status === 'partial') {
66
- throw new Error('无法安全确认原本地界面的进程身份,已拒绝重启。');
75
+ throw new Error('Could not safely verify the existing local UI process; restart was refused.');
67
76
  }
68
77
  const started = await startLocalRuntime(input, dependencies);
69
78
  return { stopped, ...started, status: 'restarted' };
@@ -71,6 +80,7 @@ export async function restartLocalRuntime(input, dependencies = {}) {
71
80
 
72
81
  export async function inspectLocalRuntime(input, dependencies = {}) {
73
82
  const deps = lifecycleDependencies(dependencies);
83
+ const runtimeSettings = input.runtimeSettings ?? DEFAULT_RUNTIME_SETTINGS;
74
84
  const config = deps.readConfig(input.paths.graphRuntimeConfigPath);
75
85
  const state = deps.readState(input.paths.graphRuntimeStatePath);
76
86
  const stateLooksOwned = isRuntimeState(state) && isLoopbackConsoleUrl(state.url);
@@ -80,13 +90,14 @@ export async function inspectLocalRuntime(input, dependencies = {}) {
80
90
  state.pid,
81
91
  state.version
82
92
  );
83
- const personalUrl = config?.personal?.providerUrl ?? MANAGED_PERSONAL_PROVIDER_URL;
93
+ const managedUrls = managedProviderUrls(runtimeSettings);
94
+ const personalUrl = config?.personal?.providerUrl ?? managedUrls.personal;
84
95
  const personal = config
85
96
  ? await deps.providerHealth(personalUrl)
86
97
  : { url: personalUrl, status: 'not_configured' };
87
98
  const workspaces = await Promise.all((config?.workspaces ?? []).map(async (workspace) => ({
88
99
  ...(await deps.providerHealth(workspace.providerUrl)),
89
- managedDevelopment: isManagedDevelopmentUrl(workspace.providerUrl)
100
+ managedDevelopment: isManagedDevelopmentUrl(workspace, runtimeSettings)
90
101
  })));
91
102
 
92
103
  let status = 'stopped';
@@ -99,7 +110,10 @@ export async function inspectLocalRuntime(input, dependencies = {}) {
99
110
  console: {
100
111
  status: consoleReady ? 'ready' : processAlive ? 'unverified' : 'stopped',
101
112
  url: stateLooksOwned ? state.url : `http://127.0.0.1:${input.port}`,
102
- pid: processAlive ? state.pid : null
113
+ pid: processAlive ? state.pid : null,
114
+ ...(stateLooksOwned && state.lan === true
115
+ ? { lan: true, lanUrls: safeLanUrls(state.lanUrls) }
116
+ : {})
103
117
  },
104
118
  personal,
105
119
  public: {
@@ -116,7 +130,7 @@ export async function openLocalConsole(input, dependencies = {}) {
116
130
  if (!isRuntimeState(state) || !isLoopbackConsoleUrl(state.url) ||
117
131
  !deps.isProcessAlive(state.pid) ||
118
132
  !await deps.consoleHealth(state.url, state.pid, state.version)) {
119
- throw new Error('本地界面尚未运行,请先执行 fl start。');
133
+ throw new Error('The local UI is not running. Run fl start first.');
120
134
  }
121
135
  await deps.openExternal(state.url);
122
136
  return { status: 'opened', url: state.url };
@@ -197,17 +211,18 @@ export function externalOpenInvocation(url, platform = process.platform) {
197
211
  return { command: 'xdg-open', args: [url] };
198
212
  }
199
213
 
200
- function hasManagedDevelopmentWorkspace(config) {
201
- return (config?.workspaces ?? []).some(({ providerUrl }) =>
202
- isManagedDevelopmentUrl(providerUrl));
214
+ function hasManagedDevelopmentWorkspace(config, runtimeSettings) {
215
+ return (config?.workspaces ?? []).some((workspace) =>
216
+ isManagedDevelopmentUrl(workspace, runtimeSettings));
203
217
  }
204
218
 
205
- function isManagedDevelopmentUrl(value) {
219
+ function isManagedDevelopmentUrl(workspace, runtimeSettings) {
220
+ if (workspace?.managedDevelopment === true) return true;
206
221
  try {
207
- const url = new URL(value);
222
+ const url = new URL(workspace?.providerUrl);
208
223
  return url.protocol === 'http:' &&
209
224
  ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname) &&
210
- url.port === new URL(MANAGED_WORKSPACE_PROVIDER_URL).port;
225
+ [8788, runtimeSettings.ports.workspaceProvider].includes(Number(url.port));
211
226
  } catch {
212
227
  return false;
213
228
  }
@@ -233,10 +248,16 @@ function publicProviderStatus(workspaces) {
233
248
  }
234
249
 
235
250
  function isRuntimeState(state) {
236
- return (state?.version === 2 || state?.version === 3) &&
251
+ return (state?.version === 2 || state?.version === 3 || state?.version === 4) &&
237
252
  Number.isInteger(state.pid) && typeof state.url === 'string';
238
253
  }
239
254
 
255
+ function safeLanUrls(value) {
256
+ return Array.isArray(value)
257
+ ? value.filter((url) => typeof url === 'string' && url.startsWith('http://'))
258
+ : [];
259
+ }
260
+
240
261
  function isProcessAlive(pid) {
241
262
  try { process.kill(pid, 0); return true; } catch { return false; }
242
263
  }
@@ -20,6 +20,7 @@ const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
20
20
  begin_task_context: 16 * 1024,
21
21
  get_collaboration_preferences: 16 * 1024,
22
22
  search_knowledge_graph: 32 * 1024,
23
+ search_connected_knowledge: 64 * 1024,
23
24
  search_current_project_knowledge: 64 * 1024,
24
25
  discover_common_knowledge_candidates: 32 * 1024
25
26
  });
@@ -1,9 +1,9 @@
1
1
  export const MCP_INSTRUCTIONS = [
2
- "Each user task: if begin_task_context supplied hook-provided task context, apply. Otherwise call exactly get_collaboration_preferences with projectPath=cwd before other tools/answer as the fallback; 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 first. Never guess personalProjectId. Writes enforce preferences in the actual payload; final text is not compliance.",
3
- 'For current-project facts use search_current_project_knowledge: exact active child first, then inheritable parent knowledge via outgoing PART_OF/USES_KNOWLEDGE_FROM within two hops. Child stable keys override. Never inherit project preferences or traverse RELATED_TO.',
4
- 'Before claiming facts unknown, use search_knowledge_graph. Use exact IDs for extra projects; mark pending. Widen a location miss to all_local_confirmed only after consent, then use rg only in the safe current repo/workspace. Never scan outside it. Live URLs need deployment evidence; changing state needs monitoring/Git MCP.',
5
- 'Use record_decision_trace for confirmed choices/rationale/validation. Use record_knowledge_feedback when retained knowledge is rejected, contradicted, outdated, or fails validation; preserve history.',
6
- 'Batch durable confirmed knowledge via capture_session_knowledge in exact scope. Never store secrets, transcripts, or guesses. Only human/source confirmation permits public review. Finish a hook-managed task with checkpoint_task_knowledge once: capture_candidates or retain_nothing; fallback Agents likewise flush or retain nothing.',
7
- 'When personal knowledge materially affects the answer/action, call record_knowledge_usage. Retrieval/injection never counts; it creates at most agent_confirmed and is never public-eligible.',
8
- '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.'
2
+ "Each user task: apply hook-provided task context from begin_task_context. Otherwise, before other tools/answer, call exactly get_collaboration_preferences with projectPath=cwd and taskPrompt=current user request as fallback; never substitute a project action/Fuli tool. Apply all effective_preferences: personal-global everywhere plus only Fuli's matched project. Resolve relevant deferred_conflict first. Never guess personalProjectId. Writes enforce preferences in the actual payload; final text is not compliance.",
3
+ 'Before asking the user to repeat a stable project fact or method, inspect task_knowledge_recall. On miss use search_current_project_knowledge with 1-4 focused action, artifact, target-system, or identifier queries; never use the whole conversational request as the only query. Search the exact active child first, then inheritable parent PART_OF/USES_KNOWLEDGE_FROM knowledge within two hops; child stable keys override. Never inherit project preferences or traverse RELATED_TO.',
4
+ 'Before claiming facts unknown, use search_knowledge_graph. Use exact IDs for extra projects. Widen a miss to all_local_confirmed only after consent, then rg only in the safe current repo/workspace. Never scan outside it. Live state needs deployment/monitoring/Git MCP evidence. External/public evidence: search_connected_knowledge; agent_decide is response-only.',
5
+ 'Use record_decision_trace for confirmed choices and record_knowledge_feedback when knowledge is rejected, contradicted, outdated, or fails validation.',
6
+ 'Batch durable confirmed knowledge via capture_session_knowledge in exact scope; never secrets, transcripts, or guesses. Finish once with checkpoint_task_knowledge: capture_candidates or retain_nothing.',
7
+ 'After material personal-knowledge use call record_knowledge_usage; retrieval alone never counts or permits public use.',
8
+ 'Terminal-safe Markdown. Final MUST begin with sourceMarker.leadMarkdown; append sourceMarker.markdown unchanged on match. On miss use noMatchSourceMarker leadMarkdown only (empty markdown). Never wrap markers in HTML.'
9
9
  ].join(' ');
@@ -5,6 +5,7 @@ const READ_TOOLS = new Set([
5
5
  'preview_common_knowledge_promotion',
6
6
  'list_knowledge_spaces',
7
7
  'list_personal_projects',
8
+ 'list_knowledge_review_candidates',
8
9
  'preview_personal_project_action',
9
10
  'list_project_releases',
10
11
  'list_project_relations',
@@ -21,6 +22,7 @@ const WRITE_TOOLS = new Set([
21
22
  'capture_session_knowledge',
22
23
  'record_decision_trace',
23
24
  'search_knowledge_graph',
25
+ 'search_connected_knowledge',
24
26
  'record_knowledge_usage',
25
27
  'record_knowledge_feedback',
26
28
  'apply_common_knowledge_promotion',
@@ -28,6 +30,9 @@ const WRITE_TOOLS = new Set([
28
30
  'search_human_knowledge_changes',
29
31
  'review_human_knowledge_change',
30
32
  'upsert_personal_project',
33
+ 'start_knowledge_review',
34
+ 'record_knowledge_review_progress',
35
+ 'finish_knowledge_review',
31
36
  'revise_personal_knowledge',
32
37
  'reassign_personal_knowledge',
33
38
  'set_personal_preference_scope',
@@ -64,6 +69,7 @@ export function annotationsFor(name) {
64
69
  name === 'record_knowledge_feedback' ||
65
70
  name === 'checkpoint_task_knowledge',
66
71
  openWorldHint: name === 'capture_session_knowledge' ||
72
+ name === 'search_connected_knowledge' ||
67
73
  name === 'record_decision_trace' ||
68
74
  name === 'record_knowledge_feedback' ||
69
75
  name === 'checkpoint_task_knowledge' ||
@@ -4,6 +4,7 @@ const OMITTED_KEYS = new Set([
4
4
  'store',
5
5
  'dbpath',
6
6
  'databasepath',
7
+ 'privatepath',
7
8
  'snapshot',
8
9
  'fullsnapshot'
9
10
  ]);
@@ -1,21 +1,12 @@
1
- import { createApplication } from '../app/create-application.js';
2
1
  import { openFederatedGraphApplication } from '../graphiti/federated-application.js';
3
- import { openLocalApplication } from '../runtime-options.js';
4
2
 
5
3
  export function createServerApplication({
6
4
  app,
7
- store,
8
- dbPath,
9
5
  runtimeConfigPath,
10
- personalSpaceName,
11
6
  closeApplicationOnShutdown
12
7
  }) {
13
- const application = app ?? (store
14
- ? createApplication({ store, activePersonalSpaceName: personalSpaceName })
15
- : dbPath
16
- ? openLocalApplication({ dbPath, personalSpaceName })
17
- : openFederatedGraphApplication({ runtimeConfigPath }));
18
- const owned = closeApplicationOnShutdown ?? (!app && !store);
8
+ const application = app ?? openFederatedGraphApplication({ runtimeConfigPath });
9
+ const owned = closeApplicationOnShutdown ?? !app;
19
10
  return {
20
11
  application,
21
12
  close: closeOnce(() => {
@@ -0,0 +1,11 @@
1
+ const FETCH_BLOCKED_PORTS = new Set([
2
+ 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95,
3
+ 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389,
4
+ 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636,
5
+ 989, 990, 993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566,
6
+ 6665, 6666, 6667, 6668, 6669, 6697, 10080
7
+ ]);
8
+
9
+ export function isFetchBlockedPort(port) {
10
+ return FETCH_BLOCKED_PORTS.has(Number(port));
11
+ }
@@ -0,0 +1,52 @@
1
+ import { isIPv4 } from 'node:net';
2
+ import { networkInterfaces } from 'node:os';
3
+
4
+ export const LAN_ACCESS_USERNAME = 'fuli';
5
+
6
+ export function discoverLanAddresses(interfaces = networkInterfaces()) {
7
+ const addresses = [];
8
+ for (const entries of Object.values(interfaces ?? {})) {
9
+ for (const entry of entries ?? []) {
10
+ if (!entry || entry.internal || !isIpv4Family(entry.family)) continue;
11
+ if (!isPrivateLanIpv4(entry.address)) continue;
12
+ addresses.push(entry.address);
13
+ }
14
+ }
15
+ return [...new Set(addresses)].sort(compareIpv4);
16
+ }
17
+
18
+ export function lanConsoleUrls(addresses, port) {
19
+ return addresses.map((address) => `http://${consoleAuthority(address, port)}`);
20
+ }
21
+
22
+ export function lanServerAuthorities(addresses, port) {
23
+ return lanConsoleUrls(addresses, port).map((url) => new URL(url).host);
24
+ }
25
+
26
+ function isIpv4Family(family) {
27
+ return family === 'IPv4' || family === 4;
28
+ }
29
+
30
+ function isPrivateLanIpv4(address) {
31
+ if (!isIPv4(address)) return false;
32
+ const [first, second] = address.split('.').map(Number);
33
+ return first === 10 ||
34
+ (first === 172 && second >= 16 && second <= 31) ||
35
+ (first === 192 && second === 168) ||
36
+ (first === 169 && second === 254);
37
+ }
38
+
39
+ function consoleAuthority(address, port) {
40
+ return Number(port) === 80 ? address : `${address}:${port}`;
41
+ }
42
+
43
+ function compareIpv4(left, right) {
44
+ const leftParts = left.split('.').map(Number);
45
+ const rightParts = right.split('.').map(Number);
46
+ for (let index = 0; index < 4; index += 1) {
47
+ if (leftParts[index] !== rightParts[index]) {
48
+ return leftParts[index] - rightParts[index];
49
+ }
50
+ }
51
+ return 0;
52
+ }
package/src/server.js CHANGED
@@ -1,59 +1,94 @@
1
1
  #!/usr/bin/env node
2
2
  import { createServer as createHttpServer } from 'node:http';
3
+ import { dirname } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
 
5
6
  import { handleApiRequest } from './http/api-router.js';
6
7
  import { mapHttpError } from './http/error-mapping.js';
7
- import { localServerAuthority, rejectDisallowedRequest } from './http/request-policy.js';
8
+ import { localServerAuthority, rejectRequestOutsidePolicy } from './http/request-policy.js';
8
9
  import { sendJson } from './http/response.js';
9
10
  import { serveStatic } from './http/static-handler.js';
10
11
  import { DEFAULT_FULI_PORT } from './defaults.js';
11
12
  import { resolveGraphRuntimeOptions } from './graphiti/runtime-config.js';
13
+ import { isFetchBlockedPort } from './server/blocked-ports.js';
14
+ import {
15
+ discoverLanAddresses,
16
+ lanConsoleUrls,
17
+ lanServerAuthorities
18
+ } from './server/lan-access.js';
12
19
  import { createServerApplication } from './server/application-lifecycle.js';
13
20
  import { listenServer } from './server/listen.js';
21
+ import { resolveSetupPaths } from './setup/paths.js';
22
+ import { createSystemService } from './system/system-service.js';
14
23
 
15
- const FETCH_BLOCKED_PORTS = new Set([
16
- 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
17
- 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
18
- 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
19
- 995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
20
- 6669, 6697, 10080
21
- ]);
24
+ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
22
25
 
23
26
  export async function createServer(options = {}) {
24
27
  const {
25
28
  port = DEFAULT_FULI_PORT,
26
- store,
27
29
  app,
28
30
  closeApplicationOnShutdown,
29
- isBlockedPort = isFetchBlockedPort
31
+ isBlockedPort = isFetchBlockedPort,
32
+ lan = false,
33
+ lanAccessToken = null,
34
+ lanAddresses: configuredLanAddresses = null
30
35
  } = options;
36
+ const lanEnabled = lan === true;
37
+ const lanToken = lanEnabled ? requiredLanToken(lanAccessToken) : null;
38
+ const lanAddresses = lanEnabled
39
+ ? [...new Set(configuredLanAddresses ?? discoverLanAddresses())]
40
+ : [];
41
+ if (lanEnabled && lanAddresses.length === 0) {
42
+ throw new Error('No private IPv4 LAN address is available');
43
+ }
31
44
  if (Number(port) !== 0 && isBlockedPort(port)) {
32
45
  throw new Error(`Port ${port} is blocked for local fetch`);
33
46
  }
34
- const legacyRuntime = app || store || Object.hasOwn(options, 'dbPath');
35
- const localOptions = legacyRuntime
36
- ? { dbPath: options.dbPath, personalSpaceName: options.personalSpaceName ?? '我' }
37
- : resolveServerGraphOptions(options);
47
+ const localOptions = app ? {} : resolveServerGraphOptions(options);
38
48
  const runtime = createServerApplication({
39
49
  app,
40
- store,
41
50
  ...localOptions,
42
51
  closeApplicationOnShutdown
43
52
  });
44
53
  const application = runtime.application;
54
+ const externalKnowledge = options.externalKnowledge ??
55
+ application.externalKnowledge ?? null;
56
+ const connectedKnowledge = options.connectedKnowledge ??
57
+ application.connectedKnowledge ?? null;
58
+ const system = options.system ?? (!app
59
+ ? createSystemService({
60
+ paths: resolveSetupPaths({
61
+ dataDir: dirname(localOptions.runtimeConfigPath),
62
+ packageRoot: PACKAGE_ROOT
63
+ }),
64
+ packageRoot: PACKAGE_ROOT,
65
+ activePort: port,
66
+ activeLan: lanEnabled
67
+ })
68
+ : null);
45
69
 
46
70
  for (let attempt = 0; attempt < 20; attempt += 1) {
47
71
  let authority = null;
72
+ let allowedLanAuthorities = [];
48
73
  const server = createHttpServer((request, response) => {
49
- handleRequest({ request, response, app: application, authority }).catch((error) => {
74
+ handleRequest({
75
+ request,
76
+ response,
77
+ app: application,
78
+ system,
79
+ externalKnowledge,
80
+ connectedKnowledge,
81
+ authority,
82
+ lanAuthorities: allowedLanAuthorities,
83
+ lanAccessToken: lanToken
84
+ }).catch((error) => {
50
85
  const mapped = mapHttpError(error);
51
86
  sendJson(response, mapped.status, mapped.body);
52
87
  });
53
88
  });
54
89
 
55
90
  try {
56
- await listenServer(server, port);
91
+ await listenServer(server, port, lanEnabled ? '0.0.0.0' : '127.0.0.1');
57
92
  } catch (error) {
58
93
  runtime.close();
59
94
  throw error;
@@ -66,6 +101,9 @@ export async function createServer(options = {}) {
66
101
  throw new Error(`Port ${address.port} is blocked for local fetch`);
67
102
  }
68
103
  authority = localServerAuthority(address);
104
+ allowedLanAuthorities = lanEnabled
105
+ ? lanServerAuthorities(lanAddresses, address.port)
106
+ : [];
69
107
  if (!authority) {
70
108
  await new Promise((resolveClose) => server.close(resolveClose));
71
109
  runtime.close();
@@ -73,7 +111,12 @@ export async function createServer(options = {}) {
73
111
  }
74
112
 
75
113
  server.once('close', runtime.close);
76
- return { server, url: `http://${authority}`, app: application };
114
+ return {
115
+ server,
116
+ url: `http://${authority}`,
117
+ lanUrls: lanEnabled ? lanConsoleUrls(lanAddresses, address.port) : [],
118
+ app: application
119
+ };
77
120
  }
78
121
 
79
122
  runtime.close();
@@ -90,13 +133,34 @@ function resolveServerGraphOptions(options) {
90
133
  });
91
134
  }
92
135
 
93
- export function isFetchBlockedPort(port) {
94
- return FETCH_BLOCKED_PORTS.has(Number(port));
95
- }
136
+ export { isFetchBlockedPort } from './server/blocked-ports.js';
96
137
 
97
- async function handleRequest({ request, response, app, authority }) {
98
- if (rejectDisallowedRequest({ request, response, authority })) return;
99
- if (await handleApiRequest({ request, response, app })) return;
138
+ async function handleRequest({
139
+ request,
140
+ response,
141
+ app,
142
+ system,
143
+ externalKnowledge,
144
+ connectedKnowledge,
145
+ authority,
146
+ lanAuthorities,
147
+ lanAccessToken
148
+ }) {
149
+ if (rejectRequestOutsidePolicy({
150
+ request,
151
+ response,
152
+ authority,
153
+ lanAuthorities,
154
+ lanAccessToken
155
+ })) return;
156
+ if (await handleApiRequest({
157
+ request,
158
+ response,
159
+ app,
160
+ system,
161
+ externalKnowledge,
162
+ connectedKnowledge
163
+ })) return;
100
164
  serveStatic(new URL(request.url, 'http://127.0.0.1').pathname, response);
101
165
  }
102
166
 
@@ -110,7 +174,13 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
110
174
  async function runProgram(args) {
111
175
  const runtimeOptions = resolveGraphRuntimeOptions(args, process.env);
112
176
  const port = numberOption(args, '--port', DEFAULT_FULI_PORT);
113
- const { server, url } = await createServer({ ...runtimeOptions, port });
177
+ const lan = args.includes('--lan');
178
+ const { server, url, lanUrls } = await createServer({
179
+ ...runtimeOptions,
180
+ port,
181
+ lan,
182
+ lanAccessToken: lan ? process.env.FULI_LAN_ACCESS_TOKEN : null
183
+ });
114
184
  const handlers = new Map();
115
185
  const close = () => server.close();
116
186
  for (const signal of ['SIGINT', 'SIGTERM']) {
@@ -122,6 +192,14 @@ async function runProgram(args) {
122
192
  for (const [signal, handler] of handlers) process.off(signal, handler);
123
193
  });
124
194
  console.log(`复利工作台 running at ${url}`);
195
+ for (const lanUrl of lanUrls) console.log(`复利工作台 LAN at ${lanUrl}`);
196
+ }
197
+
198
+ function requiredLanToken(value) {
199
+ if (typeof value !== 'string' || value.length < 16) {
200
+ throw new TypeError('LAN mode requires a generated access token');
201
+ }
202
+ return value;
125
203
  }
126
204
 
127
205
  function numberOption(args, flag, fallback) {
@@ -17,7 +17,8 @@ export function inspectAgentInstallations(agents, context, {
17
17
  const registration = inspectRegistration(agent, context, { readText, readJson });
18
18
  const skillPairs = [
19
19
  [context.sessionSkillPath, agent.skillPath],
20
- [context.projectSkillPath, agent.projectSkillPath]
20
+ [context.projectSkillPath, agent.projectSkillPath],
21
+ [context.reviewSkillPath, agent.reviewSkillPath]
21
22
  ];
22
23
  const skillsPresent = skillPairs.some(([, targetPath]) => fileExists(targetPath));
23
24
  const skillsCurrent = skillPairs.every(
@@ -9,6 +9,7 @@ import {
9
9
  } from './claude-code-config.js';
10
10
  import { connectCursor, disconnectCursor } from './cursor-config.js';
11
11
  import { connectCodex, disconnectCodex } from './codex-config.js';
12
+ import { adaptAgentForReviewSkill } from './review-skill-adapters.js';
12
13
 
13
14
  export function discoverAgents({
14
15
  platform = process.platform,
@@ -52,7 +53,7 @@ export function discoverAgents({
52
53
  projectSkillPath: pathApi.join(homeDir, '.cursor', 'skills', projectSkillName),
53
54
  available: Boolean(commandExists('cursor'))
54
55
  }
55
- ];
56
+ ].map((agent) => adaptAgentForReviewSkill(agent, { homeDir, pathApi }));
56
57
  }
57
58
 
58
59
  export function buildAgentCommands(agent, context) {
@@ -115,7 +115,8 @@ function withManagedHooks(settings) {
115
115
  ...(hooks.UserPromptSubmit ?? []),
116
116
  managedHookGroup('begin_task_context', {
117
117
  sessionId: '${session_id}',
118
- projectPath: '${cwd}'
118
+ projectPath: '${cwd}',
119
+ taskPrompt: '${prompt}'
119
120
  }, 'Loading Fuli task context')
120
121
  ];
121
122
  hooks.Stop = [
@@ -9,18 +9,23 @@ import {
9
9
  } from 'node:fs';
10
10
  import { basename, dirname, join } from 'node:path';
11
11
 
12
+ import { codexReviewCommandBridge } from './review-skill-adapters.js';
13
+
12
14
  const BLOCK_START = '<!-- BEGIN FULI MANAGED INSTRUCTIONS -->';
13
15
  const BLOCK_END = '<!-- END FULI MANAGED INSTRUCTIONS -->';
14
16
 
15
17
  const GLOBAL_INSTRUCTIONS = `${BLOCK_START}
16
18
  At the start of every user task, before any other tool or answer, call exactly
17
- \`get_collaboration_preferences\` with \`projectPath\`=current working directory. Never substitute
18
- project action or Fuli tool.
19
- Apply all returned \`effective_preferences\` before answers or tools. For write tools, enforce
20
- them in the actual payload; mentioning them later is not compliance.
21
- Before using a relevant \`deferred_conflict\`, call
22
- \`resolve_deferred_preference_conflict\`; ignore unrelated ones.
23
- The current request wins on conflict.
19
+ \`get_collaboration_preferences\` with \`projectPath\`=current working directory and
20
+ \`taskPrompt\`=current user request; never substitute a project action/Fuli tool. Fuli uses both
21
+ transiently and never stores or returns them. Apply all returned \`effective_preferences\`.
22
+ Before asking the user to repeat a stable project fact or method, inspect
23
+ \`task_knowledge_recall\`. On miss call \`search_current_project_knowledge\` with 1-4 focused
24
+ action/artifact/target/identifier queries; never use the full request as the only query.
25
+ For write tools enforce them in the actual payload; mentioning them later is not compliance.
26
+ Relevant \`deferred_conflict\`: call \`resolve_deferred_preference_conflict\` first; ignore others.
27
+ Current request wins.
28
+ ${codexReviewCommandBridge()}
24
29
  ${BLOCK_END}`;
25
30
 
26
31
  export function installCodexBootstrap(agent, _context, {