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,167 @@
1
+ import { ApplicationError } from '../app/application-error.js';
2
+
3
+ const OMITTED_KEYS = new Set([
4
+ 'store',
5
+ 'dbpath',
6
+ 'databasepath',
7
+ 'snapshot',
8
+ 'fullsnapshot'
9
+ ]);
10
+ const RESULT_LIMIT_BYTES = 1200;
11
+ const RESULT_STRING_LIMIT_BYTES = 512;
12
+ const RESULT_ITEM_LIMIT = 20;
13
+ const RESULT_PROPERTY_LIMIT = 64;
14
+ const RESULT_DEPTH_LIMIT = 8;
15
+ const MESSAGE_LIMIT = 240;
16
+ const TRUNCATION_MARKER = '...[truncated]';
17
+
18
+ export function successToolResult(value, { limitBytes = RESULT_LIMIT_BYTES } = {}) {
19
+ const state = { truncated: false };
20
+ const sanitized = sanitize(value, state);
21
+ const candidate = isObject(sanitized) && !Array.isArray(sanitized)
22
+ ? sanitized
23
+ : { result: sanitized };
24
+ const structuredContent = boundedStructuredContent(
25
+ candidate,
26
+ state.truncated,
27
+ limitBytes
28
+ );
29
+ return {
30
+ content: [{ type: 'text', text: JSON.stringify(structuredContent) }],
31
+ structuredContent
32
+ };
33
+ }
34
+
35
+ export function errorToolResult(error) {
36
+ const controlled = error instanceof ApplicationError;
37
+ const code = controlled ? error.code : 'internal_error';
38
+ const message = controlled
39
+ ? applicationErrorMessage(error)
40
+ : 'Tool execution failed';
41
+ const structuredContent = { error: { code, message } };
42
+ return {
43
+ content: [{ type: 'text', text: `${code}: ${message}` }],
44
+ structuredContent,
45
+ isError: true
46
+ };
47
+ }
48
+
49
+ export function applicationErrorMessage(error) {
50
+ const stableMessage = String(error.message).split(':', 1)[0];
51
+ return boundedMessage(stableMessage);
52
+ }
53
+
54
+ export function protocolErrorResult(message) {
55
+ if (message.includes('Input validation error')) {
56
+ return errorToolResult(new ApplicationError('validation', 'Invalid tool arguments'));
57
+ }
58
+ return errorToolResult(new Error('Protocol tool failure'));
59
+ }
60
+
61
+ function sanitize(value, state, seen = new WeakSet(), depth = 0) {
62
+ if (!isObject(value)) return value;
63
+ if (seen.has(value)) return '[circular]';
64
+ if (depth >= RESULT_DEPTH_LIMIT) {
65
+ state.truncated = true;
66
+ return TRUNCATION_MARKER;
67
+ }
68
+ seen.add(value);
69
+ if (Array.isArray(value)) {
70
+ if (value.length > RESULT_ITEM_LIMIT) state.truncated = true;
71
+ return value.slice(0, RESULT_ITEM_LIMIT)
72
+ .map((item) => sanitize(item, state, seen, depth + 1));
73
+ }
74
+
75
+ const sanitized = {};
76
+ let included = 0;
77
+ for (const [key, item] of Object.entries(value)) {
78
+ if (OMITTED_KEYS.has(key.toLowerCase().replaceAll('_', ''))) continue;
79
+ if (included >= RESULT_PROPERTY_LIMIT) {
80
+ state.truncated = true;
81
+ break;
82
+ }
83
+ sanitized[key] = sanitize(item, state, seen, depth + 1);
84
+ included += 1;
85
+ }
86
+ return sanitized;
87
+ }
88
+
89
+ function boundedStructuredContent(value, alreadyTruncated, limitBytes) {
90
+ const budget = Number.isInteger(limitBytes) && limitBytes > 0
91
+ ? limitBytes
92
+ : RESULT_LIMIT_BYTES;
93
+ if (!alreadyTruncated && jsonBytes(value) <= budget) return value;
94
+ return projectObject(value, budget, 0, true);
95
+ }
96
+
97
+ function projectValue(value, budget, depth) {
98
+ if (budget < 2) return undefined;
99
+ if (typeof value === 'string') return projectString(value, budget);
100
+ if (!isObject(value)) return jsonBytes(value) <= budget ? value : undefined;
101
+ if (depth >= RESULT_DEPTH_LIMIT) return projectString(TRUNCATION_MARKER, budget);
102
+ if (Array.isArray(value)) return projectArray(value, budget, depth);
103
+ return projectObject(value, budget, depth);
104
+ }
105
+
106
+ function projectString(value, budget) {
107
+ const limit = Math.min(budget, RESULT_STRING_LIMIT_BYTES);
108
+ if (jsonBytes(value) <= limit) return value;
109
+ if (jsonBytes('') > limit) return undefined;
110
+ if (jsonBytes(TRUNCATION_MARKER) > limit) return '';
111
+
112
+ let projected = '';
113
+ for (const character of value) {
114
+ if (jsonBytes(projected + character + TRUNCATION_MARKER) > limit) break;
115
+ projected += character;
116
+ }
117
+ return projected + TRUNCATION_MARKER;
118
+ }
119
+
120
+ function projectArray(value, budget, depth) {
121
+ const projected = [];
122
+ let used = 2;
123
+ for (const item of value.slice(0, RESULT_ITEM_LIMIT)) {
124
+ const separatorBytes = projected.length ? 1 : 0;
125
+ const child = projectValue(item, budget - used - separatorBytes, depth + 1);
126
+ if (child === undefined) continue;
127
+ const childBytes = jsonBytes(child);
128
+ if (used + separatorBytes + childBytes > budget) continue;
129
+ projected.push(child);
130
+ used += separatorBytes + childBytes;
131
+ }
132
+ return projected;
133
+ }
134
+
135
+ function projectObject(value, budget, depth, root = false) {
136
+ const projected = root ? { truncated: true } : {};
137
+ let used = jsonBytes(projected);
138
+ let propertyCount = root ? 1 : 0;
139
+
140
+ for (const [key, item] of Object.entries(value)) {
141
+ if (root && key === 'truncated') continue;
142
+ const separatorBytes = propertyCount ? 1 : 0;
143
+ const propertyBytes = separatorBytes + jsonBytes(key) + 1;
144
+ const child = projectValue(item, budget - used - propertyBytes, depth + 1);
145
+ if (child === undefined) continue;
146
+ const childBytes = jsonBytes(child);
147
+ if (used + propertyBytes + childBytes > budget) continue;
148
+ projected[key] = child;
149
+ used += propertyBytes + childBytes;
150
+ propertyCount += 1;
151
+ }
152
+ return projected;
153
+ }
154
+
155
+ function jsonBytes(value) {
156
+ const json = JSON.stringify(value);
157
+ return json === undefined ? Number.POSITIVE_INFINITY : Buffer.byteLength(json, 'utf8');
158
+ }
159
+
160
+ function boundedMessage(message) {
161
+ const singleLine = String(message).replace(/[\r\n\t]+/g, ' ').trim();
162
+ return singleLine.slice(0, MESSAGE_LIMIT) || 'Request failed';
163
+ }
164
+
165
+ function isObject(value) {
166
+ return value !== null && typeof value === 'object';
167
+ }
@@ -0,0 +1,60 @@
1
+ import * as z from 'zod/v4';
2
+
3
+ export function jsonSchemaToZod(schema) {
4
+ if (Array.isArray(schema.type)) return nullableSchema(schema);
5
+ if (schema.enum) return z.enum(schema.enum);
6
+ if (schema.type === 'object') return objectToZod(schema);
7
+ if (schema.type === 'string') return stringToZod(schema);
8
+ if (schema.type === 'boolean') return z.boolean();
9
+ if (schema.type === 'number') return numberToZod(schema, false);
10
+ if (schema.type === 'integer') return numberToZod(schema, true);
11
+ if (schema.type === 'array') return arrayToZod(schema);
12
+ if (schema.type === 'null') return z.null();
13
+ throw new TypeError(`Unsupported JSON schema type: ${schema.type}`);
14
+ }
15
+
16
+ export function openObjectSchema() {
17
+ return z.looseObject({});
18
+ }
19
+
20
+ function stringToZod(schema) {
21
+ let converted = z.string();
22
+ if (schema.minLength !== undefined) converted = converted.min(schema.minLength);
23
+ if (schema.maxLength !== undefined) converted = converted.max(schema.maxLength);
24
+ if (schema.pattern !== undefined) converted = converted.regex(new RegExp(schema.pattern));
25
+ return converted;
26
+ }
27
+
28
+ function objectToZod(schema) {
29
+ const required = new Set(schema.required ?? []);
30
+ const shape = Object.fromEntries(
31
+ Object.entries(schema.properties ?? {}).map(([name, property]) => {
32
+ const converted = jsonSchemaToZod(property);
33
+ return [name, required.has(name) ? converted : converted.optional()];
34
+ })
35
+ );
36
+ return schema.additionalProperties === false ? z.strictObject(shape) : z.looseObject(shape);
37
+ }
38
+
39
+ function nullableSchema(schema) {
40
+ const types = schema.type;
41
+ if (types.length !== 2 || !types.includes('null')) {
42
+ throw new TypeError(`Unsupported JSON schema union: ${types.join(',')}`);
43
+ }
44
+ const valueType = types.find((type) => type !== 'null');
45
+ return jsonSchemaToZod({ ...schema, type: valueType }).nullable();
46
+ }
47
+
48
+ function numberToZod(schema, integer) {
49
+ let converted = integer ? z.number().int() : z.number();
50
+ if (schema.minimum !== undefined) converted = converted.min(schema.minimum);
51
+ if (schema.maximum !== undefined) converted = converted.max(schema.maximum);
52
+ return converted;
53
+ }
54
+
55
+ function arrayToZod(schema) {
56
+ let converted = z.array(jsonSchemaToZod(schema.items));
57
+ if (schema.minItems !== undefined) converted = converted.min(schema.minItems);
58
+ if (schema.maxItems !== undefined) converted = converted.max(schema.maxItems);
59
+ return converted;
60
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { callAgentTool, listAgentTools } from './agent-tools.js';
3
+ import { ApplicationError } from './app/application-error.js';
4
+ import { openFederatedGraphApplication } from './graphiti/federated-application.js';
5
+ import {
6
+ GraphRuntimeConfigurationError,
7
+ resolveGraphRuntimeOptions
8
+ } from './graphiti/runtime-config.js';
9
+ import { runStdio } from './mcp/runtime.js';
10
+ import { applicationErrorMessage } from './mcp/tool-result.js';
11
+
12
+ await main(process.argv.slice(2)).catch((error) => {
13
+ process.stderr.write(`${startupMessage(error)}\n`);
14
+ process.exitCode = 1;
15
+ });
16
+
17
+ async function main(args) {
18
+ if (args.includes('--tools')) {
19
+ process.stdout.write(`${JSON.stringify(listAgentTools(), null, 2)}\n`);
20
+ return;
21
+ }
22
+ if (args.includes('--help') || args.includes('-h')) {
23
+ process.stdout.write(helpText());
24
+ return;
25
+ }
26
+
27
+ const { runtimeConfigPath } = resolveGraphRuntimeOptions(args, process.env);
28
+ if (args.includes('--call')) {
29
+ await runCall({ args, runtimeConfigPath });
30
+ return;
31
+ }
32
+ await runStdio({ runtimeConfigPath });
33
+ }
34
+
35
+ async function runCall({ args, runtimeConfigPath }) {
36
+ const app = openFederatedGraphApplication({ runtimeConfigPath });
37
+ try {
38
+ const result = await callAgentTool(
39
+ app,
40
+ requiredOption(args, '--call'),
41
+ parseInput(args)
42
+ );
43
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
44
+ } finally {
45
+ await app.close();
46
+ }
47
+ }
48
+
49
+ function option(args, flag) {
50
+ const index = args.indexOf(flag);
51
+ return index === -1 ? null : args[index + 1];
52
+ }
53
+
54
+ function requiredOption(args, flag) {
55
+ const value = option(args, flag);
56
+ if (!value) throw new TypeError(`Missing value for ${flag}`);
57
+ return value;
58
+ }
59
+
60
+ function parseInput(args) {
61
+ const raw = option(args, '--input');
62
+ return raw ? JSON.parse(raw) : {};
63
+ }
64
+
65
+ function startupMessage(error) {
66
+ if (error instanceof ApplicationError) return applicationErrorMessage(error);
67
+ if (error instanceof GraphRuntimeConfigurationError) return error.message;
68
+ return 'MCP server failed to start';
69
+ }
70
+
71
+ function helpText() {
72
+ return 'Usage: node src/mcp-server.js [--runtime-config <graph-runtime.json>]\n' +
73
+ ' node src/mcp-server.js --tools\n' +
74
+ ' node src/mcp-server.js --runtime-config <graph-runtime.json> ' +
75
+ '--call <tool> [--input <json>]\n';
76
+ }
package/src/models.js ADDED
@@ -0,0 +1,44 @@
1
+ export const SpaceKind = Object.freeze({
2
+ PERSONAL: 'personal',
3
+ PUBLIC: 'public'
4
+ });
5
+
6
+ export const CandidateStatus = Object.freeze({
7
+ PENDING: 'pending',
8
+ SYNCED: 'synced',
9
+ PERSONAL_ONLY: 'personal_only',
10
+ IGNORED: 'ignored'
11
+ });
12
+
13
+ export const FactStatus = Object.freeze({
14
+ CONFIRMED: 'confirmed',
15
+ OBSERVED: 'observed',
16
+ SUGGESTED: 'suggested',
17
+ REJECTED: 'rejected',
18
+ DEPRECATED: 'deprecated'
19
+ });
20
+
21
+ export const Sensitivity = Object.freeze({
22
+ NORMAL: 'normal',
23
+ PRIVATE: 'private',
24
+ RESTRICTED: 'restricted'
25
+ });
26
+
27
+ export const FactScope = Object.freeze({
28
+ PERSONAL: 'personal',
29
+ PUBLIC: 'public'
30
+ });
31
+
32
+ export const PublishRoute = Object.freeze({
33
+ PERSONAL: 'personal',
34
+ PUBLIC: 'public',
35
+ CANDIDATE: 'candidate'
36
+ });
37
+
38
+ export function nowIso() {
39
+ return new Date().toISOString();
40
+ }
41
+
42
+ export function isCurrentFact(fact) {
43
+ return !fact.invalidAt && ![FactStatus.REJECTED, FactStatus.DEPRECATED].includes(fact.status);
44
+ }
@@ -0,0 +1,83 @@
1
+ import { execFileSync } from 'node:child_process';
2
+
3
+ import { IngestionService } from './ingestion.js';
4
+
5
+ const CONTEXT_MARKERS = ['可能', '也许', '不确定', '待确认', '以后要', '似乎'];
6
+ const URL_PATTERN = /https?:\/\//;
7
+ const KEY_VALUE_PATTERN = /^[\p{L}\p{N}_.-]+\s*[::]\s*\S/u;
8
+ const FORBIDDEN_RULE_PATTERN =
9
+ /^(?:这个项目|本项目|项目)?\s*(?:禁止|不要|不允许)\s*(?:使用|采用|引入|用)?\s*\S+/;
10
+
11
+ export function observeGitDiff({ store, personalSpaceId, targetSpaceId = null, cwd = process.cwd() }) {
12
+ const service = new IngestionService(store);
13
+ const resolvedTargetSpaceId = targetSpaceId ?? inferOnlySubscribedSpace(store, personalSpaceId);
14
+ const observed = collectGitDiffEpisodes({ cwd }).map((episode) => ({
15
+ ...episode,
16
+ result: service.remember({
17
+ personalSpaceId,
18
+ targetSpaceId: resolvedTargetSpaceId,
19
+ sourceKind: 'git',
20
+ body: episode.body,
21
+ sourceUri: episode.sourceUri
22
+ })
23
+ }));
24
+
25
+ return { observed };
26
+ }
27
+
28
+ function inferOnlySubscribedSpace(store, personalSpaceId) {
29
+ const subscriptions = store.subscriptionsFor(personalSpaceId);
30
+ return subscriptions.length === 1 ? subscriptions[0].spaceId : null;
31
+ }
32
+
33
+ export function collectGitDiffEpisodes({ cwd = process.cwd(), maxBuffer = 1024 * 1024 } = {}) {
34
+ const diff = execFileSync('git', ['diff', '--unified=0', '--no-ext-diff'], {
35
+ cwd,
36
+ encoding: 'utf8',
37
+ maxBuffer
38
+ });
39
+ return parseGitDiff(diff);
40
+ }
41
+
42
+ export function parseGitDiff(diff) {
43
+ const episodes = [];
44
+ let currentPath = null;
45
+
46
+ for (const rawLine of diff.split(/\r?\n/)) {
47
+ if (rawLine.startsWith('+++ b/')) {
48
+ currentPath = rawLine.slice('+++ b/'.length);
49
+ continue;
50
+ }
51
+
52
+ if (rawLine.startsWith('+++ ')) {
53
+ currentPath = null;
54
+ continue;
55
+ }
56
+
57
+ if (!rawLine.startsWith('+')) continue;
58
+
59
+ const body = rawLine.slice(1).trim();
60
+ if (!body) continue;
61
+ if (!isContextWorthyLine(body)) continue;
62
+
63
+ episodes.push({
64
+ body,
65
+ sourceUri: `git-diff:${currentPath ?? 'unknown'}`
66
+ });
67
+ }
68
+
69
+ return episodes;
70
+ }
71
+
72
+ function isContextWorthyLine(body) {
73
+ return (
74
+ KEY_VALUE_PATTERN.test(body) ||
75
+ URL_PATTERN.test(body) ||
76
+ body.startsWith('替代:') ||
77
+ body.startsWith('替代:') ||
78
+ body.startsWith('禁止:') ||
79
+ body.startsWith('禁止:') ||
80
+ FORBIDDEN_RULE_PATTERN.test(body) ||
81
+ CONTEXT_MARKERS.some((marker) => body.includes(marker))
82
+ );
83
+ }
@@ -0,0 +1,6 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
4
+
5
+ export const FULI_PACKAGE_NAME = manifest.name;
6
+ export const FULI_VERSION = manifest.version;
@@ -0,0 +1,33 @@
1
+ export function getProjectRules(store, spaceId) {
2
+ const facts = store.currentFacts(spaceId).map((fact) => enrichFact(store, fact));
3
+
4
+ return {
5
+ spaceId,
6
+ spaceName: store.getSpace(spaceId)?.name ?? spaceId,
7
+ forbidden: facts.filter((fact) => fact.predicate === 'forbids'),
8
+ parameters: facts.filter(isProjectParameter),
9
+ links: facts.filter((fact) => fact.predicate === 'has_url'),
10
+ facts
11
+ };
12
+ }
13
+
14
+ function enrichFact(store, fact) {
15
+ const episode = store.getEpisode(fact.sourceEpisodeId);
16
+
17
+ return {
18
+ ...fact,
19
+ source: episode
20
+ ? {
21
+ id: episode.id,
22
+ kind: episode.sourceKind,
23
+ uri: episode.sourceUri,
24
+ body: episode.body,
25
+ createdAt: episode.createdAt
26
+ }
27
+ : null
28
+ };
29
+ }
30
+
31
+ function isProjectParameter(fact) {
32
+ return fact.predicate.startsWith('has_') && fact.predicate !== 'has_url';
33
+ }
@@ -0,0 +1,39 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export function canonicalJson(value) {
4
+ return JSON.stringify(canonicalValue(value, new Set()));
5
+ }
6
+
7
+ export function contentHash(value) {
8
+ return createHash('sha256').update(canonicalJson(value)).digest('hex');
9
+ }
10
+
11
+ function canonicalValue(value, ancestors) {
12
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
13
+ if (typeof value === 'number') {
14
+ if (Number.isFinite(value)) return value;
15
+ throw new TypeError('Canonical JSON requires finite numbers');
16
+ }
17
+ if (typeof value !== 'object') {
18
+ throw new TypeError('Canonical JSON contains an unsupported value');
19
+ }
20
+ if (ancestors.has(value)) throw new TypeError('Canonical JSON cannot contain circular values');
21
+
22
+ ancestors.add(value);
23
+ try {
24
+ if (Array.isArray(value)) return value.map((item) => canonicalValue(item, ancestors));
25
+ if (!isPlainObject(value)) {
26
+ throw new TypeError('Canonical JSON requires plain objects');
27
+ }
28
+ return Object.fromEntries(Object.keys(value).sort().map(
29
+ (key) => [key, canonicalValue(value[key], ancestors)]
30
+ ));
31
+ } finally {
32
+ ancestors.delete(value);
33
+ }
34
+ }
35
+
36
+ function isPlainObject(value) {
37
+ const prototype = Object.getPrototypeOf(value);
38
+ return prototype === Object.prototype || prototype === null;
39
+ }
@@ -0,0 +1,46 @@
1
+ const MAX_LOOKAHEAD = '9999-12-31T23:59:59.999Z';
2
+ const SAFE_FAILURE = 'Publication delivery failed';
3
+
4
+ export class OutboxService {
5
+ constructor(store, { now = () => new Date() } = {}) {
6
+ this.store = store;
7
+ this.now = now;
8
+ }
9
+
10
+ pending(at = this.now()) {
11
+ return this.store.listPendingOutbox(toIso(at));
12
+ }
13
+
14
+ markSent(id) {
15
+ return this.store.transaction(() => {
16
+ this.#requirePending(id);
17
+ return this.store.markOutboxSent(id, toIso(this.now()));
18
+ }, { mode: 'immediate' });
19
+ }
20
+
21
+ markFailed(id, _error) {
22
+ return this.store.transaction(() => {
23
+ const row = this.#requirePending(id);
24
+ const now = new Date(this.now());
25
+ const delayMinutes = retryDelayMinutes(row.attempts + 1);
26
+ const nextAttemptAt = new Date(now.getTime() + delayMinutes * 60_000).toISOString();
27
+ return this.store.markOutboxFailed(id, SAFE_FAILURE, nextAttemptAt);
28
+ }, { mode: 'immediate' });
29
+ }
30
+
31
+ #requirePending(id) {
32
+ const row = this.store.listPendingOutbox(MAX_LOOKAHEAD).find((item) => item.id === id);
33
+ if (!row) throw new Error(`Pending Outbox row not found: ${id}`);
34
+ return row;
35
+ }
36
+ }
37
+
38
+ export function retryDelayMinutes(attempt) {
39
+ return Math.min(30, 2 ** Math.max(0, attempt - 1));
40
+ }
41
+
42
+ function toIso(value) {
43
+ const date = value instanceof Date ? value : new Date(value);
44
+ if (Number.isNaN(date.getTime())) throw new TypeError('Outbox time must be valid');
45
+ return date.toISOString();
46
+ }
@@ -0,0 +1,75 @@
1
+ const ENVELOPE_KEYS = ['contentHash', 'facts', 'id', 'policyVersion', 'source', 'spaceId'];
2
+ const SOURCE_KEYS = ['capturedAt', 'episodeId', 'kind', 'uri'];
3
+ const FACT_KEYS = ['id', 'object', 'predicate', 'sourceEpisodeId', 'subject'];
4
+
5
+ export function validateEnvelopeSchema(envelope) {
6
+ assertExactObject('Publication envelope', envelope, ENVELOPE_KEYS);
7
+ assertNonEmptyString('Publication envelope id', envelope.id);
8
+ assertNonEmptyString('Publication envelope spaceId', envelope.spaceId);
9
+ if (envelope.policyVersion !== '1') {
10
+ throw new TypeError('Publication envelope policyVersion must be 1');
11
+ }
12
+ if (!/^[a-f0-9]{64}$/.test(envelope.contentHash)) {
13
+ throw new TypeError('Publication envelope contentHash must be lowercase SHA-256');
14
+ }
15
+
16
+ validateSource(envelope.source);
17
+ validateFacts(envelope.facts, envelope.source.episodeId);
18
+ return envelope;
19
+ }
20
+
21
+ function validateSource(source) {
22
+ assertExactObject('Publication source', source, SOURCE_KEYS);
23
+ assertNonEmptyString('Publication source episodeId', source.episodeId);
24
+ assertNonEmptyString('Publication source kind', source.kind);
25
+ if (source.uri !== null && typeof source.uri !== 'string') {
26
+ throw new TypeError('Publication source uri must be a string or null');
27
+ }
28
+ assertNonEmptyString('Publication source capturedAt', source.capturedAt);
29
+ const capturedAt = new Date(source.capturedAt);
30
+ if (Number.isNaN(capturedAt.getTime()) || capturedAt.toISOString() !== source.capturedAt) {
31
+ throw new TypeError('Publication source capturedAt must be an ISO timestamp');
32
+ }
33
+ }
34
+
35
+ function validateFacts(facts, episodeId) {
36
+ if (!Array.isArray(facts) || facts.length === 0) {
37
+ throw new TypeError('Publication envelope facts must be a non-empty array');
38
+ }
39
+ const ids = new Set();
40
+ for (const fact of facts) {
41
+ assertExactObject('Publication fact', fact, FACT_KEYS);
42
+ assertNonEmptyString('Publication fact id', fact.id);
43
+ assertNonEmptyString('Publication fact subject', fact.subject);
44
+ assertNonEmptyString('Publication fact predicate', fact.predicate);
45
+ if (typeof fact.object !== 'string') {
46
+ throw new TypeError('Publication fact object must be a string');
47
+ }
48
+ assertNonEmptyString('Publication fact sourceEpisodeId', fact.sourceEpisodeId);
49
+ if (fact.sourceEpisodeId !== episodeId) {
50
+ throw new TypeError('Publication fact sourceEpisodeId must match the source episode');
51
+ }
52
+ if (ids.has(fact.id)) throw new TypeError(`Duplicate publication fact id: ${fact.id}`);
53
+ ids.add(fact.id);
54
+ }
55
+ }
56
+
57
+ function assertExactObject(label, value, expectedKeys) {
58
+ if (!isPlainObject(value)) throw new TypeError(`${label} must be a plain object`);
59
+ const keys = Object.keys(value).sort();
60
+ if (keys.length !== expectedKeys.length || keys.some((key, index) => key !== expectedKeys[index])) {
61
+ throw new TypeError(`${label} has invalid schema keys`);
62
+ }
63
+ }
64
+
65
+ function assertNonEmptyString(label, value) {
66
+ if (typeof value !== 'string' || value.length === 0) {
67
+ throw new TypeError(`${label} must be a non-empty string`);
68
+ }
69
+ }
70
+
71
+ function isPlainObject(value) {
72
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
73
+ const prototype = Object.getPrototypeOf(value);
74
+ return prototype === Object.prototype || prototype === null;
75
+ }
@@ -0,0 +1,37 @@
1
+ import { contentHash } from './canonical-json.js';
2
+ import { validateEnvelopeSchema } from './publication-envelope-schema.js';
3
+
4
+ export function createEnvelope({ id, spaceId, episode, facts }) {
5
+ const unsigned = {
6
+ id,
7
+ spaceId,
8
+ source: {
9
+ episodeId: episode.id,
10
+ kind: episode.sourceKind,
11
+ uri: episode.sourceUri,
12
+ capturedAt: episode.createdAt
13
+ },
14
+ facts: facts.map(projectFact),
15
+ policyVersion: '1'
16
+ };
17
+ return validateEnvelopeSchema({ ...unsigned, contentHash: contentHash(unsigned) });
18
+ }
19
+
20
+ export function verifyEnvelope(envelope) {
21
+ validateEnvelopeSchema(envelope);
22
+ const { contentHash: suppliedHash, ...unsigned } = envelope;
23
+ if (contentHash(unsigned) !== suppliedHash) {
24
+ throw new Error('Publication envelope content hash does not match');
25
+ }
26
+ return true;
27
+ }
28
+
29
+ function projectFact(fact) {
30
+ return {
31
+ id: fact.id,
32
+ subject: fact.subject,
33
+ predicate: fact.predicate,
34
+ object: fact.object,
35
+ sourceEpisodeId: fact.sourceEpisodeId
36
+ };
37
+ }