fuli-context 0.7.4-beta.2 → 0.7.5

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 (133) hide show
  1. package/README.md +93 -7
  2. package/README.zh-CN.md +84 -7
  3. package/compose.graphiti.yml +6 -6
  4. package/dist/web/assets/AboutPage-ByCUor-j.js +1 -0
  5. package/dist/web/assets/ConnectionsPage-DkdkZ40Q.js +1 -0
  6. package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +1 -0
  7. package/dist/web/assets/KnowledgeInspector-CYAjAb9J.js +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CV4F54R_.js +1 -0
  9. package/dist/web/assets/KnowledgePage-vYYXipOi.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-z4u5mXOG.js +4 -0
  11. package/dist/web/assets/OverviewPage-BWOfeS6k.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-c3w4-h7a.js +1 -0
  13. package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +1 -0
  14. package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +1 -0
  15. package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +11 -0
  16. package/dist/web/assets/PublicProjectsPage-DdH0oGB3.js +1 -0
  17. package/dist/web/assets/ReviewPage-BYIpcIHG.js +1 -0
  18. package/dist/web/assets/SearchableSelect-Dz13ClQb.js +1 -0
  19. package/dist/web/assets/SettingsPage-2VkbXgZi.js +1 -0
  20. package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
  21. package/dist/web/assets/WritingTastePage-BOPQZuva.js +1 -0
  22. package/dist/web/assets/{console-DMvHxKf9.js → console-BlAhujsH.js} +1 -1
  23. package/dist/web/assets/{conversation-launcher-settings-Dnlcw7Rj.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
  24. package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
  25. package/dist/web/assets/{identity-C9mLdR8L.js → identity-4_-v6cB7.js} +1 -1
  26. package/dist/web/assets/index-BnH5mcZx.css +1 -0
  27. package/dist/web/assets/index-C67Q0JY2.js +2 -0
  28. package/dist/web/assets/{model-CsYAR8O3.js → model-yG0YqUyZ.js} +1 -1
  29. package/dist/web/assets/{useApi-CROJJdhE-BLQiPVw0.js → useApi-CROJJdhE-twhdZArA.js} +1 -1
  30. package/dist/web/assets/vue-router-C-wot_Di.js +1 -0
  31. package/dist/web/index.html +6 -6
  32. package/docs/public-personal-architecture.md +73 -0
  33. package/graph-provider/fuli_graph/app.py +8 -0
  34. package/graph-provider/fuli_graph/collaboration_context.py +69 -6
  35. package/graph-provider/fuli_graph/knowledge_search.py +84 -5
  36. package/graph-provider/fuli_graph/model_validation.py +134 -0
  37. package/graph-provider/fuli_graph/models.py +24 -5
  38. package/graph-provider/fuli_graph/project_agent_access.py +54 -0
  39. package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
  40. package/graph-provider/fuli_graph/project_agent_models.py +278 -0
  41. package/graph-provider/fuli_graph/project_agent_routes.py +597 -0
  42. package/graph-provider/fuli_graph/project_agent_task_models.py +542 -0
  43. package/graph-provider/fuli_graph/runtime.py +259 -0
  44. package/graph-provider/fuli_graph/search_projection.py +3 -0
  45. package/graph-provider/fuli_graph/store.py +6 -0
  46. package/graph-provider/fuli_graph/store_knowledge.py +62 -9
  47. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
  48. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
  49. package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
  50. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
  51. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +699 -0
  52. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
  53. package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -0
  54. package/graph-provider/fuli_graph/store_project_agents.py +965 -0
  55. package/graph-provider/pyproject.toml +2 -1
  56. package/npm-shrinkwrap.json +2 -2
  57. package/package.json +2 -1
  58. package/skills/capturing-session-knowledge/SKILL.md +43 -2
  59. package/src/adaptive-runtime/executor-pool.js +127 -0
  60. package/src/adaptive-runtime/graph-services.js +166 -0
  61. package/src/adaptive-runtime/lease-client.js +335 -0
  62. package/src/adaptive-runtime/runtime-broker.js +318 -0
  63. package/src/adaptive-runtime/settings.js +111 -0
  64. package/src/adaptive-runtime/state.js +74 -0
  65. package/src/agent-tools/graph-definitions.js +396 -0
  66. package/src/agent-tools/graph-handlers.js +61 -0
  67. package/src/agent-tools/project-agent-definitions.js +307 -0
  68. package/src/backup/graph-backup.js +289 -0
  69. package/src/backup/identity-reconciliation.js +208 -0
  70. package/src/backup/runtime-adapters.js +269 -0
  71. package/src/cli/command-registry.js +7 -2
  72. package/src/cli/graph-data-command.js +112 -0
  73. package/src/cli/local-runtime-command.js +16 -2
  74. package/src/cli/setup-command.js +19 -3
  75. package/src/cli/update-command.js +4 -0
  76. package/src/cli/workspace-connection-command.js +333 -0
  77. package/src/cli.js +11 -0
  78. package/src/graphiti/collaboration-preference-projection.js +29 -1
  79. package/src/graphiti/collaboration-preference-workflow.js +121 -0
  80. package/src/graphiti/federated-application.js +74 -112
  81. package/src/graphiti/knowledge-provider-mapping.js +7 -1
  82. package/src/graphiti/project-agent-control-plane.js +263 -0
  83. package/src/graphiti/project-agent-mapping.js +1003 -0
  84. package/src/graphiti/project-agent-workflows.js +557 -0
  85. package/src/graphiti/provider-client.js +270 -1
  86. package/src/graphiti/runtime-config.js +6 -1
  87. package/src/graphiti/workspace-provider-client.js +403 -0
  88. package/src/http/api-router.js +63 -9
  89. package/src/http/graph-api-router.js +278 -0
  90. package/src/local-runtime/lifecycle.js +46 -10
  91. package/src/mcp/context-resources.js +15 -6
  92. package/src/mcp/create-mcp-server.js +51 -16
  93. package/src/mcp/instructions.js +7 -5
  94. package/src/mcp/runtime.js +17 -5
  95. package/src/mcp/session-id.js +38 -1
  96. package/src/mcp/tool-annotations.js +69 -0
  97. package/src/mcp-server.js +23 -6
  98. package/src/native-runtime/runtime.js +717 -0
  99. package/src/server.js +6 -2
  100. package/src/setup/agent-installation-status.js +7 -1
  101. package/src/setup/agents.js +3 -1
  102. package/src/setup/claude-code-config.js +3 -1
  103. package/src/setup/codex-bootstrap.js +11 -10
  104. package/src/setup/codex-config.js +7 -1
  105. package/src/setup/cursor-config.js +3 -1
  106. package/src/setup/graph-runtime.js +158 -13
  107. package/src/setup/neo4j-memory-profile.js +43 -0
  108. package/src/setup/options.js +38 -2
  109. package/src/setup/paths.js +14 -0
  110. package/src/setup/setup.js +37 -1
  111. package/src/system/resource-monitor.js +181 -16
  112. package/src/system/runtime-settings.js +10 -1
  113. package/src/system/system-service.js +27 -2
  114. package/dist/web/assets/AboutPage-CftRexSY.js +0 -1
  115. package/dist/web/assets/ConnectionsPage-D5J91x6d.js +0 -1
  116. package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +0 -1
  119. package/dist/web/assets/KnowledgePage-OUT07aKH.js +0 -1
  120. package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +0 -4
  121. package/dist/web/assets/OverviewPage-CHE6XhdS.js +0 -1
  122. package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +0 -1
  123. package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +0 -1
  124. package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +0 -1
  125. package/dist/web/assets/ReviewPage-DUeyu7E8.js +0 -1
  126. package/dist/web/assets/SearchableSelect-C5RiB5F8.js +0 -1
  127. package/dist/web/assets/SettingsPage-BMuASSzQ.js +0 -1
  128. package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
  129. package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +0 -1
  130. package/dist/web/assets/i18n-CWfd8Dtt.js +0 -5
  131. package/dist/web/assets/index-X2j29j8i.css +0 -1
  132. package/dist/web/assets/index-nP5p5OaY.js +0 -2
  133. package/dist/web/assets/vue-router-DN8x6iY1.js +0 -1
@@ -79,6 +79,269 @@ export class GraphitiProviderClient {
79
79
  `/v1/personal-projects/${encodeURIComponent(projectId)}?${query}`
80
80
  );
81
81
  }
82
+ upsertProjectAgent(input) {
83
+ return this.#request('/v1/project-agents', { method: 'PUT', body: input });
84
+ }
85
+ listProjectAgents(
86
+ personalSpaceId,
87
+ personalProjectId = null,
88
+ { status = null, capability = null } = {}
89
+ ) {
90
+ const query = new URLSearchParams({
91
+ personal_space_id: personalSpaceId
92
+ });
93
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
94
+ if (status) query.set('status', status);
95
+ if (capability) query.set('capability', capability);
96
+ return this.#request(`/v1/project-agents?${query}`);
97
+ }
98
+ getProjectAgent(personalSpaceId, personalProjectId, agentId) {
99
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
100
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
101
+ return this.#request(
102
+ `/v1/project-agents/${encodeURIComponent(agentId)}?${query}`
103
+ );
104
+ }
105
+ deleteProjectAgent(personalSpaceId, agentId, reason = 'archived by user') {
106
+ const query = new URLSearchParams({
107
+ personal_space_id: personalSpaceId,
108
+ reason
109
+ });
110
+ return this.#request(
111
+ `/v1/project-agents/${encodeURIComponent(agentId)}?${query}`,
112
+ { method: 'DELETE' }
113
+ );
114
+ }
115
+ cleanupProjectAgentTestRoles(personalSpaceId, testSource) {
116
+ const query = new URLSearchParams({
117
+ personal_space_id: personalSpaceId,
118
+ test_source: testSource
119
+ });
120
+ return this.#request(`/v1/project-agents/test-cleanup?${query}`, {
121
+ method: 'POST'
122
+ });
123
+ }
124
+ createProjectAgentAssignment(input) {
125
+ return this.#request('/v1/project-agent-assignments', {
126
+ method: 'POST', body: input
127
+ });
128
+ }
129
+ listProjectAgentAssignments({
130
+ personalSpaceId,
131
+ personalProjectId = null,
132
+ agentId = null,
133
+ status = null
134
+ }) {
135
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
136
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
137
+ if (agentId) query.set('agent_id', agentId);
138
+ if (status) query.set('status', status);
139
+ return this.#request(`/v1/project-agent-assignments?${query}`);
140
+ }
141
+ endProjectAgentAssignment(input) {
142
+ return this.#request('/v1/project-agent-assignments/end', {
143
+ method: 'POST', body: input
144
+ });
145
+ }
146
+ replaceProjectAgentAssignment(input) {
147
+ return this.#request('/v1/project-agent-assignments/replace', {
148
+ method: 'POST', body: input
149
+ });
150
+ }
151
+ submitProjectAgentTask(input) {
152
+ return this.#request('/v1/project-agent-tasks', {
153
+ method: 'POST', body: input
154
+ });
155
+ }
156
+ listProjectAgentTasks({
157
+ personalSpaceId,
158
+ personalProjectId = null,
159
+ agentId = null,
160
+ status = null,
161
+ limit = null
162
+ }) {
163
+ const query = new URLSearchParams({
164
+ personal_space_id: personalSpaceId
165
+ });
166
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
167
+ if (agentId) query.set('agent_id', agentId);
168
+ if (status) query.set('status', status);
169
+ if (limit !== null && limit !== undefined) query.set('limit', String(limit));
170
+ return this.#request(`/v1/project-agent-tasks?${query}`);
171
+ }
172
+ viewProjectAgentTask(personalSpaceId, taskId, { includeEvents = true } = {}) {
173
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
174
+ query.set('include_events', String(includeEvents));
175
+ return this.#request(
176
+ `/v1/project-agent-tasks/${encodeURIComponent(taskId)}?${query}`
177
+ );
178
+ }
179
+ recordProjectAgentTaskActivity(input) {
180
+ return this.#request(
181
+ `/v1/project-agent-tasks/${encodeURIComponent(input.task_id ?? input.taskId)}/events`,
182
+ {
183
+ method: 'POST', body: input
184
+ }
185
+ );
186
+ }
187
+ listProjectAgentActivity({
188
+ personalSpaceId,
189
+ agentId,
190
+ fromDate = null,
191
+ toDate = null
192
+ }) {
193
+ const today = new Date().toISOString().slice(0, 10);
194
+ const query = new URLSearchParams({
195
+ personal_space_id: personalSpaceId,
196
+ from: fromDate ?? toDate ?? today,
197
+ to: toDate ?? fromDate ?? today
198
+ });
199
+ return this.#request(
200
+ `/v1/project-agents/${encodeURIComponent(agentId)}/activity?${query}`
201
+ );
202
+ }
203
+ getProjectAgentRecruitmentPolicy(personalSpaceId) {
204
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
205
+ return this.#request(`/v1/project-agent-recruitment-policy?${query}`);
206
+ }
207
+ updateProjectAgentRecruitmentPolicy(input) {
208
+ return this.#request('/v1/project-agent-recruitment-policy', {
209
+ method: 'PUT', body: input
210
+ });
211
+ }
212
+ listProjectAgentRecruitments({
213
+ personalSpaceId,
214
+ personalProjectId = null,
215
+ taskId = null,
216
+ status = null
217
+ }) {
218
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
219
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
220
+ if (taskId) query.set('task_id', taskId);
221
+ if (status) query.set('status', status);
222
+ return this.#request(`/v1/project-agent-recruitments?${query}`);
223
+ }
224
+ decideProjectAgentRecruitment(input) {
225
+ return this.#request(
226
+ `/v1/project-agent-recruitments/${encodeURIComponent(
227
+ input.recruitment_id ?? input.recruitmentId
228
+ )}/decision`,
229
+ { method: 'POST', body: input }
230
+ );
231
+ }
232
+ upsertExecutor(input) {
233
+ return this.#request('/v1/executors', { method: 'PUT', body: input });
234
+ }
235
+ listExecutors(
236
+ personalSpaceId,
237
+ { capability = null, availableOnly = false } = {}
238
+ ) {
239
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
240
+ if (capability) query.set('capability', capability);
241
+ if (availableOnly) query.set('available_only', 'true');
242
+ return this.#request(`/v1/executors?${query}`);
243
+ }
244
+ getExecutor(personalSpaceId, executorId) {
245
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
246
+ return this.#request(`/v1/executors/${encodeURIComponent(executorId)}?${query}`);
247
+ }
248
+ deleteExecutor(personalSpaceId, executorId) {
249
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
250
+ return this.#request(`/v1/executors/${encodeURIComponent(executorId)}?${query}`, {
251
+ method: 'DELETE'
252
+ });
253
+ }
254
+ preflightExecutor(input) {
255
+ return this.#request('/v1/executors/preflight', { method: 'POST', body: input });
256
+ }
257
+ authorizeExecutor(input) {
258
+ return this.#request('/v1/executors/authorization', {
259
+ method: 'POST', body: input
260
+ });
261
+ }
262
+ reportExecutorHealth(input) {
263
+ return this.#request('/v1/executors/health', {
264
+ method: 'POST', body: input
265
+ });
266
+ }
267
+ recordProjectAgentExecutorActual(input) {
268
+ return this.#request('/v1/project-agent-executor-actuals', {
269
+ method: 'POST', body: input
270
+ });
271
+ }
272
+ upsertExecutorRoutingRule(input) {
273
+ return this.#request('/v1/executor-routing-rules', {
274
+ method: 'PUT', body: input
275
+ });
276
+ }
277
+ updateExecutorRoutingRule(input) {
278
+ return this.#request(
279
+ `/v1/executor-routing-rules/${encodeURIComponent(
280
+ input.rule_id ?? input.ruleId
281
+ )}`,
282
+ { method: 'PATCH', body: input }
283
+ );
284
+ }
285
+ listExecutorRoutingRules({
286
+ personalSpaceId,
287
+ scope = null,
288
+ personalProjectId = null,
289
+ taskId = null,
290
+ status = null,
291
+ enabled = null
292
+ }) {
293
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
294
+ if (scope) query.set('scope', scope);
295
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
296
+ if (taskId) query.set('task_id', taskId);
297
+ if (status) query.set('status', status);
298
+ else if (enabled !== null && enabled !== undefined) {
299
+ query.set('status', enabled ? 'active' : 'disabled');
300
+ }
301
+ return this.#request(`/v1/executor-routing-rules?${query}`);
302
+ }
303
+ getExecutorRoutingRule(personalSpaceId, ruleId) {
304
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
305
+ return this.#request(
306
+ `/v1/executor-routing-rules/${encodeURIComponent(ruleId)}?${query}`
307
+ );
308
+ }
309
+ deleteExecutorRoutingRule(personalSpaceId, ruleId) {
310
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
311
+ return this.#request(
312
+ `/v1/executor-routing-rules/${encodeURIComponent(ruleId)}?${query}`,
313
+ { method: 'DELETE' }
314
+ );
315
+ }
316
+ recordProjectAgentTaskOutcome(input) {
317
+ return this.#request('/v1/project-agent-routing-outcomes', {
318
+ method: 'POST', body: input
319
+ });
320
+ }
321
+ listProjectAgentRoutingLearning({
322
+ personalSpaceId,
323
+ personalProjectId = null,
324
+ workKind = null,
325
+ agentId = null,
326
+ executorId = null
327
+ }) {
328
+ const query = new URLSearchParams({ personal_space_id: personalSpaceId });
329
+ if (personalProjectId) query.set('personal_project_id', personalProjectId);
330
+ if (workKind) query.set('work_kind', workKind);
331
+ if (agentId) query.set('agent_id', agentId);
332
+ if (executorId) query.set('executor_id', executorId);
333
+ return this.#request(`/v1/project-agent-routing-learning?${query}`);
334
+ }
335
+ ignoreProjectAgentRoutingLearning(input) {
336
+ return this.#request('/v1/project-agent-routing-learning/ignore', {
337
+ method: 'POST', body: input
338
+ });
339
+ }
340
+ resetProjectAgentRoutingLearning(input) {
341
+ return this.#request('/v1/project-agent-routing-learning/reset', {
342
+ method: 'POST', body: input
343
+ });
344
+ }
82
345
  createPublicationDraft(input) {
83
346
  return this.#request('/v1/publication-drafts', { method: 'POST', body: input });
84
347
  }
@@ -320,12 +583,18 @@ export class GraphitiProviderClient {
320
583
  );
321
584
  }
322
585
  search(input) { return this.#request('/v1/search', { method: 'POST', body: input }); }
323
- collaborationPreferences(personalSpaceId, personalProjectId = null, limit = 100) {
586
+ collaborationPreferences(
587
+ personalSpaceId,
588
+ personalProjectId = null,
589
+ limit = 100,
590
+ projectAgentId = null
591
+ ) {
324
592
  const query = new URLSearchParams({
325
593
  personal_space_id: personalSpaceId,
326
594
  limit: String(limit)
327
595
  });
328
596
  if (personalProjectId) query.set('personal_project_id', personalProjectId);
597
+ if (projectAgentId) query.set('project_agent_id', projectAgentId);
329
598
  return this.#request(`/v1/collaboration-preferences?${query}`);
330
599
  }
331
600
  graph(spaceId, limit = 500, personalProjectId = null, offset = null) {
@@ -38,7 +38,12 @@ function validateConfig(config) {
38
38
  if (!config.personal.spaceId) fail('personal.spaceId is required');
39
39
  if (!Array.isArray(config.workspaces)) fail('workspaces must be an array');
40
40
  for (const [index, workspace] of config.workspaces.entries()) {
41
- provider(workspace, `workspaces[${index}]`);
41
+ const label = `workspaces[${index}]`;
42
+ provider(workspace, label);
43
+ if (workspace.protocol !== undefined &&
44
+ !['graphiti-v1', 'fuli-workspace-v1'].includes(workspace.protocol)) {
45
+ fail(`${label}.protocol is unsupported`);
46
+ }
42
47
  }
43
48
  }
44
49
 
@@ -0,0 +1,403 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import {
4
+ GraphitiProviderClient,
5
+ ProviderRequestError
6
+ } from './provider-client.js';
7
+ import { canonicalProviderUrl } from './runtime-config.js';
8
+
9
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
10
+ const MAX_REQUEST_TIMEOUT_MS = 2_147_483_647;
11
+ const MAX_LIST_PAGES = 100;
12
+ const FULI_WORKSPACE_PROTOCOL = 'fuli-workspace-v1';
13
+ const GRAPHITI_PROTOCOL = 'graphiti-v1';
14
+
15
+ const GRAPHITI_CAPABILITIES = Object.freeze({
16
+ browsePublicProjects: true,
17
+ publishProject: true,
18
+ submitKnowledge: true,
19
+ subscribeProject: true,
20
+ reviewProposals: true,
21
+ query: true
22
+ });
23
+
24
+ export const FULI_WORKSPACE_CAPABILITIES = Object.freeze({
25
+ browsePublicProjects: true,
26
+ publishProject: false,
27
+ submitKnowledge: false,
28
+ subscribeProject: true,
29
+ reviewProposals: false,
30
+ query: true
31
+ });
32
+
33
+ export function createWorkspaceProvider(workspace, {
34
+ fetchImpl = globalThis.fetch,
35
+ requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS
36
+ } = {}) {
37
+ const providerUrl = canonicalProviderUrl(workspace.providerUrl);
38
+ const protocol = workspace.protocol ?? GRAPHITI_PROTOCOL;
39
+ const fuliWorkspace = protocol === FULI_WORKSPACE_PROTOCOL;
40
+ const client = fuliWorkspace
41
+ ? new FuliWorkspaceProviderClient({
42
+ baseUrl: providerUrl,
43
+ accessToken: workspace.accessToken,
44
+ fetchImpl,
45
+ requestTimeoutMs
46
+ })
47
+ : new GraphitiProviderClient({
48
+ baseUrl: providerUrl,
49
+ accessToken: workspace.accessToken,
50
+ fetchImpl,
51
+ requestTimeoutMs
52
+ });
53
+ return {
54
+ ...workspace,
55
+ providerUrl,
56
+ protocol,
57
+ capabilities: fuliWorkspace ? FULI_WORKSPACE_CAPABILITIES : GRAPHITI_CAPABILITIES,
58
+ client
59
+ };
60
+ }
61
+
62
+ export function aggregatePublicCapabilities(workspaces, providerStatuses, projects) {
63
+ const ready = new Set(providerStatuses
64
+ .filter(({ status }) => status === 'ready')
65
+ .map(({ providerUrl }) => providerUrl));
66
+ const supports = (capability) => [...workspaces.values()].some((workspace) =>
67
+ ready.has(workspace.providerUrl) && workspace.capabilities[capability] === true
68
+ );
69
+ return {
70
+ browsePublicProjects: supports('browsePublicProjects'),
71
+ publishProject: supports('publishProject'),
72
+ submitKnowledge: supports('submitKnowledge'),
73
+ subscribeProject: supports('subscribeProject'),
74
+ reviewProposals: supports('reviewProposals') &&
75
+ projects.some(({ role }) => role === 'maintainer')
76
+ };
77
+ }
78
+
79
+ export class FuliWorkspaceProviderClient {
80
+ #accessToken;
81
+ #fetch;
82
+
83
+ constructor({
84
+ baseUrl,
85
+ accessToken,
86
+ fetchImpl = globalThis.fetch,
87
+ requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS
88
+ }) {
89
+ if (!baseUrl) throw new TypeError('Fuli Workspace provider baseUrl is required');
90
+ if (!accessToken) throw new TypeError('Fuli Workspace provider accessToken is required');
91
+ if (typeof fetchImpl !== 'function') throw new TypeError('fetch implementation is required');
92
+ if (!Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 1 ||
93
+ requestTimeoutMs > MAX_REQUEST_TIMEOUT_MS) {
94
+ throw new TypeError('Fuli Workspace provider timeout must be a positive safe integer');
95
+ }
96
+ this.baseUrl = canonicalProviderUrl(baseUrl);
97
+ this.#accessToken = accessToken;
98
+ this.#fetch = fetchImpl;
99
+ this.requestTimeoutMs = requestTimeoutMs;
100
+ }
101
+
102
+ async health() {
103
+ const [payload, session] = await Promise.all([
104
+ this.#request('/healthz', { authenticated: false }),
105
+ this.#request('/v1/auth/session')
106
+ ]);
107
+ if (payload?.status !== 'ok' || String(payload?.protocolVersion) !== '1') {
108
+ throw invalidResponse('Fuli Workspace health response is incompatible');
109
+ }
110
+ if (session?.authenticated !== true) {
111
+ throw invalidResponse('Fuli Workspace authentication session is invalid');
112
+ }
113
+ return {
114
+ status: 'ready',
115
+ providerId: 'fuli-workspace',
116
+ mode: 'workspace',
117
+ storage: 'sqlite',
118
+ protocol: FULI_WORKSPACE_PROTOCOL,
119
+ protocolVersion: '1'
120
+ };
121
+ }
122
+
123
+ async listSpaces() {
124
+ const workspaces = [];
125
+ let cursor = null;
126
+ for (let page = 0; page < MAX_LIST_PAGES; page += 1) {
127
+ const query = new URLSearchParams({ limit: '500' });
128
+ if (cursor !== null) query.set('cursor', cursor);
129
+ const payload = await this.#request(`/v1/workspaces?${query}`);
130
+ if (!Array.isArray(payload?.workspaces)) {
131
+ throw invalidResponse('Fuli Workspace returned an invalid workspace list');
132
+ }
133
+ workspaces.push(...payload.workspaces.map(workspaceSpace));
134
+ if (payload.hasMore !== true) return workspaces;
135
+ const nextCursor = nonEmptyString(payload.nextCursor);
136
+ if (!nextCursor || nextCursor === cursor) {
137
+ throw invalidResponse('Fuli Workspace returned an invalid list cursor');
138
+ }
139
+ cursor = nextCursor;
140
+ }
141
+ throw invalidResponse('Fuli Workspace workspace listing exceeded the page limit');
142
+ }
143
+
144
+ async search(input) {
145
+ const spaceIds = uniqueStrings(input?.space_ids);
146
+ const text = nonEmptyString(input?.query);
147
+ if (!spaceIds.length) return { facts: [], entities: [] };
148
+ if (!text || text.length > 512) {
149
+ throw new TypeError('Fuli Workspace search query must contain 1 to 512 characters');
150
+ }
151
+ const limit = boundedInteger(input?.limit, 20, 1, 100);
152
+ const pages = await Promise.all(spaceIds.map(async (spaceId) => {
153
+ const payload = await this.#request(
154
+ `/v1/workspaces/${encodeURIComponent(spaceId)}/query`,
155
+ {
156
+ method: 'POST',
157
+ body: {
158
+ text,
159
+ includeHistory: input?.include_historical === true,
160
+ limit
161
+ }
162
+ }
163
+ );
164
+ if (!Array.isArray(payload?.facts)) {
165
+ throw invalidResponse('Fuli Workspace returned an invalid query result');
166
+ }
167
+ return payload.facts.map((fact) => searchFact(fact, spaceId));
168
+ }));
169
+ return { facts: pages.flat(), entities: [] };
170
+ }
171
+
172
+ async graph(spaceId, limit = 500, personalProjectId = null, offset = null) {
173
+ if (personalProjectId !== null) {
174
+ throw new TypeError('Fuli Workspace graphs do not support personal project scope');
175
+ }
176
+ const safeSpaceId = nonEmptyString(spaceId);
177
+ if (!safeSpaceId) throw new TypeError('Fuli Workspace ID is required');
178
+ const pageLimit = boundedInteger(limit, 500, 1, 500);
179
+ const pageOffset = offset === null
180
+ ? 0
181
+ : boundedInteger(offset, 0, 0, Number.MAX_SAFE_INTEGER);
182
+ const payload = await this.#request(
183
+ `/v1/workspaces/${encodeURIComponent(safeSpaceId)}/snapshot`
184
+ );
185
+ if (!Array.isArray(payload?.facts)) {
186
+ throw invalidResponse('Fuli Workspace returned an invalid snapshot');
187
+ }
188
+ const selected = payload.facts.slice(pageOffset, pageOffset + pageLimit);
189
+ const graph = factsGraph(selected, safeSpaceId);
190
+ const truncated = pageOffset + selected.length < payload.facts.length;
191
+ return {
192
+ space_id: safeSpaceId,
193
+ ...graph,
194
+ truncated,
195
+ next_offset: truncated ? pageOffset + selected.length : null
196
+ };
197
+ }
198
+
199
+ createSpace() { return unsupported(); }
200
+ listProjectReleases() { return unsupported(); }
201
+ deleteProject() { return unsupported(); }
202
+ createProjectRelation() { return unsupported(); }
203
+ listProjectRelations() { return unsupported(); }
204
+ decideProjectRelation() { return unsupported(); }
205
+ createProposal() { return unsupported(); }
206
+ listProposals() { return unsupported(); }
207
+ decideProposal() { return unsupported(); }
208
+
209
+ async #request(path, { method = 'GET', body, authenticated = true } = {}) {
210
+ const url = new URL(path, `${this.baseUrl}/`);
211
+ if (url.origin !== this.baseUrl || !url.pathname.startsWith('/')) {
212
+ throw new TypeError('Fuli Workspace request escaped the configured provider origin');
213
+ }
214
+ const headers = { accept: 'application/json' };
215
+ if (authenticated) headers.authorization = `Bearer ${this.#accessToken}`;
216
+ if (body !== undefined) headers['content-type'] = 'application/json';
217
+ const controller = new AbortController();
218
+ let timedOut = false;
219
+ const timeout = setTimeout(() => {
220
+ timedOut = true;
221
+ controller.abort();
222
+ }, this.requestTimeoutMs);
223
+ try {
224
+ const response = await this.#fetch(url, {
225
+ method,
226
+ headers,
227
+ body: body === undefined ? undefined : JSON.stringify(body),
228
+ redirect: 'error',
229
+ signal: controller.signal
230
+ });
231
+ if (!response.ok) {
232
+ throw new ProviderRequestError(
233
+ `Fuli Workspace provider returned HTTP ${response.status}.`,
234
+ {
235
+ status: response.status,
236
+ code: response.status >= 500 ? 'provider_http_5xx' : 'provider_error'
237
+ }
238
+ );
239
+ }
240
+ return await safeJson(response);
241
+ } catch (error) {
242
+ if (error instanceof ProviderRequestError) throw error;
243
+ throw new ProviderRequestError(
244
+ timedOut
245
+ ? 'Fuli Workspace provider request timed out.'
246
+ : 'Fuli Workspace provider is unavailable.',
247
+ {
248
+ status: timedOut ? 504 : 0,
249
+ code: timedOut ? 'provider_timeout' : 'provider_unavailable'
250
+ }
251
+ );
252
+ } finally {
253
+ clearTimeout(timeout);
254
+ }
255
+ }
256
+ }
257
+
258
+ function workspaceSpace(workspace) {
259
+ const id = nonEmptyString(workspace?.id);
260
+ const name = nonEmptyString(workspace?.name);
261
+ if (!id || !name) throw invalidResponse('Fuli Workspace returned an invalid workspace');
262
+ return {
263
+ id,
264
+ name,
265
+ kind: 'project',
266
+ description: typeof workspace.description === 'string' ? workspace.description : null,
267
+ visibility: workspace.visibility ?? null,
268
+ status: workspace.status ?? null,
269
+ current_revision: workspace.revision ?? null,
270
+ created_at: workspace.createdAt ?? null,
271
+ updated_at: workspace.updatedAt ?? null,
272
+ owner_id: null
273
+ };
274
+ }
275
+
276
+ function searchFact(fact, spaceId) {
277
+ const triple = factTriple(fact);
278
+ return {
279
+ id: triple.id,
280
+ fact: `${triple.subject} ${triple.predicate} ${triple.object}`,
281
+ source_entity: triple.subject,
282
+ target_entity: triple.object,
283
+ relationship: triple.predicate,
284
+ space_id: spaceId,
285
+ defined_project_id: spaceId,
286
+ scope_distance: 0,
287
+ score: 1,
288
+ origin_quadrant: fact.originQuadrant ?? 'known_known',
289
+ confirmation_status: fact.confirmationStatus ?? 'confirmed',
290
+ valid_at: fact.validFrom ?? fact.createdAt ?? null,
291
+ invalid_at: fact.validTo ?? null,
292
+ created_at: fact.createdAt ?? null
293
+ };
294
+ }
295
+
296
+ function factsGraph(facts, spaceId) {
297
+ const nodes = new Map();
298
+ const edges = facts.map((fact) => {
299
+ const triple = factTriple(fact);
300
+ const source = entityNode(spaceId, triple.subject, fact);
301
+ const target = entityNode(spaceId, triple.object, fact);
302
+ nodes.set(source.id, source);
303
+ nodes.set(target.id, target);
304
+ return {
305
+ id: triple.id,
306
+ source: source.id,
307
+ target: target.id,
308
+ source_name: triple.subject,
309
+ target_name: triple.object,
310
+ type: triple.predicate,
311
+ fact: `${triple.subject} ${triple.predicate} ${triple.object}`,
312
+ origin_quadrant: fact.originQuadrant ?? 'known_known',
313
+ current_quadrant: fact.originQuadrant ?? 'known_known',
314
+ epistemic_status: 'confirmed',
315
+ epistemic_state_explicit: true,
316
+ confirmation_status: fact.confirmationStatus ?? 'confirmed',
317
+ confirmation_state_explicit: true,
318
+ profile_aspect: null,
319
+ valid_at: fact.validFrom ?? fact.createdAt ?? null,
320
+ invalid_at: fact.validTo ?? null,
321
+ created_at: fact.createdAt ?? null,
322
+ attributes: {
323
+ workspaceId: spaceId,
324
+ revision: fact.revision ?? null,
325
+ confirmedBy: fact.confirmedBy ?? null,
326
+ sources: Array.isArray(fact.sources) ? fact.sources : []
327
+ },
328
+ evidence: Array.isArray(fact.evidence) ? fact.evidence : []
329
+ };
330
+ });
331
+ return { nodes: [...nodes.values()], edges };
332
+ }
333
+
334
+ function entityNode(spaceId, name, fact) {
335
+ return {
336
+ id: `workspace-entity:${stableId(spaceId, name)}`,
337
+ name,
338
+ type: 'WorkspaceEntity',
339
+ group_id: spaceId,
340
+ summary: 'Entity referenced by a confirmed shared workspace fact.',
341
+ origin_quadrant: fact.originQuadrant ?? 'known_known',
342
+ current_quadrant: fact.originQuadrant ?? 'known_known',
343
+ epistemic_status: 'confirmed',
344
+ epistemic_state_explicit: true,
345
+ confirmation_status: fact.confirmationStatus ?? 'confirmed',
346
+ confirmation_state_explicit: true,
347
+ profile_aspect: null,
348
+ created_at: fact.createdAt ?? null,
349
+ invalid_at: null
350
+ };
351
+ }
352
+
353
+ function factTriple(fact) {
354
+ const id = nonEmptyString(fact?.id);
355
+ const subject = nonEmptyString(fact?.subject);
356
+ const predicate = nonEmptyString(fact?.predicate);
357
+ const object = nonEmptyString(fact?.object);
358
+ if (!id || !subject || !predicate || !object) {
359
+ throw invalidResponse('Fuli Workspace returned an invalid fact');
360
+ }
361
+ return { id, subject, predicate, object };
362
+ }
363
+
364
+ function stableId(spaceId, value) {
365
+ return createHash('sha256').update(`${spaceId}\0${value}`, 'utf8').digest('hex').slice(0, 24);
366
+ }
367
+
368
+ function uniqueStrings(values) {
369
+ if (!Array.isArray(values)) return [];
370
+ return [...new Set(values.map(nonEmptyString).filter(Boolean))];
371
+ }
372
+
373
+ function nonEmptyString(value) {
374
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
375
+ }
376
+
377
+ function boundedInteger(value, fallback, minimum, maximum) {
378
+ if (value === undefined || value === null) return fallback;
379
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
380
+ throw new TypeError('Fuli Workspace pagination value is invalid');
381
+ }
382
+ return value;
383
+ }
384
+
385
+ async function safeJson(response) {
386
+ const type = response.headers.get('content-type') ?? '';
387
+ if (!type.includes('application/json')) {
388
+ throw invalidResponse('Fuli Workspace returned a non-JSON response');
389
+ }
390
+ try {
391
+ return await response.json();
392
+ } catch {
393
+ throw invalidResponse('Fuli Workspace returned invalid JSON');
394
+ }
395
+ }
396
+
397
+ function invalidResponse(message) {
398
+ return new ProviderRequestError(message, { code: 'provider_invalid_response' });
399
+ }
400
+
401
+ function unsupported() {
402
+ throw new TypeError('Operation is not supported by fuli-workspace-v1');
403
+ }