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,206 @@
1
+ import { isCurrentFact } from './models.js';
2
+ import { listCandidates } from './candidates.js';
3
+ import { getProjectRules } from './project-rules.js';
4
+ import { getFactHistory } from './fact-history.js';
5
+
6
+ export function buildContextPack(store, { personalSpaceId, spaceId, query = '' }) {
7
+ const personalSpace = store.getSpace(personalSpaceId);
8
+ const space = store.getSpace(spaceId);
9
+ const scopedSpaceIds = [...new Set([personalSpaceId, spaceId])];
10
+ const matches = store
11
+ .searchFacts(scopedSpaceIds, query, { includeHistorical: false })
12
+ .map((fact) => compactMatch(store, fact));
13
+ const histories = buildHistories(store, matches);
14
+ const rules = compactRules(getProjectRules(store, spaceId), store);
15
+ const candidates = listCandidates(store, personalSpaceId)
16
+ .filter((candidate) => !candidate.targetSpaceId || candidate.targetSpaceId === spaceId)
17
+ .map(compactCandidate);
18
+
19
+ return {
20
+ query,
21
+ personalSpace: compactSpace(personalSpace, personalSpaceId),
22
+ space: compactSpace(space, spaceId),
23
+ rules,
24
+ matches,
25
+ histories,
26
+ candidateCount: candidates.length,
27
+ candidates,
28
+ answer: formatPackAnswer(matches)
29
+ };
30
+ }
31
+
32
+ export function formatContextPack(pack) {
33
+ const lines = [
34
+ `Context ${pack.personalSpace.name} -> ${pack.space.name}`,
35
+ `Query ${pack.query || '(empty)'}`,
36
+ 'Rules',
37
+ ...formatRules(pack.rules),
38
+ 'Matches',
39
+ ...formatMatches(pack.matches),
40
+ ...formatHistories(pack.histories),
41
+ `Candidates ${pack.candidateCount}`
42
+ ];
43
+
44
+ return lines.join('\n');
45
+ }
46
+
47
+ function compactRules(rules, store) {
48
+ return {
49
+ spaceId: rules.spaceId,
50
+ spaceName: rules.spaceName,
51
+ forbidden: rules.forbidden.map((fact) => compactFact(store, fact)),
52
+ parameters: rules.parameters.map((fact) => compactFact(store, fact)),
53
+ links: rules.links.map((fact) => compactFact(store, fact))
54
+ };
55
+ }
56
+
57
+ function compactFact(store, fact) {
58
+ return {
59
+ id: fact.id,
60
+ spaceId: fact.spaceId,
61
+ spaceName: store.getSpace(fact.spaceId)?.name ?? fact.spaceId,
62
+ subject: fact.subject,
63
+ predicate: fact.predicate,
64
+ object: fact.object,
65
+ current: isCurrentFact(fact),
66
+ validAt: fact.validAt,
67
+ invalidAt: fact.invalidAt,
68
+ source: compactSource(store, fact.sourceEpisodeId)
69
+ };
70
+ }
71
+
72
+ function compactMatch(store, fact) {
73
+ const compact = compactFact(store, fact);
74
+ return {
75
+ spaceId: compact.spaceId,
76
+ spaceName: compact.spaceName,
77
+ current: compact.current,
78
+ fact: {
79
+ id: compact.id,
80
+ spaceId: compact.spaceId,
81
+ subject: compact.subject,
82
+ predicate: compact.predicate,
83
+ object: compact.object,
84
+ validAt: compact.validAt,
85
+ invalidAt: compact.invalidAt
86
+ },
87
+ source: compact.source
88
+ };
89
+ }
90
+
91
+ function buildHistories(store, matches) {
92
+ const keys = new Set();
93
+ const histories = [];
94
+
95
+ for (const match of matches) {
96
+ const key = `${match.fact.spaceId}:${match.fact.predicate}`;
97
+ if (keys.has(key)) continue;
98
+ keys.add(key);
99
+
100
+ const history = getFactHistory(store, {
101
+ spaceId: match.fact.spaceId,
102
+ predicate: match.fact.predicate
103
+ });
104
+ if (history.facts.length <= 1) continue;
105
+
106
+ histories.push({
107
+ spaceId: history.spaceId,
108
+ spaceName: history.spaceName,
109
+ predicate: history.predicate,
110
+ facts: history.facts.map((fact) => compactFact(store, fact))
111
+ });
112
+ }
113
+
114
+ return histories;
115
+ }
116
+
117
+ function compactCandidate(candidate) {
118
+ return {
119
+ id: candidate.id,
120
+ reason: candidate.reason,
121
+ createdAt: candidate.createdAt,
122
+ targetSpaceId: candidate.targetSpaceId,
123
+ targetSpaceName: candidate.targetSpaceName,
124
+ source: candidate.episode
125
+ ? {
126
+ id: candidate.episode.id,
127
+ kind: candidate.episode.sourceKind,
128
+ uri: candidate.episode.sourceUri,
129
+ createdAt: candidate.episode.createdAt,
130
+ preview: firstLine(candidate.episode.body)
131
+ }
132
+ : null
133
+ };
134
+ }
135
+
136
+ function compactSource(store, episodeId) {
137
+ const episode = store.getEpisode(episodeId);
138
+ if (!episode) return null;
139
+
140
+ return {
141
+ id: episode.id,
142
+ kind: episode.sourceKind,
143
+ uri: episode.sourceUri,
144
+ createdAt: episode.createdAt,
145
+ preview: firstLine(episode.body)
146
+ };
147
+ }
148
+
149
+ function compactSpace(space, fallbackId) {
150
+ return {
151
+ id: space?.id ?? fallbackId,
152
+ name: space?.name ?? fallbackId,
153
+ kind: space?.kind ?? null
154
+ };
155
+ }
156
+
157
+ function firstLine(value) {
158
+ return String(value).split(/\r?\n/)[0].slice(0, 160);
159
+ }
160
+
161
+ function formatPackAnswer(matches) {
162
+ if (matches.length === 0) {
163
+ return 'No matching current facts found.';
164
+ }
165
+
166
+ return matches
167
+ .map((match) => `${match.fact.subject} ${match.fact.predicate} ${match.fact.object}`)
168
+ .join('\n');
169
+ }
170
+
171
+ function formatRules(rules) {
172
+ const lines = [
173
+ ...rules.forbidden.map((fact) => `forbids ${fact.object}`),
174
+ ...rules.parameters.map((fact) => `${humanPredicate(fact.predicate)} ${fact.object}`),
175
+ ...rules.links.map((fact) => `url ${fact.object}`)
176
+ ];
177
+
178
+ return lines.length ? lines : ['(none)'];
179
+ }
180
+
181
+ function formatMatches(matches) {
182
+ return matches.length
183
+ ? matches.map(
184
+ (match) =>
185
+ `${match.spaceName} ${humanPredicate(match.fact.predicate)} ${match.fact.object}`
186
+ )
187
+ : ['(none)'];
188
+ }
189
+
190
+ function formatHistories(histories) {
191
+ if (!histories?.length) return [];
192
+
193
+ return [
194
+ 'History',
195
+ ...histories.flatMap((history) =>
196
+ history.facts.map(
197
+ (fact) =>
198
+ `${fact.current ? 'current' : 'historical'} ${humanPredicate(history.predicate)} ${fact.object}`
199
+ )
200
+ )
201
+ ];
202
+ }
203
+
204
+ function humanPredicate(predicate) {
205
+ return predicate.startsWith('has_') ? predicate.slice(4) : predicate;
206
+ }
@@ -0,0 +1 @@
1
+ export const DEFAULT_FULI_PORT = 2727;
@@ -0,0 +1,77 @@
1
+ const URL_PATTERN = /https?:\/\/[^\s"',。))]+/g;
2
+
3
+ export function extractFactSpecs(subject, body) {
4
+ const lines = body
5
+ .split(/\r?\n/)
6
+ .map((line) => line.trim())
7
+ .filter(Boolean);
8
+ const specs = [];
9
+
10
+ for (const line of lines) {
11
+ const replacement = parseReplacement(subject, line);
12
+ if (replacement) {
13
+ specs.push(replacement);
14
+ continue;
15
+ }
16
+
17
+ const forbidden = parseForbidden(subject, line);
18
+ if (forbidden) {
19
+ specs.push(forbidden);
20
+ continue;
21
+ }
22
+
23
+ const keyValue = parseKeyValue(subject, line);
24
+ if (keyValue) {
25
+ specs.push(keyValue);
26
+ continue;
27
+ }
28
+
29
+ for (const url of line.match(URL_PATTERN) ?? []) {
30
+ specs.push({ kind: 'fact', subject, predicate: 'has_url', object: url });
31
+ }
32
+ }
33
+
34
+ return specs;
35
+ }
36
+
37
+ function parseReplacement(subject, line) {
38
+ const match = line.match(/^替代[::]\s*(.+?)\s*=>\s*(.+)$/);
39
+ if (!match) return null;
40
+
41
+ return {
42
+ kind: 'replacement',
43
+ subject,
44
+ oldValue: match[1].trim(),
45
+ newValue: match[2].trim()
46
+ };
47
+ }
48
+
49
+ function parseForbidden(subject, line) {
50
+ const match =
51
+ line.match(/^禁止[::]\s*(.+)$/) ??
52
+ line.match(/^(?:这个项目|本项目|项目)?\s*(?:禁止|不要|不允许)\s*(?:使用|采用|引入|用)?\s*(.+)$/);
53
+ if (!match) return null;
54
+
55
+ return {
56
+ kind: 'fact',
57
+ subject,
58
+ predicate: 'forbids',
59
+ object: cleanForbiddenObject(match[1])
60
+ };
61
+ }
62
+
63
+ function cleanForbiddenObject(value) {
64
+ return value.trim().replace(/[。.!!,,;;]+$/g, '').trim();
65
+ }
66
+
67
+ function parseKeyValue(subject, line) {
68
+ const match = line.match(/^([\p{L}\p{N}_.-]+)\s*[::]\s*(.+)$/u);
69
+ if (!match) return null;
70
+
71
+ return {
72
+ kind: 'fact',
73
+ subject,
74
+ predicate: `has_${match[1].trim()}`,
75
+ object: match[2].trim()
76
+ };
77
+ }
@@ -0,0 +1,39 @@
1
+ import { isCurrentFact } from './models.js';
2
+
3
+ export function getFactHistory(store, { spaceId, predicate }) {
4
+ const normalizedPredicate = normalizePredicate(predicate);
5
+ const facts = store.listFacts({ includeHistorical: true })
6
+ .filter((fact) => fact.spaceId === spaceId && fact.predicate === normalizedPredicate)
7
+ .sort((left, right) => left.validAt.localeCompare(right.validAt))
8
+ .map((fact) => enrichFact(store, fact));
9
+
10
+ return {
11
+ spaceId,
12
+ spaceName: store.getSpace(spaceId)?.name ?? spaceId,
13
+ predicate: normalizedPredicate,
14
+ facts
15
+ };
16
+ }
17
+
18
+ export function normalizePredicate(value) {
19
+ if (value.startsWith('has_') || value === 'forbids') return value;
20
+ return `has_${value}`;
21
+ }
22
+
23
+ function enrichFact(store, fact) {
24
+ const episode = store.getEpisode(fact.sourceEpisodeId);
25
+
26
+ return {
27
+ ...fact,
28
+ current: isCurrentFact(fact),
29
+ source: episode
30
+ ? {
31
+ id: episode.id,
32
+ kind: episode.sourceKind,
33
+ uri: episode.sourceUri,
34
+ body: episode.body,
35
+ createdAt: episode.createdAt
36
+ }
37
+ : null
38
+ };
39
+ }
@@ -0,0 +1,89 @@
1
+ import { FactScope, FactStatus } from '../models.js';
2
+
3
+ export class FactService {
4
+ constructor(store) {
5
+ this.store = store;
6
+ }
7
+
8
+ writeSpecs({ spaceId, subject, episodeId, specs, scope = FactScope.PERSONAL }) {
9
+ const written = [];
10
+ for (const spec of specs) {
11
+ if (spec.kind === 'replacement') {
12
+ written.push(...this.#replaceFact({ spaceId, subject, episodeId, spec, scope }));
13
+ continue;
14
+ }
15
+
16
+ if (this.#hasCurrentFact(spaceId, subject, spec.predicate, spec.object)) continue;
17
+
18
+ const supersededFacts = this.#supersededCurrentFacts(
19
+ spaceId,
20
+ subject,
21
+ spec.predicate,
22
+ spec.object
23
+ );
24
+ const newFact = this.store.addFact({
25
+ spaceId,
26
+ subject,
27
+ predicate: spec.predicate,
28
+ object: spec.object,
29
+ sourceEpisodeId: episodeId,
30
+ status: FactStatus.CONFIRMED,
31
+ scope
32
+ });
33
+ written.push(newFact);
34
+
35
+ for (const oldFact of supersededFacts) {
36
+ this.store.invalidateFact(oldFact.id, newFact.id);
37
+ }
38
+ }
39
+ return written;
40
+ }
41
+
42
+ #hasCurrentFact(spaceId, subject, predicate, object) {
43
+ return this.store.currentFacts(spaceId).some(
44
+ (fact) =>
45
+ fact.subject === subject &&
46
+ fact.predicate === predicate &&
47
+ fact.object === object
48
+ );
49
+ }
50
+
51
+ #supersededCurrentFacts(spaceId, subject, predicate, object) {
52
+ if (!isUniqueParameterPredicate(predicate)) return [];
53
+
54
+ return this.store.currentFacts(spaceId).filter(
55
+ (fact) =>
56
+ fact.subject === subject &&
57
+ fact.predicate === predicate &&
58
+ fact.object !== object
59
+ );
60
+ }
61
+
62
+ #replaceFact({ spaceId, subject, episodeId, spec, scope }) {
63
+ const oldFacts = this.store.currentFacts(spaceId).filter(
64
+ (fact) => fact.subject === subject && fact.object === spec.oldValue
65
+ );
66
+ const predicates = oldFacts.length
67
+ ? [...new Set(oldFacts.map((fact) => fact.predicate))]
68
+ : ['has_replacement'];
69
+ const newFacts = predicates.map((predicate) => this.store.addFact({
70
+ spaceId,
71
+ subject,
72
+ predicate,
73
+ object: spec.newValue,
74
+ sourceEpisodeId: episodeId,
75
+ status: FactStatus.CONFIRMED,
76
+ scope
77
+ }));
78
+
79
+ for (const oldFact of oldFacts) {
80
+ const replacement = newFacts.find((fact) => fact.predicate === oldFact.predicate) ?? newFacts[0];
81
+ this.store.invalidateFact(oldFact.id, replacement.id);
82
+ }
83
+ return newFacts;
84
+ }
85
+ }
86
+
87
+ function isUniqueParameterPredicate(predicate) {
88
+ return predicate.startsWith('has_') && predicate !== 'has_url';
89
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ mkdirSync,
3
+ readFileSync,
4
+ renameSync,
5
+ rmSync,
6
+ writeFileSync
7
+ } from 'node:fs';
8
+ import { randomUUID } from 'node:crypto';
9
+ import { dirname, join, resolve } from 'node:path';
10
+
11
+ const DEFAULT_POLICY = Object.freeze({
12
+ enabled: true,
13
+ updatedAt: null
14
+ });
15
+
16
+ export class AgentAccessPolicyConfigurationError extends TypeError {}
17
+
18
+ export class AgentAccessPolicyStore {
19
+ constructor(path = null, { now = () => new Date().toISOString() } = {}) {
20
+ this.path = path ? resolve(path) : null;
21
+ this.now = now;
22
+ this.memory = { ...DEFAULT_POLICY };
23
+ }
24
+
25
+ read() {
26
+ if (!this.path) return { ...this.memory };
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(readFileSync(this.path, 'utf8'));
30
+ } catch (error) {
31
+ if (error?.code === 'ENOENT') return { ...DEFAULT_POLICY };
32
+ throw new AgentAccessPolicyConfigurationError(
33
+ 'Agent access policy could not be read'
34
+ );
35
+ }
36
+ return validateStoredPolicy(parsed);
37
+ }
38
+
39
+ update(input) {
40
+ if (!input || typeof input.enabled !== 'boolean') {
41
+ throw new TypeError('Agent access policy enabled must be a boolean');
42
+ }
43
+ const next = {
44
+ enabled: input.enabled,
45
+ updatedAt: this.now()
46
+ };
47
+ if (!this.path) {
48
+ this.memory = next;
49
+ return { ...next };
50
+ }
51
+ writeAtomically(this.path, { version: 1, ...next });
52
+ return { ...next };
53
+ }
54
+ }
55
+
56
+ export function agentAccessPolicyPathForRuntime(runtimeConfigPath) {
57
+ if (!runtimeConfigPath) return null;
58
+ return join(dirname(resolve(runtimeConfigPath)), 'agent-access-policy.json');
59
+ }
60
+
61
+ function validateStoredPolicy(value) {
62
+ if (value?.version !== 1 || typeof value.enabled !== 'boolean') {
63
+ throw new AgentAccessPolicyConfigurationError('Agent access policy is invalid');
64
+ }
65
+ if (value.updatedAt !== null && typeof value.updatedAt !== 'string') {
66
+ throw new AgentAccessPolicyConfigurationError(
67
+ 'Agent access policy updatedAt is invalid'
68
+ );
69
+ }
70
+ return {
71
+ enabled: value.enabled,
72
+ updatedAt: value.updatedAt ?? null
73
+ };
74
+ }
75
+
76
+ function writeAtomically(path, value) {
77
+ mkdirSync(dirname(path), { recursive: true });
78
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
79
+ try {
80
+ writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, {
81
+ encoding: 'utf8',
82
+ mode: 0o600
83
+ });
84
+ renameSync(temporaryPath, path);
85
+ } finally {
86
+ rmSync(temporaryPath, { force: true });
87
+ }
88
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ mkdirSync,
3
+ readFileSync,
4
+ renameSync,
5
+ rmSync,
6
+ writeFileSync
7
+ } from 'node:fs';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { randomUUID } from 'node:crypto';
10
+
11
+ const DEFAULT_POLICY = Object.freeze({
12
+ enabled: true,
13
+ updatedAt: null
14
+ });
15
+
16
+ export class CapturePolicyConfigurationError extends TypeError {}
17
+
18
+ export class CapturePolicyStore {
19
+ constructor(path = null, { now = () => new Date().toISOString() } = {}) {
20
+ this.path = path ? resolve(path) : null;
21
+ this.now = now;
22
+ this.memory = { ...DEFAULT_POLICY };
23
+ }
24
+
25
+ read() {
26
+ if (!this.path) return { ...this.memory };
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(readFileSync(this.path, 'utf8'));
30
+ } catch (error) {
31
+ if (error?.code === 'ENOENT') return { ...DEFAULT_POLICY };
32
+ throw new CapturePolicyConfigurationError('Capture policy could not be read');
33
+ }
34
+ return validateStoredPolicy(parsed);
35
+ }
36
+
37
+ update(input) {
38
+ if (!input || typeof input.enabled !== 'boolean') {
39
+ throw new TypeError('Capture policy enabled must be a boolean');
40
+ }
41
+ const next = {
42
+ enabled: input.enabled,
43
+ updatedAt: this.now()
44
+ };
45
+ if (!this.path) {
46
+ this.memory = next;
47
+ return { ...next };
48
+ }
49
+ writeAtomically(this.path, { version: 1, ...next });
50
+ return { ...next };
51
+ }
52
+ }
53
+
54
+ export function capturePolicyPathForRuntime(runtimeConfigPath) {
55
+ if (!runtimeConfigPath) return null;
56
+ return join(dirname(resolve(runtimeConfigPath)), 'capture-policy.json');
57
+ }
58
+
59
+ function validateStoredPolicy(value) {
60
+ if (value?.version !== 1 || typeof value.enabled !== 'boolean') {
61
+ throw new CapturePolicyConfigurationError('Capture policy is invalid');
62
+ }
63
+ if (value.updatedAt !== null && typeof value.updatedAt !== 'string') {
64
+ throw new CapturePolicyConfigurationError('Capture policy updatedAt is invalid');
65
+ }
66
+ return {
67
+ enabled: value.enabled,
68
+ updatedAt: value.updatedAt ?? null
69
+ };
70
+ }
71
+
72
+ function writeAtomically(path, value) {
73
+ mkdirSync(dirname(path), { recursive: true });
74
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
75
+ try {
76
+ writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, {
77
+ encoding: 'utf8',
78
+ mode: 0o600
79
+ });
80
+ renameSync(temporaryPath, path);
81
+ } finally {
82
+ rmSync(temporaryPath, { force: true });
83
+ }
84
+ }