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,143 @@
1
+ import { assertStorePort } from '../storage/store-port.js';
2
+ import { AgentCandidateReadService } from '../agent/candidate-read-service.js';
3
+ import { AgentContextPackService } from '../agent/context-pack-service.js';
4
+ import { AgentPublicReadService } from '../agent/public-read-service.js';
5
+ import { AgentSearchService } from '../agent/search-service.js';
6
+ import { ContextRouter } from '../router.js';
7
+ import { IngestionService } from '../ingestion.js';
8
+ import { observeGitDiff } from '../observer.js';
9
+ import { decideCandidate } from '../candidates.js';
10
+ import { buildContextPack } from '../context-pack.js';
11
+ import { FactService } from '../facts/fact-service.js';
12
+ import { ApplicationError, ApplicationErrorCode } from './application-error.js';
13
+ import { StateService } from './state-service.js';
14
+ import { bootstrapStarterSpaces } from './bootstrap.js';
15
+ import { LensService } from '../lens/lens-service.js';
16
+ import { LensQuery } from '../lens/lens-query.js';
17
+ import { LensResourceService } from '../lens/lens-resource-service.js';
18
+ import { PublicationService } from '../publication/publication-service.js';
19
+ import { OutboxService } from '../publication/outbox-service.js';
20
+
21
+ const CANDIDATE_DECISIONS = new Set(['sync', 'personal_only', 'ignore']);
22
+
23
+ export function createApplication({ store, activePersonalSpaceName = '我' }) {
24
+ assertStorePort(store);
25
+ const facts = new FactService(store);
26
+ const lensService = new LensService(store);
27
+ const lensQuery = new LensQuery(store);
28
+ const publicationService = new PublicationService(store);
29
+ const outboxService = new OutboxService(store);
30
+ const ingestion = new IngestionService(store, {
31
+ facts,
32
+ lens: lensService,
33
+ publication: publicationService
34
+ });
35
+ const context = new ContextRouter(store);
36
+ const state = new StateService(store);
37
+ const agentReads = new AgentPublicReadService(store);
38
+ const agentSearch = new AgentSearchService(store, lensQuery);
39
+ const agentCandidates = new AgentCandidateReadService(store);
40
+ const agentContextPack = new AgentContextPackService(
41
+ store,
42
+ agentReads,
43
+ agentSearch,
44
+ agentCandidates
45
+ );
46
+ const applyCandidateDecision = (candidateId, decision) => {
47
+ if (!CANDIDATE_DECISIONS.has(decision)) {
48
+ throw new ApplicationError(
49
+ ApplicationErrorCode.VALIDATION,
50
+ `Unknown candidate decision: ${decision}`
51
+ );
52
+ }
53
+ if (!store.getCandidate(candidateId)) {
54
+ throw new ApplicationError(
55
+ ApplicationErrorCode.NOT_FOUND,
56
+ `Candidate not found: ${candidateId}`
57
+ );
58
+ }
59
+ return decideCandidate(store, candidateId, decision, { ingestion });
60
+ };
61
+ const lens = Object.freeze({
62
+ rememberUserFact: (input) => lensService.rememberUserFact(input),
63
+ submitUserObservation: (input) => lensService.submitUserObservation(input),
64
+ confirmObservation: (input) => lensService.confirmObservation(input),
65
+ correctUserFact: (input) => lensService.correctUserFact(input),
66
+ getUserLens: (input) => lensQuery.getUserLens(input),
67
+ getUserLensView: (input) => lensQuery.getUserLensView(input),
68
+ searchUserContext: (input) => lensQuery.searchUserContext(input)
69
+ });
70
+ const lensResourceService = new LensResourceService({
71
+ store,
72
+ lens,
73
+ activePersonalSpace: () => requireActivePersonalSpace(store, activePersonalSpaceName)
74
+ });
75
+ const publication = Object.freeze({
76
+ prepare: (input) => publicationService.prepare(input),
77
+ verify: (envelope) => publicationService.verify(envelope),
78
+ pending: (at) => outboxService.pending(at),
79
+ markFailed: (id, error) => outboxService.markFailed(id, error),
80
+ markSent: (id) => outboxService.markSent(id)
81
+ });
82
+ const lensResources = Object.freeze({
83
+ current: () => lensResourceService.current(),
84
+ history: () => lensResourceService.history(),
85
+ subscribed: () => lensResourceService.subscribed()
86
+ });
87
+ const agent = Object.freeze({
88
+ remember: (input) => ingestion.remember(input),
89
+ search: (input) => agentSearch.search(input),
90
+ currentFacts: (spaceId) => agentReads.currentFacts(spaceId),
91
+ timeline: (spaceId, subject) => agentReads.timeline(spaceId, subject),
92
+ projectRules: (spaceId) => agentReads.projectRules(spaceId),
93
+ factHistory: (input) => agentReads.factHistory(input),
94
+ contextPack: (input) => agentContextPack.build(input),
95
+ observe: (input) => observeGitDiff({ store, ...input }),
96
+ listCandidates: (personalSpaceId) => agentCandidates.list(personalSpaceId),
97
+ decideCandidate: applyCandidateDecision
98
+ });
99
+
100
+ return {
101
+ bootstrap: () => bootstrapStarterSpaces(store),
102
+ createSpace: (name, kind, description = null) =>
103
+ store.createSpace(name, kind, description),
104
+ resolveSpace: ({ id, name } = {}) => {
105
+ if (id) return store.getSpace(id);
106
+ return name ? store.findSpaceByName(name) : null;
107
+ },
108
+ requireSpaceId: ({ id, name, label }) => {
109
+ const space = id ? store.getSpace(id) : (name ? store.findSpaceByName(name) : null);
110
+ if (space) return space.id;
111
+ throw new ApplicationError(
112
+ ApplicationErrorCode.NOT_FOUND,
113
+ `${label} not found: ${name || id || 'missing'}`
114
+ );
115
+ },
116
+ subscribe: (personalSpaceId, spaceId, mode = 'latest') =>
117
+ store.subscribe(personalSpaceId, spaceId, mode),
118
+ remember: agent.remember,
119
+ agent,
120
+ lens,
121
+ lensResources,
122
+ publication,
123
+ observe: agent.observe,
124
+ decideCandidate: agent.decideCandidate,
125
+ search: (input) => context.searchContext(input),
126
+ contextPack: (input) => buildContextPack(store, input),
127
+ state: () => state.build(),
128
+ activePersonalSpace: () => requireActivePersonalSpace(store, activePersonalSpaceName),
129
+ requireActivePersonalSpace: () => requireActivePersonalSpace(store, activePersonalSpaceName),
130
+ close: () => store.close()
131
+ };
132
+ }
133
+
134
+ function requireActivePersonalSpace(store, name) {
135
+ const space = store.findSpaceByName(name);
136
+ if (!space || space.kind !== 'personal') {
137
+ throw new ApplicationError(
138
+ ApplicationErrorCode.NOT_FOUND,
139
+ `Active personal space not found: ${name}`
140
+ );
141
+ }
142
+ return space;
143
+ }
@@ -0,0 +1,38 @@
1
+ import { enrichCandidate } from '../candidates.js';
2
+
3
+ export class StateService {
4
+ constructor(store) {
5
+ this.store = store;
6
+ }
7
+
8
+ build() {
9
+ const spaces = this.store.listSpaces();
10
+ const currentFacts = spaces.flatMap((space) =>
11
+ this.store.currentFacts(space.id).map((fact) => this.#enrichFact(fact))
12
+ );
13
+ const historicalFacts = this.store
14
+ .listFacts({ includeHistorical: true })
15
+ .filter((fact) => fact.invalidAt)
16
+ .map((fact) => this.#enrichFact(fact));
17
+ const candidates = this.store.listCandidates().map((candidate) =>
18
+ enrichCandidate(this.store, candidate)
19
+ );
20
+
21
+ return {
22
+ spaces,
23
+ subscriptions: this.store.listSubscriptions(),
24
+ episodes: this.store.listEpisodes(),
25
+ currentFacts,
26
+ historicalFacts,
27
+ candidates
28
+ };
29
+ }
30
+
31
+ #enrichFact(fact) {
32
+ return {
33
+ ...fact,
34
+ spaceName: this.store.getSpace(fact.spaceId)?.name ?? fact.spaceId,
35
+ episode: this.store.getEpisode(fact.sourceEpisodeId)
36
+ };
37
+ }
38
+ }
@@ -0,0 +1,78 @@
1
+ import { IngestionService } from './ingestion.js';
2
+ import { ApplicationError, ApplicationErrorCode } from './app/application-error.js';
3
+ import { CandidateStatus } from './models.js';
4
+
5
+ export function listCandidates(store, personalSpaceId) {
6
+ return store.pendingCandidates(personalSpaceId).map((candidate) => enrichCandidate(store, candidate));
7
+ }
8
+
9
+ export function decideCandidate(store, candidateId, decision, { ingestion = new IngestionService(store) } = {}) {
10
+ return store.transaction(
11
+ () => decideCandidateInCurrentTransaction(store, candidateId, decision, ingestion),
12
+ { mode: 'immediate' }
13
+ );
14
+ }
15
+
16
+ function decideCandidateInCurrentTransaction(store, candidateId, decision, ingestion) {
17
+ const candidate = store.getCandidate(candidateId);
18
+ if (!candidate) {
19
+ throw new Error(`Candidate not found: ${candidateId}`);
20
+ }
21
+ if (candidate.status !== CandidateStatus.PENDING) {
22
+ throw new ApplicationError(
23
+ ApplicationErrorCode.VALIDATION,
24
+ `Candidate already decided: ${candidateId}`
25
+ );
26
+ }
27
+
28
+ if (decision === 'sync') {
29
+ if (!candidate.targetSpaceId) {
30
+ throw new ApplicationError(
31
+ ApplicationErrorCode.VALIDATION,
32
+ 'Syncing a candidate requires a target space'
33
+ );
34
+ }
35
+ const episode = episodeFor(store, candidate);
36
+ if (episode) {
37
+ ingestion.publishConfirmedInCurrentTransaction({
38
+ targetSpaceId: candidate.targetSpaceId,
39
+ sourceKind: episode.sourceKind,
40
+ body: episode.body,
41
+ sourceUri: episode.sourceUri
42
+ });
43
+ }
44
+ return store.updateCandidateStatus(candidate.id, CandidateStatus.SYNCED);
45
+ }
46
+
47
+ if (decision === 'personal_only') {
48
+ const episode = episodeFor(store, candidate);
49
+ if (episode) {
50
+ ingestion.confirmEpisodeInCurrentTransaction({
51
+ spaceId: candidate.personalSpaceId,
52
+ episode
53
+ });
54
+ }
55
+ return store.updateCandidateStatus(candidate.id, CandidateStatus.PERSONAL_ONLY);
56
+ }
57
+
58
+ if (decision === 'ignore') {
59
+ return store.updateCandidateStatus(candidate.id, CandidateStatus.IGNORED);
60
+ }
61
+
62
+ throw new Error(`Unknown candidate decision: ${decision}`);
63
+ }
64
+
65
+ export function enrichCandidate(store, candidate) {
66
+ return {
67
+ ...candidate,
68
+ personalSpaceName: store.getSpace(candidate.personalSpaceId)?.name ?? candidate.personalSpaceId,
69
+ targetSpaceName: candidate.targetSpaceId
70
+ ? store.getSpace(candidate.targetSpaceId)?.name ?? candidate.targetSpaceId
71
+ : null,
72
+ episode: episodeFor(store, candidate)
73
+ };
74
+ }
75
+
76
+ function episodeFor(store, candidate) {
77
+ return store.getEpisode(candidate.episodeId);
78
+ }
@@ -0,0 +1,63 @@
1
+ import { PublishRoute } from './models.js';
2
+
3
+ const PUBLIC_SOURCE_KINDS = new Set(['prd', 'git', 'config', 'docs']);
4
+ const PERSONAL_SOURCE_KINDS = new Set(['personal']);
5
+ const PERSONAL_MARKERS = [
6
+ '我觉得',
7
+ '我认为',
8
+ '我希望',
9
+ '我想',
10
+ '我打算',
11
+ '我更喜欢',
12
+ '我喜欢',
13
+ '我不喜欢',
14
+ '我的',
15
+ '个人',
16
+ '偏好',
17
+ '判断'
18
+ ];
19
+ const UNCERTAIN_MARKERS = ['可能', '也许', '不确定', '待确认', '以后要', '似乎'];
20
+
21
+ export function classifyEpisode({ sourceKind, body }) {
22
+ if (PERSONAL_SOURCE_KINDS.has(sourceKind)) {
23
+ return PublishRoute.PERSONAL;
24
+ }
25
+
26
+ if (hasAny(body, PERSONAL_MARKERS)) {
27
+ return PublishRoute.PERSONAL;
28
+ }
29
+
30
+ if (hasAny(body, UNCERTAIN_MARKERS)) {
31
+ return PublishRoute.CANDIDATE;
32
+ }
33
+
34
+ if (PUBLIC_SOURCE_KINDS.has(sourceKind) && looksLikeFact(body)) {
35
+ return PublishRoute.PUBLIC;
36
+ }
37
+
38
+ return PublishRoute.CANDIDATE;
39
+ }
40
+
41
+ function looksLikeFact(body) {
42
+ return (
43
+ body.includes(':') ||
44
+ body.includes(':') ||
45
+ body.includes('=>') ||
46
+ /https?:\/\//.test(body) ||
47
+ looksLikeForbiddenRule(body)
48
+ );
49
+ }
50
+
51
+ function looksLikeForbiddenRule(body) {
52
+ return body
53
+ .split(/\r?\n/)
54
+ .some((line) =>
55
+ /^(?:这个项目|本项目|项目)?\s*(?:禁止|不要|不允许)\s*(?:使用|采用|引入|用)?\s*\S+/.test(
56
+ line.trim()
57
+ )
58
+ );
59
+ }
60
+
61
+ function hasAny(body, markers) {
62
+ return markers.some((marker) => body.includes(marker));
63
+ }
@@ -0,0 +1,14 @@
1
+ export function option(args, flag) {
2
+ const index = args.indexOf(flag);
3
+ return index === -1 ? null : args[index + 1];
4
+ }
5
+
6
+ export function requireSpace(app, name) {
7
+ const space = app.resolveSpace({ name });
8
+ if (!space) throw new Error(`Space not found: ${name}`);
9
+ return space;
10
+ }
11
+
12
+ export function humanPredicate(predicate) {
13
+ return predicate.startsWith('has_') ? predicate.slice(4) : predicate;
14
+ }
@@ -0,0 +1,78 @@
1
+ import { candidate, candidates } from './commands/candidates.js';
2
+ import { observe, remember } from './commands/ingestion.js';
3
+ import { context, history, rules, search, timeline } from './commands/reads.js';
4
+ import { createSpace, subscribe } from './commands/spaces.js';
5
+
6
+ const COMMANDS = Object.freeze({
7
+ space: createSpace,
8
+ subscribe,
9
+ remember,
10
+ observe,
11
+ search,
12
+ timeline,
13
+ rules,
14
+ history,
15
+ context,
16
+ candidates,
17
+ candidate
18
+ });
19
+
20
+ const LOCAL_RUNTIME_COMMANDS = new Set(['start', 'stop', 'restart', 'status', 'open']);
21
+
22
+ export function dispatchCommand(app, command, args) {
23
+ const handler = COMMANDS[command];
24
+ if (!handler) throw new Error(`Unknown command: ${command}`);
25
+ return handler(app, args);
26
+ }
27
+
28
+ export function isRegisteredCommand(command) {
29
+ return command === '--help' || command === '-h' ||
30
+ command === '--version' || command === '-v' ||
31
+ Object.hasOwn(COMMANDS, command);
32
+ }
33
+
34
+ export function isCliCommand(command) {
35
+ return command === 'setup' || command === 'uninstall' || command === 'migrate' ||
36
+ LOCAL_RUNTIME_COMMANDS.has(command) || isRegisteredCommand(command);
37
+ }
38
+
39
+ export function isLocalRuntimeCommand(command) {
40
+ return LOCAL_RUNTIME_COMMANDS.has(command);
41
+ }
42
+
43
+ export function printHelp() {
44
+ console.log(`fl <command>
45
+
46
+ General:
47
+ --help, -h
48
+ --version, -v
49
+
50
+ Local service:
51
+ start [--port 2727] [--open] [--rebuild] [--data-dir DIR]
52
+ stop [--data-dir DIR]
53
+ restart [--port 2727] [--open] [--rebuild] [--data-dir DIR]
54
+ status [--json] [--data-dir DIR]
55
+ open [--data-dir DIR]
56
+
57
+ Install and Agent connection:
58
+ setup [--yes] [--codex-only] [--data-dir DIR] [--personal-space NAME] [--port PORT] [--skip-agents] [--no-start] [--with-dev-public]
59
+ uninstall [--yes] [--data-dir DIR]
60
+
61
+ Legacy local knowledge commands:
62
+ fl [--db SQLITE_DB] [--personal-space 我] <command>
63
+ Global options must appear before the command.
64
+
65
+ Commands:
66
+ space create NAME --kind personal|public
67
+ subscribe PERSONAL_SPACE PUBLIC_SPACE
68
+ remember PERSONAL_SPACE --target SPACE --source-kind prd --text TEXT
69
+ observe PERSONAL_SPACE --target SPACE
70
+ search PERSONAL_SPACE QUERY
71
+ timeline SPACE SUBJECT
72
+ rules SPACE
73
+ history SPACE PREDICATE
74
+ context PERSONAL_SPACE SPACE QUERY
75
+ candidates PERSONAL_SPACE
76
+ candidate CANDIDATE_ID sync|personal_only|ignore
77
+ migrate --from LEGACY_JSON --to SQLITE_DB`);
78
+ }
@@ -0,0 +1,17 @@
1
+ import { requireSpace } from '../command-arguments.js';
2
+
3
+ export function candidates(app, args) {
4
+ const personal = requireSpace(app, args[0]);
5
+ const pending = app.agent.listCandidates(personal.id);
6
+ console.log(pending.map((candidate) => `${candidate.id} ${candidate.reason ?? ''}`.trim())
7
+ .join('\n'));
8
+ }
9
+
10
+ export function candidate(app, args) {
11
+ const [candidateId, decision] = args;
12
+ if (!candidateId || !decision) {
13
+ throw new Error('Usage: candidate CANDIDATE_ID sync|personal_only|ignore');
14
+ }
15
+ const result = app.decideCandidate(candidateId, decision);
16
+ console.log(`candidate ${result.status} ${result.id}`);
17
+ }
@@ -0,0 +1,32 @@
1
+ import { option, requireSpace } from '../command-arguments.js';
2
+
3
+ export function remember(app, args) {
4
+ const [personalName, ...rest] = args;
5
+ const personal = requireSpace(app, personalName);
6
+ const targetName = option(rest, '--target');
7
+ const target = targetName ? requireSpace(app, targetName) : null;
8
+ const body = option(rest, '--text');
9
+ if (!body) throw new Error('remember requires --text');
10
+
11
+ const result = app.remember({
12
+ personalSpaceId: personal.id,
13
+ targetSpaceId: target?.id ?? null,
14
+ sourceKind: option(rest, '--source-kind') ?? 'agent',
15
+ body
16
+ });
17
+ console.log(`remembered ${result.route}`);
18
+ }
19
+
20
+ export function observe(app, args) {
21
+ const [personalName, ...rest] = args;
22
+ const personal = requireSpace(app, personalName);
23
+ const targetName = option(rest, '--target');
24
+ const target = targetName ? requireSpace(app, targetName) : null;
25
+ const result = app.observe({
26
+ personalSpaceId: personal.id,
27
+ targetSpaceId: target?.id ?? null,
28
+ cwd: option(rest, '--cwd') ?? process.cwd()
29
+ });
30
+ const suffix = result.observed.length === 1 ? 'change' : 'changes';
31
+ console.log(`observed ${result.observed.length} ${suffix}`);
32
+ }
@@ -0,0 +1,50 @@
1
+ import { formatContextPack } from '../../context-pack.js';
2
+ import { humanPredicate, requireSpace } from '../command-arguments.js';
3
+
4
+ export function search(app, args) {
5
+ const [personalName, ...queryParts] = args;
6
+ const personal = requireSpace(app, personalName);
7
+ console.log(app.search({
8
+ personalSpaceId: personal.id,
9
+ query: queryParts.join(' ')
10
+ }).answer);
11
+ }
12
+
13
+ export function timeline(app, args) {
14
+ const [spaceName, ...subjectParts] = args;
15
+ const space = requireSpace(app, spaceName);
16
+ const facts = app.agent.timeline(space.id, subjectParts.join(' ') || space.name);
17
+ console.log(facts.map((fact) =>
18
+ `${fact.validAt} ${fact.predicate} ${fact.object}`).join('\n'));
19
+ }
20
+
21
+ export function rules(app, args) {
22
+ const space = requireSpace(app, args[0]);
23
+ const result = app.agent.projectRules(space.id);
24
+ const lines = [
25
+ ...result.forbidden.map((fact) => `forbids ${fact.object}`),
26
+ ...result.parameters.map((fact) => `${humanPredicate(fact.predicate)} ${fact.object}`),
27
+ ...result.links.map((fact) => `url ${fact.object}`)
28
+ ];
29
+ console.log(lines.join('\n'));
30
+ }
31
+
32
+ export function history(app, args) {
33
+ const [spaceName, predicate] = args;
34
+ const space = requireSpace(app, spaceName);
35
+ const result = app.agent.factHistory({ spaceId: space.id, predicate });
36
+ console.log(result.facts.map((fact) =>
37
+ `${fact.current ? 'current' : 'historical'} ` +
38
+ `${humanPredicate(fact.predicate)} ${fact.object}`).join('\n'));
39
+ }
40
+
41
+ export function context(app, args) {
42
+ const [personalName, spaceName, ...queryParts] = args;
43
+ const personal = requireSpace(app, personalName);
44
+ const space = requireSpace(app, spaceName);
45
+ console.log(formatContextPack(app.agent.contextPack({
46
+ personalSpaceId: personal.id,
47
+ spaceId: space.id,
48
+ query: queryParts.join(' ')
49
+ })));
50
+ }
@@ -0,0 +1,20 @@
1
+ import { SpaceKind } from '../../models.js';
2
+ import { option, requireSpace } from '../command-arguments.js';
3
+
4
+ export function createSpace(app, args) {
5
+ const [subcommand, name, ...rest] = args;
6
+ if (subcommand !== 'create') {
7
+ throw new Error('Usage: space create NAME --kind personal|public');
8
+ }
9
+ const kind = option(rest, '--kind') ?? SpaceKind.PUBLIC;
10
+ const space = app.createSpace(name, kind);
11
+ console.log(`${space.name} ${space.kind} ${space.id}`);
12
+ }
13
+
14
+ export function subscribe(app, args) {
15
+ const [personalName, spaceName] = args;
16
+ const personal = requireSpace(app, personalName);
17
+ const space = requireSpace(app, spaceName);
18
+ app.subscribe(personal.id, space.id);
19
+ console.log(`${personal.name} subscribed ${space.name}`);
20
+ }
@@ -0,0 +1,47 @@
1
+ import { isCliCommand } from './command-registry.js';
2
+
3
+ const RUNTIME_FLAGS = new Set(['--db', '--personal-space']);
4
+
5
+ export function parseCliInvocation(argv = []) {
6
+ const { runtimeArgs, commandArgs } = splitRuntimePrefix(argv);
7
+ const [command, ...args] = commandArgs;
8
+ return { runtimeArgs, command, args };
9
+ }
10
+
11
+ export function splitRuntimePrefix(argv = []) {
12
+ const runtimeArgs = [];
13
+ let index = 0;
14
+ while (index < argv.length && RUNTIME_FLAGS.has(argv[index])) {
15
+ const flag = argv[index];
16
+ const value = argv[index + 1];
17
+ if (!isRuntimeValue(value, argv, index + 2)) {
18
+ throw new TypeError(`Missing value for ${flag}`);
19
+ }
20
+ runtimeArgs.push(flag, value);
21
+ index += 2;
22
+ }
23
+ return { runtimeArgs, commandArgs: argv.slice(index) };
24
+ }
25
+
26
+ function isRuntimeValue(value, argv, nextIndex) {
27
+ if (!isPlainValue(value)) return false;
28
+ return !isCommandBoundary(value) || hasCommandBoundary(argv, nextIndex);
29
+ }
30
+
31
+ function hasCommandBoundary(argv, startIndex) {
32
+ let index = startIndex;
33
+ while (RUNTIME_FLAGS.has(argv[index])) {
34
+ if (!isPlainValue(argv[index + 1])) return false;
35
+ index += 2;
36
+ }
37
+ return isCommandBoundary(argv[index]);
38
+ }
39
+
40
+ function isPlainValue(value) {
41
+ return typeof value === 'string' && Boolean(value.trim()) &&
42
+ !value.startsWith('--') && value !== '-h';
43
+ }
44
+
45
+ function isCommandBoundary(value) {
46
+ return isCliCommand(value);
47
+ }
@@ -0,0 +1,94 @@
1
+ import {
2
+ inspectLocalRuntime,
3
+ openLocalConsole,
4
+ restartLocalRuntime,
5
+ startLocalRuntime,
6
+ stopLocalRuntime
7
+ } from '../local-runtime/lifecycle.js';
8
+ import { resolveSetupPaths } from '../setup/paths.js';
9
+ import { parseLocalRuntimeOptions } from './local-runtime-options.js';
10
+
11
+ export async function runLocalRuntimeCommand(command, args, dependencies = {}) {
12
+ const options = parseLocalRuntimeOptions(command, args);
13
+ const resolvePaths = dependencies.resolvePaths ?? resolveSetupPaths;
14
+ const env = dependencies.env ?? process.env;
15
+ const paths = resolvePaths({ dataDir: options.dataDir, env });
16
+ const write = dependencies.write ?? writeLine;
17
+ const input = { ...options, paths, env, onProgress: write };
18
+ const handlers = {
19
+ start: dependencies.start ?? startLocalRuntime,
20
+ stop: dependencies.stop ?? stopLocalRuntime,
21
+ restart: dependencies.restart ?? restartLocalRuntime,
22
+ status: dependencies.inspect ?? inspectLocalRuntime,
23
+ open: dependencies.openConsole ?? openLocalConsole
24
+ };
25
+ const result = await handlers[command](input, dependencies.lifecycleDependencies);
26
+
27
+ if (command === 'status' && options.json) {
28
+ write(JSON.stringify(result, null, 2));
29
+ } else {
30
+ write(formatLocalRuntimeResult(command, result));
31
+ }
32
+ return {
33
+ ...result,
34
+ exitCode: command === 'status' && result.status !== 'running'
35
+ ? 1
36
+ : result.status === 'partial' ? 1 : 0
37
+ };
38
+ }
39
+
40
+ export function formatLocalRuntimeResult(command, result) {
41
+ if (command === 'start') {
42
+ const verb = result.status === 'running' ? '已经在运行' : '已启动';
43
+ return [`Fuli 本地服务${verb}。`, `管理界面:${result.url}`].join('\n');
44
+ }
45
+ if (command === 'restart') {
46
+ return [`Fuli 本地服务已重启。`, `管理界面:${result.url}`].join('\n');
47
+ }
48
+ if (command === 'stop') {
49
+ if (result.status === 'partial') {
50
+ return [
51
+ 'Fuli 本地 Provider 已停止,但没有终止无法验证身份的界面进程。',
52
+ '请执行 fl status 检查,避免误停其他进程。'
53
+ ].join('\n');
54
+ }
55
+ return result.console === 'not_running' && result.providers === 'not_initialized'
56
+ ? 'Fuli 本地服务尚未初始化。'
57
+ : 'Fuli 本地服务已停止,图谱数据已保留。';
58
+ }
59
+ if (command === 'open') return `已打开:${result.url}`;
60
+ if (command === 'status') return formatStatus(result);
61
+ throw new TypeError(`Unknown local runtime command: ${command}`);
62
+ }
63
+
64
+ function formatStatus(result) {
65
+ const labels = {
66
+ running: '运行中',
67
+ degraded: '部分可用',
68
+ stopped: '已停止',
69
+ not_configured: '未初始化',
70
+ ready: '可用',
71
+ unavailable: '不可用',
72
+ not_connected: '未连接',
73
+ not_configured_provider: '未配置',
74
+ unverified: '身份未验证'
75
+ };
76
+ const publicSummary = result.public.configured
77
+ ? `${labels[result.public.status] ?? result.public.status}(${result.public.providers.length} 个 Provider)`
78
+ : labels.not_connected;
79
+ return [
80
+ `Fuli 本地状态:${labels[result.status] ?? result.status}`,
81
+ `管理界面:${labels[result.console.status] ?? result.console.status} · ${result.console.url}`,
82
+ `个人图谱:${providerLabel(result.personal.status, labels)}`,
83
+ `公共服务:${publicSummary}`
84
+ ].join('\n');
85
+ }
86
+
87
+ function providerLabel(status, labels) {
88
+ if (status === 'not_configured') return labels.not_configured_provider;
89
+ return labels[status] ?? status;
90
+ }
91
+
92
+ function writeLine(value) {
93
+ process.stdout.write(`${value}\n`);
94
+ }