fuli-context 0.5.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.en.md +2 -333
  2. package/README.md +370 -202
  3. package/README.zh-CN.md +448 -0
  4. package/compose.graphiti.yml +6 -6
  5. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  6. package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
  7. package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
  8. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  10. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
  13. package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
  14. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
  15. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
  18. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
  19. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
  20. package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
  21. package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
  22. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  23. package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
  24. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  25. package/dist/web/assets/console-CbDsKrsO.js +1 -0
  26. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  27. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  28. package/dist/web/assets/i18n-BQcns1PH.js +5 -0
  29. package/dist/web/assets/identity-B8K4VYg6.js +1 -0
  30. package/dist/web/assets/index-CB8Xmsc2.js +2 -0
  31. package/dist/web/assets/index-CZeuPh-U.css +1 -0
  32. package/dist/web/assets/model-eF52GgPF.js +1 -0
  33. package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  35. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
  36. package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
  37. package/dist/web/index.html +9 -6
  38. package/docs/external-knowledge-architecture.md +279 -0
  39. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  40. package/graph-provider/fuli_graph/app.py +57 -1
  41. package/graph-provider/fuli_graph/graph_models.py +3 -0
  42. package/graph-provider/fuli_graph/graph_query.py +62 -16
  43. package/graph-provider/fuli_graph/knowledge_review.py +612 -0
  44. package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
  45. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  46. package/graph-provider/fuli_graph/models.py +19 -0
  47. package/graph-provider/fuli_graph/runtime.py +15 -0
  48. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  49. package/graph-provider/pyproject.toml +1 -1
  50. package/npm-shrinkwrap.json +5 -407
  51. package/package.json +7 -5
  52. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  53. package/skills/flreview/SKILL.md +73 -0
  54. package/skills/flreview/agents/openai.yaml +6 -0
  55. package/src/agent-tools/graph-definitions.js +79 -4
  56. package/src/agent-tools/graph-handlers.js +12 -0
  57. package/src/cli/command-registry.js +3 -3
  58. package/src/cli/local-runtime-command.js +51 -28
  59. package/src/cli/local-runtime-options.js +14 -8
  60. package/src/cli/setup-agent-selection.js +12 -12
  61. package/src/cli/setup-command.js +41 -18
  62. package/src/cli/shell-argument.js +10 -0
  63. package/src/cli/uninstall-command.js +17 -15
  64. package/src/cli/update-command.js +38 -30
  65. package/src/external-knowledge/catalog.js +61 -0
  66. package/src/external-knowledge/conflict-policy.js +58 -0
  67. package/src/external-knowledge/connected-search.js +166 -0
  68. package/src/external-knowledge/connectors/custom.js +133 -0
  69. package/src/external-knowledge/connectors/feishu.js +461 -0
  70. package/src/external-knowledge/connectors/mcp.js +438 -0
  71. package/src/external-knowledge/connectors/notion.js +307 -0
  72. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  73. package/src/external-knowledge/document-mapping.js +151 -0
  74. package/src/external-knowledge/graph-projection.js +17 -0
  75. package/src/external-knowledge/index.js +8 -0
  76. package/src/external-knowledge/registry.js +91 -0
  77. package/src/external-knowledge/runtime.js +68 -0
  78. package/src/external-knowledge/safe-diagnostic.js +14 -0
  79. package/src/external-knowledge/service.js +811 -0
  80. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  81. package/src/graphiti/federated-application.js +66 -41
  82. package/src/graphiti/knowledge-item-role.js +21 -0
  83. package/src/graphiti/knowledge-review.js +43 -0
  84. package/src/graphiti/project-profile-mapping.js +32 -0
  85. package/src/graphiti/provider-client.js +22 -1
  86. package/src/graphiti/source-marker.js +54 -3
  87. package/src/graphiti/task-knowledge-recall.js +250 -0
  88. package/src/http/api-router.js +23 -108
  89. package/src/http/external-knowledge-api-router.js +104 -0
  90. package/src/http/graph-api-router.js +12 -1
  91. package/src/http/request-policy.js +1 -1
  92. package/src/local-runtime/lifecycle.js +26 -15
  93. package/src/mcp/create-mcp-server.js +1 -0
  94. package/src/mcp/instructions.js +7 -7
  95. package/src/mcp/tool-annotations.js +6 -0
  96. package/src/mcp/tool-result.js +1 -0
  97. package/src/server/application-lifecycle.js +2 -11
  98. package/src/server/blocked-ports.js +11 -0
  99. package/src/server.js +36 -17
  100. package/src/setup/agent-installation-status.js +2 -1
  101. package/src/setup/agents.js +2 -1
  102. package/src/setup/claude-code-config.js +2 -1
  103. package/src/setup/codex-bootstrap.js +12 -7
  104. package/src/setup/container-runtime.js +25 -21
  105. package/src/setup/graph-runtime.js +148 -56
  106. package/src/setup/node-runtime.js +3 -3
  107. package/src/setup/options.js +2 -4
  108. package/src/setup/paths.js +18 -3
  109. package/src/setup/review-skill-adapters.js +49 -0
  110. package/src/setup/setup.js +26 -2
  111. package/src/setup/uninstall.js +4 -2
  112. package/src/system/resource-monitor.js +317 -0
  113. package/src/system/runtime-settings.js +131 -0
  114. package/src/system/system-service.js +119 -0
  115. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  116. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
  120. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  121. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  122. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  123. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  124. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  125. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  126. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  127. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  128. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  129. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  130. package/dist/web/assets/index-CpLDx3of.css +0 -1
  131. package/dist/web/assets/index-DAzeFJSc.js +0 -2
  132. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  133. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  134. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  135. package/src/agent/candidate-read-service.js +0 -53
  136. package/src/agent/context-pack-service.js +0 -83
  137. package/src/agent/public-read-policy.js +0 -39
  138. package/src/agent/public-read-service.js +0 -69
  139. package/src/agent/read-projection.js +0 -107
  140. package/src/agent/search-service.js +0 -61
  141. package/src/agent/space-boundary.js +0 -33
  142. package/src/app/bootstrap.js +0 -8
  143. package/src/app/create-application.js +0 -143
  144. package/src/app/state-service.js +0 -38
  145. package/src/candidates.js +0 -78
  146. package/src/classifier.js +0 -63
  147. package/src/cli/command-arguments.js +0 -14
  148. package/src/context-pack.js +0 -206
  149. package/src/extractor.js +0 -77
  150. package/src/fact-history.js +0 -39
  151. package/src/facts/fact-service.js +0 -89
  152. package/src/http/bounded-json-array.js +0 -19
  153. package/src/http/lens-route.js +0 -50
  154. package/src/ingestion.js +0 -161
  155. package/src/lens/lens-correction-service.js +0 -81
  156. package/src/lens/lens-fact-writer.js +0 -85
  157. package/src/lens/lens-input.js +0 -62
  158. package/src/lens/lens-query-projection.js +0 -87
  159. package/src/lens/lens-query-ranking.js +0 -62
  160. package/src/lens/lens-query.js +0 -143
  161. package/src/lens/lens-resource-projection.js +0 -93
  162. package/src/lens/lens-resource-selection.js +0 -45
  163. package/src/lens/lens-resource-service.js +0 -103
  164. package/src/lens/lens-search-query.js +0 -122
  165. package/src/lens/lens-service.js +0 -183
  166. package/src/models.js +0 -44
  167. package/src/observer.js +0 -83
  168. package/src/project-rules.js +0 -33
  169. package/src/publication/outbox-service.js +0 -46
  170. package/src/publication/publication-envelope-schema.js +0 -75
  171. package/src/publication/publication-envelope.js +0 -37
  172. package/src/publication/publication-policy.js +0 -82
  173. package/src/publication/publication-service.js +0 -48
  174. package/src/router.js +0 -58
  175. package/src/runtime-options.js +0 -138
  176. package/src/security/episode-visibility.js +0 -14
  177. package/src/security/fact-visibility.js +0 -15
  178. package/src/security/source-metadata-policy.js +0 -8
  179. package/src/storage/episode-evidence-preview.js +0 -100
  180. package/src/storage/fact-search-order.js +0 -18
  181. package/src/storage/file/episode-evidence.js +0 -37
  182. package/src/storage/file/episode-index.js +0 -59
  183. package/src/storage/file/fact-search.js +0 -60
  184. package/src/storage/file-store.js +0 -437
  185. package/src/storage/migrate.js +0 -128
  186. package/src/storage/migrations/001-initial.sql +0 -83
  187. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  188. package/src/storage/record-validation.js +0 -46
  189. package/src/storage/snapshot-defaults.js +0 -54
  190. package/src/storage/sqlite/candidates-repository.js +0 -84
  191. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  192. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  193. package/src/storage/sqlite/episodes-repository.js +0 -52
  194. package/src/storage/sqlite/fact-search-repository.js +0 -162
  195. package/src/storage/sqlite/facts-repository.js +0 -146
  196. package/src/storage/sqlite/imports-repository.js +0 -39
  197. package/src/storage/sqlite/mapper.js +0 -84
  198. package/src/storage/sqlite/open-database.js +0 -15
  199. package/src/storage/sqlite/outbox-repository.js +0 -103
  200. package/src/storage/sqlite/snapshot-repository.js +0 -69
  201. package/src/storage/sqlite/spaces-repository.js +0 -52
  202. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  203. package/src/storage/sqlite-store.js +0 -159
  204. package/src/storage/stable-json.js +0 -24
  205. package/src/storage/store-port.js +0 -20
  206. package/src/store.js +0 -2
@@ -1,37 +0,0 @@
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
- }
@@ -1,82 +0,0 @@
1
- import {
2
- FactScope,
3
- FactStatus,
4
- Sensitivity,
5
- SpaceKind,
6
- isCurrentFact
7
- } from '../models.js';
8
- import { detectSensitiveContent } from '../security/sensitive-content.js';
9
- import { canonicalJson } from './canonical-json.js';
10
-
11
- export function validatePublication(store, { spaceId, episode, facts }) {
12
- const space = store.getSpace(spaceId);
13
- if (!space || space.kind !== SpaceKind.PUBLIC) {
14
- throw new Error(`Publication target must be a public space: ${spaceId}`);
15
- }
16
- if (!episode?.id) throw new Error('Publication source episode is required');
17
-
18
- const storedEpisode = store.getEpisode(episode.id);
19
- if (!storedEpisode) throw new Error(`Publication source episode not found: ${episode.id}`);
20
- if (storedEpisode.spaceId !== spaceId) throw new Error('Publication episode crosses spaces');
21
- assertAuthentic('episode', episode, storedEpisode);
22
- assertNoSensitiveSource(storedEpisode);
23
-
24
- if (!Array.isArray(facts) || facts.length === 0) {
25
- throw new TypeError('Publication facts must be a non-empty array');
26
- }
27
- const factIds = facts.map((fact) => fact?.id);
28
- if (new Set(factIds).size !== factIds.length) {
29
- throw new TypeError('Publication facts contain a duplicate fact id');
30
- }
31
- const storedFacts = facts.map((fact) => validateFact(store, fact, spaceId, storedEpisode.id));
32
- return { episode: storedEpisode, facts: storedFacts };
33
- }
34
-
35
- function validateFact(store, fact, spaceId, episodeId) {
36
- if (!fact?.id) throw new Error('Publication fact is required');
37
- const stored = store.getFact(fact.id);
38
- if (!stored) throw new Error(`Publication fact not found: ${fact.id}`);
39
- assertAuthentic('fact', fact, stored);
40
- if (stored.spaceId !== spaceId) throw new Error('Publication fact crosses spaces');
41
- if (stored.sourceEpisodeId !== episodeId) throw new Error('Publication fact has missing source');
42
- if (stored.scope !== FactScope.PUBLIC) throw new Error('Personal facts cannot be published');
43
- if (stored.status !== FactStatus.CONFIRMED) throw new Error('Publication fact is not confirmed');
44
- if (stored.sensitivity !== Sensitivity.NORMAL) {
45
- throw new Error('Private or restricted facts cannot be published');
46
- }
47
- if (!isCurrentFact(stored) || !store.currentFacts(spaceId).some(({ id }) => id === stored.id)) {
48
- throw new Error('Historical facts cannot be published');
49
- }
50
- assertSafeTexts([stored.subject, stored.predicate, stored.object]);
51
- return stored;
52
- }
53
-
54
- function assertAuthentic(label, supplied, stored) {
55
- if (canonicalJson(supplied) !== canonicalJson(stored)) {
56
- throw new Error(`Publication ${label} does not match stored record`);
57
- }
58
- }
59
-
60
- function assertNoSensitiveSource(episode) {
61
- assertSafeTexts([
62
- episode.body,
63
- episode.sourceKind,
64
- episode.sourceUri,
65
- ...metadataStrings(episode.metadata)
66
- ]);
67
- }
68
-
69
- function assertSafeTexts(texts) {
70
- if (texts.some((text) => detectSensitiveContent(text).restricted)) {
71
- throw new Error('Detected secret cannot be published');
72
- }
73
- }
74
-
75
- function metadataStrings(value) {
76
- if (typeof value === 'string') return [value];
77
- if (Array.isArray(value)) return value.flatMap(metadataStrings);
78
- if (value && typeof value === 'object') {
79
- return Object.entries(value).flatMap(([key, item]) => [key, ...metadataStrings(item)]);
80
- }
81
- return [];
82
- }
@@ -1,48 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
-
3
- import { createEnvelope, verifyEnvelope } from './publication-envelope.js';
4
- import { validatePublication } from './publication-policy.js';
5
-
6
- export class PublicationService {
7
- constructor(store, { id = randomUUID } = {}) {
8
- this.store = store;
9
- this.id = id;
10
- }
11
-
12
- prepare(input) {
13
- validatePublication(this.store, input);
14
- return this.store.transaction(
15
- () => this.#validateBuildAndEnqueue(input),
16
- { mode: 'immediate' }
17
- );
18
- }
19
-
20
- prepareInCurrentTransaction(input) {
21
- return this.#validateBuildAndEnqueue(input);
22
- }
23
-
24
- verify(envelope) {
25
- return verifyEnvelope(envelope);
26
- }
27
-
28
- #validateBuildAndEnqueue(input) {
29
- const records = validatePublication(this.store, input);
30
- return this.#enqueue({
31
- spaceId: input.spaceId,
32
- envelope: createEnvelope({
33
- id: this.id(),
34
- spaceId: input.spaceId,
35
- ...records
36
- })
37
- });
38
- }
39
-
40
- #enqueue({ spaceId, envelope }) {
41
- const outbox = this.store.enqueueOutbox({
42
- kind: 'publication',
43
- aggregateId: `${spaceId}:${envelope.id}`,
44
- payload: { envelope }
45
- });
46
- return { envelope, outbox };
47
- }
48
- }
package/src/router.js DELETED
@@ -1,58 +0,0 @@
1
- import { isCurrentFact } from './models.js';
2
-
3
- export class ContextRouter {
4
- constructor(store) {
5
- this.store = store;
6
- }
7
-
8
- searchContext({ personalSpaceId, query, includeHistorical = false }) {
9
- const spaceIds = [
10
- personalSpaceId,
11
- ...this.store.subscriptionsFor(personalSpaceId).map((subscription) => subscription.spaceId)
12
- ];
13
- const facts = this.store.searchFacts(spaceIds, query, { includeHistorical });
14
- const matches = facts.map((fact) => buildMatch(this.store, fact));
15
-
16
- return {
17
- query,
18
- spaceIds,
19
- matches,
20
- facts,
21
- answer: formatAnswer(facts)
22
- };
23
- }
24
- }
25
-
26
- function buildMatch(store, fact) {
27
- const episode = store.getEpisode(fact.sourceEpisodeId);
28
-
29
- return {
30
- spaceId: fact.spaceId,
31
- spaceName: store.getSpace(fact.spaceId)?.name ?? fact.spaceId,
32
- current: isCurrentFact(fact),
33
- fact,
34
- source: episode
35
- ? {
36
- id: episode.id,
37
- kind: episode.sourceKind,
38
- uri: episode.sourceUri,
39
- body: episode.body,
40
- createdAt: episode.createdAt
41
- }
42
- : null
43
- };
44
- }
45
-
46
- function formatAnswer(facts) {
47
- if (facts.length === 0) {
48
- return '没有找到相关当前事实';
49
- }
50
-
51
- return facts
52
- .map((fact) => `${fact.subject}:${humanPredicate(fact.predicate)} 是 ${fact.object}`)
53
- .join('\n');
54
- }
55
-
56
- function humanPredicate(predicate) {
57
- return predicate.startsWith('has_') ? predicate.slice(4) : predicate;
58
- }
@@ -1,138 +0,0 @@
1
- import { existsSync as fileExistsSync } from 'node:fs';
2
- import { extname, resolve } from 'node:path';
3
-
4
- import { createApplication } from './app/create-application.js';
5
- import { SpaceKind } from './models.js';
6
- import { resolveSetupPaths } from './setup/paths.js';
7
- import { SqliteStore } from './storage/sqlite-store.js';
8
-
9
- const LEGACY_LOCAL_DB_PATH = '.fuli/context.db';
10
- const DEFAULT_PERSONAL_SPACE = '我';
11
- const RETRY_BUFFER = new Int32Array(new SharedArrayBuffer(4));
12
- const RETRY_DELAY_MS = 5;
13
- const STORE_OPEN_TIMEOUT_MS = 5000;
14
-
15
- export class RuntimeConfigurationError extends TypeError {}
16
-
17
- export function resolveRuntimeOptions(args = [], env = process.env, {
18
- cwd = process.cwd(),
19
- existsSync = fileExistsSync,
20
- setupPaths = {}
21
- } = {}) {
22
- const cliDbPath = runtimeValue(args, '--db');
23
- const envDbPath = environmentValue(env, 'FULI_DB_PATH');
24
- const systemDbPath = resolveSetupPaths({ env, ...setupPaths }).dbPath;
25
- let defaultDbPath = systemDbPath;
26
- if (cliDbPath === null && envDbPath === null) {
27
- defaultDbPath = resolveDefaultDbPath({ cwd, existsSync, systemDbPath });
28
- guardImplicitDefaultUpgrade({ cwd, existsSync });
29
- }
30
- return {
31
- dbPath: cliDbPath ?? envDbPath ?? defaultDbPath,
32
- personalSpaceName: runtimeValue(args, '--personal-space') ??
33
- environmentValue(env, 'FULI_PERSONAL_SPACE') ?? DEFAULT_PERSONAL_SPACE
34
- };
35
- }
36
-
37
- export function resolveStore({ dbPath }) {
38
- if (extname(dbPath).toLowerCase() === '.json') {
39
- throw new RuntimeConfigurationError(legacyJsonMessage(dbPath));
40
- }
41
- return openSqliteStore(dbPath);
42
- }
43
-
44
- export function quoteShellArgument(value, platform = process.platform) {
45
- const argument = String(value);
46
- if (platform === 'win32') {
47
- if (argument.includes('"')) {
48
- throw new TypeError('Windows paths cannot contain a double quote');
49
- }
50
- return `"${argument}"`;
51
- }
52
- return `'${argument.replaceAll("'", "'\\''")}'`;
53
- }
54
-
55
- function resolveDefaultDbPath({ cwd, existsSync, systemDbPath }) {
56
- if (existsSync(systemDbPath)) return systemDbPath;
57
- if (existsSync(resolve(cwd, LEGACY_LOCAL_DB_PATH))) return LEGACY_LOCAL_DB_PATH;
58
- return systemDbPath;
59
- }
60
-
61
- function guardImplicitDefaultUpgrade({ cwd, existsSync }) {
62
- const legacyPath = resolve(cwd, '.fuli/context.json');
63
- const localDatabasePath = resolve(cwd, LEGACY_LOCAL_DB_PATH);
64
- if (existsSync(legacyPath) && !existsSync(localDatabasePath)) {
65
- throw new RuntimeConfigurationError(legacyJsonMessage(legacyPath));
66
- }
67
- }
68
-
69
- function legacyJsonMessage(source) {
70
- return `JSON is a retired legacy format (${quoteShellArgument(source)}). ` +
71
- 'The current Fuli CLI no longer provides SQLite knowledge commands or JSON migration.';
72
- }
73
-
74
- export function openLocalApplication({ dbPath, personalSpaceName = DEFAULT_PERSONAL_SPACE }) {
75
- const store = resolveStore({ dbPath });
76
- const app = createApplication({ store, activePersonalSpaceName: personalSpaceName });
77
- try {
78
- initializeLocalSpaces(app, store, personalSpaceName);
79
- return app;
80
- } catch (error) {
81
- app.close();
82
- throw error;
83
- }
84
- }
85
-
86
- export function initializeLocalSpaces(app, store, personalSpaceName) {
87
- return store.transaction(() => {
88
- if (store.listSpaces().length === 0) {
89
- if (personalSpaceName === DEFAULT_PERSONAL_SPACE) {
90
- app.bootstrap();
91
- } else {
92
- const personal = app.createSpace(personalSpaceName, SpaceKind.PERSONAL);
93
- const project = app.createSpace('工作', SpaceKind.PUBLIC);
94
- app.subscribe(personal.id, project.id);
95
- }
96
- }
97
- return app.requireActivePersonalSpace();
98
- }, { mode: 'immediate' });
99
- }
100
-
101
- function runtimeValue(args, flag) {
102
- const values = [];
103
- for (let index = 0; index < args.length; index += 1) {
104
- if (args[index] !== flag) continue;
105
- const value = args[index + 1];
106
- if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
107
- throw new TypeError(`Missing value for ${flag}`);
108
- }
109
- values.push(value);
110
- }
111
- if (values.length > 1) throw new TypeError(`Duplicate ${flag}`);
112
- return values[0] ?? null;
113
- }
114
-
115
- function environmentValue(env, name) {
116
- const value = env?.[name];
117
- if (value === undefined) return null;
118
- if (typeof value !== 'string' || !value.trim()) {
119
- throw new TypeError(`${name} must not be empty`);
120
- }
121
- return value;
122
- }
123
-
124
- function openSqliteStore(dbPath) {
125
- const deadline = Date.now() + STORE_OPEN_TIMEOUT_MS;
126
- while (true) {
127
- try {
128
- return new SqliteStore(dbPath);
129
- } catch (error) {
130
- if (!isDatabaseBusy(error) || Date.now() >= deadline) throw error;
131
- Atomics.wait(RETRY_BUFFER, 0, 0, RETRY_DELAY_MS);
132
- }
133
- }
134
- }
135
-
136
- function isDatabaseBusy(error) {
137
- return error?.code === 'SQLITE_BUSY' || error?.code === 'SQLITE_LOCKED';
138
- }
@@ -1,14 +0,0 @@
1
- import { detectSensitiveContent } from './sensitive-content.js';
2
-
3
- export function isRestrictedEpisode(episode) {
4
- return [
5
- episode.sourceKind,
6
- episode.body,
7
- episode.sourceUri,
8
- JSON.stringify(episode.metadata)
9
- ].some(hasSensitiveContent);
10
- }
11
-
12
- function hasSensitiveContent(text) {
13
- return detectSensitiveContent(text).restricted;
14
- }
@@ -1,15 +0,0 @@
1
- import { Sensitivity } from '../models.js';
2
- import { detectSensitiveContent } from './sensitive-content.js';
3
-
4
- export function isRestrictedFact(fact) {
5
- return fact.sensitivity === Sensitivity.RESTRICTED ||
6
- [
7
- fact.subject,
8
- fact.predicate,
9
- fact.object
10
- ].some(hasSensitiveContent);
11
- }
12
-
13
- function hasSensitiveContent(text) {
14
- return detectSensitiveContent(text).restricted;
15
- }
@@ -1,8 +0,0 @@
1
- import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
2
- import { detectSensitiveContent } from './sensitive-content.js';
3
-
4
- export function assertSafeSourceMetadata(sourceKind, sourceUri, errorMessage) {
5
- if ([sourceKind, sourceUri].some((text) => detectSensitiveContent(text).restricted)) {
6
- throw new ApplicationError(ApplicationErrorCode.VALIDATION, errorMessage);
7
- }
8
- }
@@ -1,100 +0,0 @@
1
- export const MAX_EPISODE_EVIDENCE_BODY_BYTES = 16 * 1024;
2
- export const MAX_EPISODE_METADATA_BYTES = 16 * 1024;
3
- export const MAX_EPISODE_METADATA_FIELD_BYTES = 2048;
4
- export const DEFAULT_CORRECTION_EVIDENCE_PER_FACT = 5;
5
- export const MAX_CORRECTION_EVIDENCE_PER_FACT = 20;
6
-
7
- export function normalizeEvidencePreviewOptions(options = {}) {
8
- const maxBodyBytes = options.maxBodyBytes ?? MAX_EPISODE_EVIDENCE_BODY_BYTES;
9
- if (
10
- !Number.isInteger(maxBodyBytes) ||
11
- maxBodyBytes < 0 ||
12
- maxBodyBytes > MAX_EPISODE_EVIDENCE_BODY_BYTES
13
- ) {
14
- throw new TypeError(
15
- `Episode evidence body limit must be between 0 and ${MAX_EPISODE_EVIDENCE_BODY_BYTES}`
16
- );
17
- }
18
- return {
19
- maxBodyBytes,
20
- includeRestricted: options.includeRestricted === true
21
- };
22
- }
23
-
24
- export function normalizeCorrectionEvidencePreviewOptions(options = {}) {
25
- const normalized = normalizeEvidencePreviewOptions(options);
26
- const maxCorrectionsPerFact = options.maxCorrectionsPerFact ??
27
- DEFAULT_CORRECTION_EVIDENCE_PER_FACT;
28
- if (
29
- !Number.isInteger(maxCorrectionsPerFact) ||
30
- maxCorrectionsPerFact < 1 ||
31
- maxCorrectionsPerFact > MAX_CORRECTION_EVIDENCE_PER_FACT
32
- ) {
33
- throw new TypeError(
34
- `Correction evidence limit must be between 1 and ${MAX_CORRECTION_EVIDENCE_PER_FACT}`
35
- );
36
- }
37
- return { ...normalized, maxCorrectionsPerFact };
38
- }
39
-
40
- export function previewEpisode(episode, options, { correction = false } = {}) {
41
- if (!episode) return null;
42
- const { value: body, truncated: bodyTruncated } = clipUtf8(
43
- episode.body,
44
- options.maxBodyBytes
45
- );
46
- const metadata = correction
47
- ? previewCorrectionMetadata(episode.metadata)
48
- : previewSourceMetadata(episode.metadata);
49
- const truncatedFields = [];
50
- if (bodyTruncated || episode.bodyTruncated) truncatedFields.push('body');
51
- if (metadata.truncated || episode.metadataTruncated) truncatedFields.push('metadata');
52
-
53
- return {
54
- id: episode.id,
55
- spaceId: episode.spaceId,
56
- sourceKind: episode.sourceKind,
57
- body,
58
- sourceUri: episode.sourceUri,
59
- metadata: metadata.value,
60
- createdAt: episode.createdAt,
61
- truncatedFields
62
- };
63
- }
64
-
65
- export function clipUtf8(value, maxBytes) {
66
- const text = typeof value === 'string' ? value : '';
67
- const encoded = Buffer.from(text, 'utf8');
68
- if (encoded.length <= maxBytes) return { value: text, truncated: false };
69
- let end = maxBytes;
70
- while (end > 0 && (encoded[end] & 0xc0) === 0x80) end -= 1;
71
- return { value: encoded.subarray(0, end).toString('utf8'), truncated: true };
72
- }
73
-
74
- function previewSourceMetadata(metadata) {
75
- if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
76
- return { value: {}, truncated: metadata !== undefined };
77
- }
78
- const serialized = JSON.stringify(metadata);
79
- if (Buffer.byteLength(serialized, 'utf8') > MAX_EPISODE_METADATA_BYTES) {
80
- return { value: {}, truncated: true };
81
- }
82
- return { value: JSON.parse(serialized), truncated: false };
83
- }
84
-
85
- function previewCorrectionMetadata(metadata) {
86
- const source = metadata && typeof metadata === 'object' && !Array.isArray(metadata)
87
- ? metadata
88
- : {};
89
- const value = {};
90
- let truncated = Object.keys(source).some(
91
- (key) => !['kind', 'factId', 'action'].includes(key)
92
- );
93
- for (const key of ['kind', 'factId', 'action']) {
94
- if (typeof source[key] !== 'string') continue;
95
- const clipped = clipUtf8(source[key], MAX_EPISODE_METADATA_FIELD_BYTES);
96
- value[key] = clipped.value;
97
- truncated ||= clipped.truncated;
98
- }
99
- return { value, truncated };
100
- }
@@ -1,18 +0,0 @@
1
- export function compareSearchFacts(left, right) {
2
- return compareText(right.validAt, left.validAt) || compareText(left.id, right.id);
3
- }
4
-
5
- export function searchTextSortKey(value) {
6
- const text = String(value ?? '');
7
- const key = Buffer.allocUnsafe(text.length * 2);
8
- for (let index = 0; index < text.length; index += 1) {
9
- key.writeUInt16BE(text.charCodeAt(index), index * 2);
10
- }
11
- return key;
12
- }
13
-
14
- function compareText(left, right) {
15
- const first = String(left ?? '');
16
- const second = String(right ?? '');
17
- return first === second ? 0 : (first < second ? -1 : 1);
18
- }
@@ -1,37 +0,0 @@
1
- import { isRestrictedEpisode } from '../../security/episode-visibility.js';
2
- import {
3
- normalizeCorrectionEvidencePreviewOptions,
4
- normalizeEvidencePreviewOptions,
5
- previewEpisode
6
- } from '../episode-evidence-preview.js';
7
-
8
- export function fileEpisodeEvidencePreview(index, spaceId, episodeId, options = {}) {
9
- const normalized = normalizeEvidencePreviewOptions(options);
10
- const episode = index.get(episodeId);
11
- if (!isVisibleEpisode(episode, spaceId, normalized.includeRestricted)) return null;
12
- return previewEpisode(episode, normalized);
13
- }
14
-
15
- export function fileCorrectionEpisodeEvidencePreviews(
16
- index,
17
- spaceId,
18
- factIds,
19
- options = {}
20
- ) {
21
- const normalized = normalizeCorrectionEvidencePreviewOptions(options);
22
- return index.correctionWindows(spaceId, factIds, {
23
- includeRestricted: normalized.includeRestricted,
24
- limit: normalized.maxCorrectionsPerFact
25
- }).map((group) => ({
26
- factId: group.factId,
27
- episodes: group.episodes.map(
28
- (episode) => previewEpisode(episode, normalized, { correction: true })
29
- ),
30
- truncated: group.truncated
31
- }));
32
- }
33
-
34
- function isVisibleEpisode(episode, spaceId, includeRestricted) {
35
- return episode?.spaceId === spaceId &&
36
- (includeRestricted || !isRestrictedEpisode(episode));
37
- }
@@ -1,59 +0,0 @@
1
- import { isRestrictedEpisode } from '../../security/episode-visibility.js';
2
-
3
- export class FileEpisodeIndex {
4
- #byId = new Map();
5
- #correctionsBySpace = new Map();
6
- #safeCorrectionsBySpace = new Map();
7
-
8
- constructor(episodes) {
9
- this.rebuild(episodes);
10
- }
11
-
12
- rebuild(episodes) {
13
- this.#byId = new Map();
14
- this.#correctionsBySpace = new Map();
15
- this.#safeCorrectionsBySpace = new Map();
16
- for (const episode of episodes) this.add(episode);
17
- }
18
-
19
- add(episode) {
20
- this.#byId.set(episode.id, episode);
21
- const factId = episode.metadata?.kind === 'lens_correction'
22
- ? episode.metadata.factId
23
- : null;
24
- if (typeof factId !== 'string') return;
25
-
26
- addCorrection(this.#correctionsBySpace, episode, factId);
27
- if (!isRestrictedEpisode(episode)) {
28
- addCorrection(this.#safeCorrectionsBySpace, episode, factId);
29
- }
30
- }
31
-
32
- get(id) {
33
- return this.#byId.get(id) ?? null;
34
- }
35
-
36
- correctionWindows(spaceId, factIds, { includeRestricted, limit }) {
37
- const index = includeRestricted
38
- ? this.#correctionsBySpace
39
- : this.#safeCorrectionsBySpace;
40
- const byFact = index.get(spaceId) ?? new Map();
41
- return [...new Set(factIds)].map((factId) => {
42
- const episodes = byFact.get(factId) ?? [];
43
- return {
44
- factId,
45
- episodes: episodes.slice(Math.max(0, episodes.length - limit)).reverse(),
46
- truncated: episodes.length > limit
47
- };
48
- });
49
- }
50
- }
51
-
52
- function addCorrection(index, episode, factId) {
53
- const byFact = index.get(episode.spaceId) ?? new Map();
54
- const episodes = byFact.get(factId) ?? [];
55
- episodes.push(episode);
56
- episodes.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
57
- byFact.set(factId, episodes);
58
- index.set(episode.spaceId, byFact);
59
- }
@@ -1,60 +0,0 @@
1
- import { isCurrentFact } from '../../models.js';
2
- import { isRestrictedEpisode } from '../../security/episode-visibility.js';
3
- import { isRestrictedFact } from '../../security/fact-visibility.js';
4
- import { compareSearchFacts } from '../fact-search-order.js';
5
-
6
- export function searchFileFacts(facts, episodeIndex, spaceIds, query, options = {}) {
7
- return searchFileFactsPage(facts, episodeIndex, spaceIds, query, options).facts;
8
- }
9
-
10
- export function searchFileFactsPage(facts, episodeIndex, spaceIds, query, options = {}) {
11
- const search = {
12
- includeHistorical: options.includeHistorical ?? false,
13
- scope: options.scope ?? null,
14
- limit: options.limit ?? null,
15
- excludeRestricted: options.excludeRestricted === true,
16
- safeSourceSpaceId: options.safeSourceSpaceId ?? null,
17
- maxObjectBytes: options.maxObjectBytes ?? null,
18
- query: query.toLowerCase()
19
- };
20
-
21
- if (search.limit !== null && (!Number.isInteger(search.limit) || search.limit <= 0)) {
22
- throw new TypeError('Fact search limit must be a positive integer');
23
- }
24
-
25
- const selected = [];
26
- let objectLimitTruncated = false;
27
- for (const fact of facts) {
28
- if (!matchesSearchWithoutObjectLimit(fact, episodeIndex, spaceIds, search)) continue;
29
- if (
30
- search.maxObjectBytes !== null &&
31
- Buffer.byteLength(fact.object, 'utf8') > search.maxObjectBytes
32
- ) {
33
- objectLimitTruncated = true;
34
- continue;
35
- }
36
- selected.push(fact);
37
- if (search.limit === null) continue;
38
- selected.sort(compareSearchFacts);
39
- if (selected.length > search.limit) selected.pop();
40
- }
41
- return { facts: selected, objectLimitTruncated };
42
- }
43
-
44
- function matchesSearchWithoutObjectLimit(fact, episodeIndex, spaceIds, search) {
45
- if (!spaceIds.includes(fact.spaceId)) return false;
46
- if (search.scope !== null && fact.scope !== search.scope) return false;
47
- if (!search.includeHistorical && !isCurrentFact(fact)) return false;
48
- if (search.excludeRestricted && isRestrictedFact(fact)) return false;
49
- if (search.safeSourceSpaceId !== null) {
50
- const source = episodeIndex.get(fact.sourceEpisodeId);
51
- if (
52
- !source ||
53
- source.spaceId !== search.safeSourceSpaceId ||
54
- isRestrictedEpisode(source)
55
- ) return false;
56
- }
57
- return `${fact.subject} ${fact.predicate} ${fact.object}`
58
- .toLowerCase()
59
- .includes(search.query);
60
- }