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,142 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+
4
+ import { DEFAULT_FULI_PORT } from '../defaults.js';
5
+
6
+ const MAX_CITATIONS = 1;
7
+ const DEFAULT_CONSOLE_URL = `http://127.0.0.1:${DEFAULT_FULI_PORT}`;
8
+
9
+ export function sourceConsoleUrl(runtimeConfigPath, {
10
+ readText = (path) => readFileSync(path, 'utf8')
11
+ } = {}) {
12
+ if (!runtimeConfigPath) return DEFAULT_CONSOLE_URL;
13
+ try {
14
+ const statePath = join(dirname(resolve(runtimeConfigPath)), 'graph-runtime-state.json');
15
+ const state = JSON.parse(readText(statePath));
16
+ return safeLoopbackOrigin(state?.url) ?? DEFAULT_CONSOLE_URL;
17
+ } catch {
18
+ return DEFAULT_CONSOLE_URL;
19
+ }
20
+ }
21
+
22
+ export function createFuliSourceMarker({
23
+ consoleUrl = DEFAULT_CONSOLE_URL,
24
+ facts = [],
25
+ entities = []
26
+ } = {}) {
27
+ const citations = uniqueCitations([
28
+ ...facts.map(factCitation),
29
+ ...entities.map(entityCitation)
30
+ ].filter(Boolean));
31
+ const status = citations.length ? 'matched' : 'no_match';
32
+ return {
33
+ required: true,
34
+ label: 'FULI 来源',
35
+ status,
36
+ count: citations.length,
37
+ leadMarkdown: sourceLeadMarkdown({ consoleUrl, citations }),
38
+ markdown: sourceMarkerMarkdown({ consoleUrl, citations })
39
+ };
40
+ }
41
+
42
+ function factCitation(fact) {
43
+ return citation({
44
+ scope: fact.scope,
45
+ spaceId: fact.spaceId ?? fact.space_id,
46
+ itemKind: 'relationship',
47
+ itemId: fact.id,
48
+ title: [fact.source_entity, fact.target_entity].filter(Boolean).join(' → ') ||
49
+ fact.fact || '关系知识'
50
+ });
51
+ }
52
+
53
+ function entityCitation(entity) {
54
+ return citation({
55
+ scope: entity.scope,
56
+ spaceId: entity.spaceId ?? entity.space_id,
57
+ itemKind: 'entity',
58
+ itemId: entity.id,
59
+ title: entity.name || entity.summary || '实体知识'
60
+ });
61
+ }
62
+
63
+ function citation({ scope, spaceId, itemKind, itemId, title }) {
64
+ if (!['personal', 'project'].includes(scope) || !spaceId || !itemId) return null;
65
+ return {
66
+ scope,
67
+ spaceId: String(spaceId),
68
+ itemKind,
69
+ itemId: String(itemId),
70
+ title: singleLine(title)
71
+ };
72
+ }
73
+
74
+ function uniqueCitations(citations) {
75
+ const seen = new Set();
76
+ return citations.filter(({ scope, spaceId, itemKind, itemId }) => {
77
+ const key = `${scope}:${spaceId}:${itemKind}:${itemId}`;
78
+ if (seen.has(key)) return false;
79
+ seen.add(key);
80
+ return true;
81
+ });
82
+ }
83
+
84
+ function sourceLeadMarkdown({ consoleUrl, citations }) {
85
+ if (!citations.length) {
86
+ const origin = safeLoopbackOrigin(consoleUrl) ?? DEFAULT_CONSOLE_URL;
87
+ return `**[◇ FULI · 已检索,未命中](${origin}/)**`;
88
+ }
89
+ return `**[🌠 FULI · 知识增强](${knowledgeUrl(consoleUrl, citations[0])})**`;
90
+ }
91
+
92
+ function sourceMarkerMarkdown({ consoleUrl, citations }) {
93
+ if (!citations.length) {
94
+ return [
95
+ '**FULI 来源 · 未命中**',
96
+ '',
97
+ `- [打开 Fuli](${safeLoopbackOrigin(consoleUrl) ?? DEFAULT_CONSOLE_URL}/)`
98
+ ].join('\n');
99
+ }
100
+ const shown = citations.slice(0, MAX_CITATIONS);
101
+ const hidden = citations.length - shown.length;
102
+ return [
103
+ `**FULI 来源 · ${citations.length} 条**`,
104
+ '',
105
+ ...shown.map((item) =>
106
+ `- [${markdownLabel(item.title)}](${knowledgeUrl(consoleUrl, item)})`
107
+ ),
108
+ ...(hidden ? [`- 另有 ${hidden} 条命中,可在 Fuli 中查看`] : [])
109
+ ].join('\n');
110
+ }
111
+
112
+ function knowledgeUrl(consoleUrl, { scope, spaceId, itemKind, itemId }) {
113
+ const origin = safeLoopbackOrigin(consoleUrl) ?? DEFAULT_CONSOLE_URL;
114
+ const segments = ['knowledge', scope, spaceId, itemKind, itemId]
115
+ .map((segment) => encodeURIComponent(segment));
116
+ return `${origin}/#/${segments.join('/')}`;
117
+ }
118
+
119
+ function safeLoopbackOrigin(value) {
120
+ try {
121
+ const url = new URL(value);
122
+ if (url.protocol !== 'http:') return null;
123
+ if (!['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname)) return null;
124
+ if (url.username || url.password) return null;
125
+ return url.origin;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ function singleLine(value) {
132
+ return String(value ?? '知识记录').replace(/[\r\n\t]+/g, ' ').trim().slice(0, 120) ||
133
+ '知识记录';
134
+ }
135
+
136
+ function markdownLabel(value) {
137
+ return singleLine(value)
138
+ .replaceAll('&', '&')
139
+ .replaceAll('<', '&lt;')
140
+ .replaceAll('>', '&gt;')
141
+ .replace(/([\\[\]])/g, '\\$1');
142
+ }
@@ -0,0 +1,133 @@
1
+ import { readJson, sendJson } from './response.js';
2
+ import { handleLensRequest } from './lens-route.js';
3
+ import { handleGraphApiRequest } from './graph-api-router.js';
4
+
5
+ export async function handleApiRequest({ request, response, app }) {
6
+ const url = new URL(request.url, 'http://127.0.0.1');
7
+
8
+ if (url.pathname === '/api/health' && request.method === 'GET') {
9
+ sendJson(response, 200, {
10
+ status: 'ready',
11
+ service: 'fuli-local-console',
12
+ pid: process.pid
13
+ });
14
+ return true;
15
+ }
16
+
17
+ if (app.graphiti) {
18
+ return handleGraphApiRequest({ request, response, url, app });
19
+ }
20
+
21
+ if (handleLensRequest({ request, response, url, app })) return true;
22
+
23
+ if (url.pathname === '/api/state' && request.method === 'GET') {
24
+ sendJson(response, 200, app.state());
25
+ return true;
26
+ }
27
+
28
+ if (url.pathname === '/api/spaces' && request.method === 'POST') {
29
+ const body = await readJson(request);
30
+ const space = app.createSpace(body.name, body.kind, body.description ?? null);
31
+ sendJson(response, 200, { space });
32
+ return true;
33
+ }
34
+
35
+ if (url.pathname === '/api/bootstrap' && request.method === 'POST') {
36
+ sendJson(response, 200, app.bootstrap());
37
+ return true;
38
+ }
39
+
40
+ if (url.pathname === '/api/subscriptions' && request.method === 'POST') {
41
+ const body = await readJson(request);
42
+ const subscription = app.subscribe(
43
+ body.personalSpaceId,
44
+ body.spaceId,
45
+ body.mode ?? 'latest'
46
+ );
47
+ sendJson(response, 200, { subscription });
48
+ return true;
49
+ }
50
+
51
+ if (url.pathname === '/api/remember' && request.method === 'POST') {
52
+ const body = await readJson(request);
53
+ const result = app.remember({
54
+ personalSpaceId: app.requireSpaceId({
55
+ id: body.personalSpaceId,
56
+ name: body.personalSpaceName,
57
+ label: 'Personal space'
58
+ }),
59
+ targetSpaceId: optionalSpaceId(app, {
60
+ id: body.targetSpaceId,
61
+ name: body.targetSpaceName,
62
+ label: 'Target space'
63
+ }),
64
+ sourceKind: body.sourceKind ?? 'agent',
65
+ body: body.body,
66
+ sourceUri: body.sourceUri ?? null
67
+ });
68
+ sendJson(response, 200, result);
69
+ return true;
70
+ }
71
+
72
+ if (url.pathname === '/api/observe/git-diff' && request.method === 'POST') {
73
+ const body = await readJson(request);
74
+ const result = app.observe({
75
+ personalSpaceId: body.personalSpaceId,
76
+ targetSpaceId: body.targetSpaceId || null,
77
+ cwd: body.cwd ?? process.cwd()
78
+ });
79
+ sendJson(response, 200, result);
80
+ return true;
81
+ }
82
+
83
+ const candidateDecisionMatch = url.pathname.match(/^\/api\/candidates\/([^/]+)\/decision$/);
84
+ if (candidateDecisionMatch && request.method === 'POST') {
85
+ const body = await readJson(request);
86
+ const candidate = app.decideCandidate(candidateDecisionMatch[1], body.decision);
87
+ sendJson(response, 200, { candidate });
88
+ return true;
89
+ }
90
+
91
+ if (url.pathname === '/api/search' && request.method === 'GET') {
92
+ const result = app.search({
93
+ personalSpaceId: requiredSpaceId(app, {
94
+ id: url.searchParams.get('personalSpaceId'),
95
+ name: url.searchParams.get('personalSpaceName'),
96
+ label: 'Personal space'
97
+ }),
98
+ query: url.searchParams.get('q') ?? '',
99
+ includeHistorical: url.searchParams.get('historical') === 'true'
100
+ });
101
+ sendJson(response, 200, result);
102
+ return true;
103
+ }
104
+
105
+ if (url.pathname === '/api/context-pack' && request.method === 'GET') {
106
+ const result = app.contextPack({
107
+ personalSpaceId: requiredSpaceId(app, {
108
+ id: url.searchParams.get('personalSpaceId'),
109
+ name: url.searchParams.get('personalSpaceName'),
110
+ label: 'Personal space'
111
+ }),
112
+ spaceId: requiredSpaceId(app, {
113
+ id: url.searchParams.get('spaceId'),
114
+ name: url.searchParams.get('spaceName'),
115
+ label: 'Space'
116
+ }),
117
+ query: url.searchParams.get('q') ?? ''
118
+ });
119
+ sendJson(response, 200, result);
120
+ return true;
121
+ }
122
+
123
+ return false;
124
+ }
125
+
126
+ function requiredSpaceId(app, input) {
127
+ return app.requireSpaceId(input);
128
+ }
129
+
130
+ function optionalSpaceId(app, input) {
131
+ if (!input.id && !input.name) return null;
132
+ return requiredSpaceId(app, input);
133
+ }
@@ -0,0 +1,19 @@
1
+ export function selectBoundedJsonArray(items, budget) {
2
+ const selected = [];
3
+ let usedBytes = Buffer.byteLength('[]', 'utf8');
4
+ let truncated = false;
5
+
6
+ for (const item of items) {
7
+ const serialized = JSON.stringify(item);
8
+ const itemBytes = Buffer.byteLength(serialized, 'utf8');
9
+ const nextBytes = usedBytes + itemBytes + (selected.length ? 1 : 0);
10
+ if (nextBytes > budget) {
11
+ truncated = true;
12
+ continue;
13
+ }
14
+ selected.push(item);
15
+ usedBytes = nextBytes;
16
+ }
17
+
18
+ return { items: selected, usedBytes, truncated };
19
+ }
@@ -0,0 +1,36 @@
1
+ import {
2
+ ApplicationError,
3
+ ApplicationErrorCode
4
+ } from '../app/application-error.js';
5
+ import { JsonBodyTooLargeError } from './response.js';
6
+ import { ProviderRequestError } from '../graphiti/provider-client.js';
7
+
8
+ const BAD_REQUEST_CODES = new Set([
9
+ ApplicationErrorCode.NOT_FOUND,
10
+ ApplicationErrorCode.VALIDATION
11
+ ]);
12
+
13
+ export function mapHttpError(error) {
14
+ if (error instanceof JsonBodyTooLargeError) {
15
+ return { status: 413, body: { error: 'Request body too large' } };
16
+ }
17
+
18
+ if (error instanceof SyntaxError) {
19
+ return { status: 400, body: { error: 'Malformed JSON' } };
20
+ }
21
+
22
+ if (error instanceof ApplicationError && BAD_REQUEST_CODES.has(error.code)) {
23
+ return { status: 400, body: { error: error.message } };
24
+ }
25
+
26
+ if (error instanceof ProviderRequestError) {
27
+ const status = error.status >= 400 && error.status <= 599 ? error.status : 502;
28
+ return { status, body: { error: error.message, code: error.code } };
29
+ }
30
+
31
+ if (error instanceof TypeError) {
32
+ return { status: 400, body: { error: error.message } };
33
+ }
34
+
35
+ return { status: 500, body: { error: 'Internal server error' } };
36
+ }
@@ -0,0 +1,257 @@
1
+ import { readJson, sendJson } from './response.js';
2
+
3
+ export async function handleGraphApiRequest({ request, response, url, app }) {
4
+ if (url.pathname === '/api/state' && request.method === 'GET') {
5
+ sendJson(response, 200, await app.state());
6
+ return true;
7
+ }
8
+ if (url.pathname === '/api/capture-policy' && request.method === 'GET') {
9
+ sendJson(response, 200, app.getCapturePolicy());
10
+ return true;
11
+ }
12
+ if (url.pathname === '/api/capture-policy' && request.method === 'PATCH') {
13
+ sendJson(response, 200, app.updateCapturePolicy(await readJson(request)));
14
+ return true;
15
+ }
16
+ if (url.pathname === '/api/agent-access-policy' && request.method === 'GET') {
17
+ sendJson(response, 200, app.getAgentAccessPolicy());
18
+ return true;
19
+ }
20
+ if (url.pathname === '/api/agent-access-policy' && request.method === 'PATCH') {
21
+ sendJson(response, 200, app.updateAgentAccessPolicy(await readJson(request)));
22
+ return true;
23
+ }
24
+ if (url.pathname === '/api/capture' && request.method === 'POST') {
25
+ sendJson(response, 200, await app.captureSessionKnowledge(await readJson(request)));
26
+ return true;
27
+ }
28
+ if (url.pathname === '/api/search' && request.method === 'GET') {
29
+ sendJson(response, 200, await app.searchKnowledge({
30
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
31
+ query: url.searchParams.get('q') ?? '',
32
+ projectIds: url.searchParams.getAll('projectId'),
33
+ personalProjectId: url.searchParams.get('personalProjectId'),
34
+ contextPersonalProjectIds: url.searchParams.getAll('contextPersonalProjectId'),
35
+ limit: numberParam(url, 'limit', 12),
36
+ includeHistorical: url.searchParams.get('historical') === 'true',
37
+ includePending: url.searchParams.get('pending') === 'true' ||
38
+ url.searchParams.get('exploratory') === 'true'
39
+ }));
40
+ return true;
41
+ }
42
+ if (url.pathname === '/api/graph' && request.method === 'GET') {
43
+ sendJson(response, 200, await app.getKnowledgeGraph({
44
+ spaceId: url.searchParams.get('spaceId'),
45
+ providerUrl: url.searchParams.get('providerUrl'),
46
+ personalProjectId: url.searchParams.get('personalProjectId'),
47
+ limit: numberParam(url, 'limit', 500)
48
+ }));
49
+ return true;
50
+ }
51
+ if (url.pathname === '/api/preference-conflicts' && request.method === 'GET') {
52
+ sendJson(response, 200, await app.listPreferenceConflicts({
53
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
54
+ status: url.searchParams.get('status'),
55
+ limit: numberParam(url, 'limit', 500)
56
+ }));
57
+ return true;
58
+ }
59
+ if (
60
+ url.pathname === '/api/preference-conflicts/defer'
61
+ && request.method === 'POST'
62
+ ) {
63
+ sendJson(response, 200, await app.deferPreferenceConflict({
64
+ ...(await readJson(request)),
65
+ operationActor: 'human'
66
+ }));
67
+ return true;
68
+ }
69
+ const completedPreferenceConflict = url.pathname.match(
70
+ /^\/api\/preference-conflicts\/([^/]+)\/complete$/
71
+ );
72
+ if (completedPreferenceConflict && request.method === 'POST') {
73
+ sendJson(response, 200, await app.completePreferenceConflict({
74
+ ...(await readJson(request)),
75
+ conflictId: decodeURIComponent(completedPreferenceConflict[1]),
76
+ operationActor: 'human'
77
+ }));
78
+ return true;
79
+ }
80
+ const assignment = url.pathname.match(
81
+ /^\/api\/knowledge\/(entity|relationship)\/([^/]+)\/assignment$/
82
+ );
83
+ if (assignment && request.method === 'POST') {
84
+ sendJson(response, 200, await app.reassignKnowledgeItem({
85
+ ...(await readJson(request)),
86
+ itemKind: assignment[1],
87
+ itemId: decodeURIComponent(assignment[2]),
88
+ operationActor: 'human'
89
+ }));
90
+ return true;
91
+ }
92
+ const preferenceScope = url.pathname.match(
93
+ /^\/api\/knowledge\/(entity|relationship)\/([^/]+)\/preference-scope$/
94
+ );
95
+ if (preferenceScope && request.method === 'POST') {
96
+ sendJson(response, 200, await app.setPersonalPreferenceScope({
97
+ ...(await readJson(request)),
98
+ itemKind: preferenceScope[1],
99
+ itemId: decodeURIComponent(preferenceScope[2]),
100
+ operationActor: 'human'
101
+ }));
102
+ return true;
103
+ }
104
+ const projectAction = url.pathname.match(
105
+ /^\/api\/knowledge\/(entity)\/([^/]+)\/project-action(\/preview)?$/
106
+ );
107
+ if (projectAction && request.method === 'POST') {
108
+ const body = await readJson(request);
109
+ const input = {
110
+ ...body,
111
+ itemKind: projectAction[1],
112
+ itemId: decodeURIComponent(projectAction[2]),
113
+ operationActor: 'human'
114
+ };
115
+ const result = projectAction[3]
116
+ ? await app.previewKnowledgeProjectAction(input)
117
+ : await app.applyKnowledgeProjectAction(input);
118
+ sendJson(response, 200, result);
119
+ return true;
120
+ }
121
+ if (url.pathname === '/api/knowledge/batch-confirmation' && request.method === 'POST') {
122
+ sendJson(
123
+ response,
124
+ 200,
125
+ await app.confirmKnowledgeBatch({
126
+ ...(await readJson(request)),
127
+ operationActor: 'human'
128
+ })
129
+ );
130
+ return true;
131
+ }
132
+ const knowledgeItem = url.pathname.match(
133
+ /^\/api\/knowledge\/(entity|relationship)\/([^/]+)$/
134
+ );
135
+ if (knowledgeItem && request.method === 'PATCH') {
136
+ sendJson(response, 200, await app.reviseKnowledgeItem({
137
+ ...(await readJson(request)),
138
+ itemKind: knowledgeItem[1],
139
+ itemId: decodeURIComponent(knowledgeItem[2]),
140
+ operationActor: 'human'
141
+ }));
142
+ return true;
143
+ }
144
+ if (url.pathname === '/api/subscriptions' && request.method === 'POST') {
145
+ sendJson(response, 200, await app.subscribePublicProject(await readJson(request)));
146
+ return true;
147
+ }
148
+ const subscription = url.pathname.match(/^\/api\/subscriptions\/([^/]+)$/);
149
+ if (subscription && request.method === 'DELETE') {
150
+ sendJson(response, 200, await app.unsubscribePublicProject({
151
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
152
+ projectId: decodeURIComponent(subscription[1]),
153
+ providerUrl: url.searchParams.get('providerUrl')
154
+ }));
155
+ return true;
156
+ }
157
+ if (url.pathname === '/api/personal-projects' && request.method === 'GET') {
158
+ sendJson(response, 200, await app.listPersonalProjects({
159
+ personalSpaceId: url.searchParams.get('personalSpaceId')
160
+ }));
161
+ return true;
162
+ }
163
+ if (url.pathname === '/api/personal-projects' && request.method === 'PUT') {
164
+ sendJson(response, 200, await app.upsertPersonalProject(await readJson(request)));
165
+ return true;
166
+ }
167
+ if (url.pathname === '/api/projects/publish' && request.method === 'POST') {
168
+ sendJson(response, 200, await app.publishPersonalProject(await readJson(request)));
169
+ return true;
170
+ }
171
+ const projectReleases = url.pathname.match(/^\/api\/projects\/([^/]+)\/releases$/);
172
+ if (projectReleases && request.method === 'GET') {
173
+ sendJson(response, 200, await app.listProjectReleases({
174
+ projectId: decodeURIComponent(projectReleases[1]),
175
+ providerUrl: url.searchParams.get('providerUrl')
176
+ }));
177
+ return true;
178
+ }
179
+ const publicProject = url.pathname.match(/^\/api\/projects\/([^/]+)$/);
180
+ if (publicProject && request.method === 'DELETE') {
181
+ sendJson(response, 200, await app.deletePublicProject({
182
+ projectId: decodeURIComponent(publicProject[1]),
183
+ providerUrl: url.searchParams.get('providerUrl')
184
+ }));
185
+ return true;
186
+ }
187
+ if (url.pathname === '/api/project-relations' && request.method === 'GET') {
188
+ sendJson(response, 200, await app.listProjectRelations({
189
+ projectId: url.searchParams.get('projectId'),
190
+ providerUrl: url.searchParams.get('providerUrl')
191
+ }));
192
+ return true;
193
+ }
194
+ if (url.pathname === '/api/project-relations' && request.method === 'POST') {
195
+ sendJson(response, 200, await app.createProjectRelation(await readJson(request)));
196
+ return true;
197
+ }
198
+ const relationDecision = url.pathname.match(
199
+ /^\/api\/project-relations\/([^/]+)\/decision$/
200
+ );
201
+ if (relationDecision && request.method === 'POST') {
202
+ const body = await readJson(request);
203
+ sendJson(response, 200, await app.reviewProjectRelation({
204
+ ...body,
205
+ relationId: decodeURIComponent(relationDecision[1])
206
+ }));
207
+ return true;
208
+ }
209
+ if (url.pathname === '/api/personal-review' && request.method === 'GET') {
210
+ sendJson(response, 200, await app.listPersonalReviewQueue({
211
+ personalSpaceId: url.searchParams.get('personalSpaceId'),
212
+ status: url.searchParams.get('status') ?? 'pending'
213
+ }));
214
+ return true;
215
+ }
216
+ const personalDecision = url.pathname.match(/^\/api\/personal-review\/([^/]+)\/decision$/);
217
+ if (personalDecision && request.method === 'POST') {
218
+ const body = await readJson(request);
219
+ sendJson(response, 200, await app.reviewPersonalDraft({
220
+ ...body,
221
+ draftId: decodeURIComponent(personalDecision[1])
222
+ }));
223
+ return true;
224
+ }
225
+ if (url.pathname === '/api/review' && request.method === 'GET') {
226
+ sendJson(response, 200, await app.listReviewQueue({
227
+ projectId: url.searchParams.get('projectId'),
228
+ providerUrl: url.searchParams.get('providerUrl'),
229
+ status: url.searchParams.get('status') ?? 'pending'
230
+ }));
231
+ return true;
232
+ }
233
+ const decision = url.pathname.match(/^\/api\/review\/([^/]+)\/decision$/);
234
+ if (decision && request.method === 'POST') {
235
+ const body = await readJson(request);
236
+ sendJson(response, 200, await app.reviewProposal({
237
+ ...body,
238
+ proposalId: decodeURIComponent(decision[1])
239
+ }));
240
+ return true;
241
+ }
242
+ if (url.pathname === '/api/graphiti/status' && request.method === 'GET') {
243
+ sendJson(response, 200, await app.getGraphitiStatus());
244
+ return true;
245
+ }
246
+ return false;
247
+ }
248
+
249
+ function numberParam(url, name, fallback) {
250
+ const raw = url.searchParams.get(name);
251
+ if (raw === null) return fallback;
252
+ const value = Number(raw);
253
+ if (!Number.isInteger(value) || value <= 0) {
254
+ throw new TypeError(`${name} must be a positive integer`);
255
+ }
256
+ return value;
257
+ }
@@ -0,0 +1,50 @@
1
+ import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
2
+ import { selectBoundedJsonArray } from './bounded-json-array.js';
3
+ import { sendJson } from './response.js';
4
+
5
+ const DEFAULT_BUDGET = 1200;
6
+ const MAX_BUDGET = 16384;
7
+
8
+ export function handleLensRequest({ request, response, url, app }) {
9
+ if (url.pathname !== '/api/lens' || request.method !== 'GET') return false;
10
+
11
+ const personalSpaceId = app.requireSpaceId({
12
+ id: url.searchParams.get('personalSpaceId'),
13
+ name: url.searchParams.get('personalSpaceName'),
14
+ label: 'Personal space'
15
+ });
16
+ const budget = parseBudget(url.searchParams.get('budget'));
17
+ const view = app.lens.getUserLensView({
18
+ personalSpaceId,
19
+ task: '',
20
+ budget,
21
+ includeObserved: true,
22
+ includeSuggested: false,
23
+ includeRestricted: false
24
+ });
25
+ const bounded = selectBoundedJsonArray(view.entries, budget);
26
+
27
+ sendJson(response, 200, {
28
+ personalSpaceId,
29
+ budget,
30
+ usedBytes: bounded.usedBytes,
31
+ truncated: view.truncated || bounded.truncated,
32
+ facts: bounded.items
33
+ });
34
+ return true;
35
+ }
36
+
37
+ function parseBudget(value) {
38
+ if (value === null) return DEFAULT_BUDGET;
39
+ if (!/^[1-9]\d*$/.test(value)) throw invalidBudget();
40
+ const budget = Number(value);
41
+ if (!Number.isSafeInteger(budget) || budget < 2 || budget > MAX_BUDGET) throw invalidBudget();
42
+ return budget;
43
+ }
44
+
45
+ function invalidBudget() {
46
+ return new ApplicationError(
47
+ ApplicationErrorCode.VALIDATION,
48
+ `Budget must be an integer between 2 and ${MAX_BUDGET}`
49
+ );
50
+ }
@@ -0,0 +1,41 @@
1
+ import { sendJson } from './response.js';
2
+
3
+ export function localServerAuthority(address) {
4
+ if (!address || typeof address === 'string') return null;
5
+ if (!Number.isSafeInteger(address.port) || address.port < 1 || address.port > 65535) {
6
+ return null;
7
+ }
8
+ return address.port === 80 ? '127.0.0.1' : `127.0.0.1:${address.port}`;
9
+ }
10
+
11
+ export function rejectDisallowedRequest({ request, response, authority }) {
12
+ const origin = request.headers.origin;
13
+ const fetchSite = request.headers['sec-fetch-site'];
14
+ if (
15
+ !authority ||
16
+ request.headers.host !== authority ||
17
+ (origin !== undefined && origin !== `http://${authority}`) ||
18
+ (typeof fetchSite === 'string' && fetchSite.toLowerCase() === 'cross-site')
19
+ ) {
20
+ sendJson(response, 403, { error: 'Forbidden' });
21
+ return true;
22
+ }
23
+
24
+ if (requiresJson(request) && !isJson(request.headers['content-type'])) {
25
+ sendJson(response, 415, { error: 'Unsupported media type' });
26
+ return true;
27
+ }
28
+
29
+ return false;
30
+ }
31
+
32
+ function requiresJson(request) {
33
+ if (request.method !== 'POST') return false;
34
+ const pathname = new URL(request.url, 'http://127.0.0.1').pathname;
35
+ return pathname.startsWith('/api/');
36
+ }
37
+
38
+ function isJson(contentType) {
39
+ if (typeof contentType !== 'string') return false;
40
+ return contentType.split(';', 1)[0].trim().toLowerCase() === 'application/json';
41
+ }