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.
- package/LICENSE +201 -0
- package/README.md +178 -0
- package/compose.graphiti.yml +86 -0
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
- package/dist/web/assets/console-tbvTUi6p.js +1 -0
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-B3s5CXhv.js +1 -0
- package/dist/web/assets/index-Devf7glZ.css +1 -0
- package/dist/web/assets/index-DwUDhQKc.js +6 -0
- package/dist/web/assets/model-DVT8sjy9.js +1 -0
- package/dist/web/assets/paths-B790iniO.js +1 -0
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
- package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
- package/dist/web/index.html +22 -0
- package/graph-provider/Dockerfile +16 -0
- package/graph-provider/fuli_graph/__init__.py +5 -0
- package/graph-provider/fuli_graph/app.py +547 -0
- package/graph-provider/fuli_graph/auth.py +17 -0
- package/graph-provider/fuli_graph/collaboration_context.py +290 -0
- package/graph-provider/fuli_graph/config.py +34 -0
- package/graph-provider/fuli_graph/graph_models.py +123 -0
- package/graph-provider/fuli_graph/graph_projection.py +405 -0
- package/graph-provider/fuli_graph/graph_query.py +561 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
- package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
- package/graph-provider/fuli_graph/knowledge_management.py +816 -0
- package/graph-provider/fuli_graph/knowledge_records.py +65 -0
- package/graph-provider/fuli_graph/knowledge_search.py +723 -0
- package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
- package/graph-provider/fuli_graph/model_validation.py +58 -0
- package/graph-provider/fuli_graph/models.py +1179 -0
- package/graph-provider/fuli_graph/personal_project_access.py +29 -0
- package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
- package/graph-provider/fuli_graph/project_action_models.py +142 -0
- package/graph-provider/fuli_graph/project_knowledge.py +707 -0
- package/graph-provider/fuli_graph/project_management.py +251 -0
- package/graph-provider/fuli_graph/provider_values.py +40 -0
- package/graph-provider/fuli_graph/runtime.py +350 -0
- package/graph-provider/fuli_graph/search_projection.py +156 -0
- package/graph-provider/fuli_graph/store.py +24 -0
- package/graph-provider/fuli_graph/store_identity.py +237 -0
- package/graph-provider/fuli_graph/store_knowledge.py +447 -0
- package/graph-provider/fuli_graph/store_projects.py +242 -0
- package/graph-provider/fuli_graph/store_publication.py +370 -0
- package/graph-provider/fuli_graph/store_records.py +134 -0
- package/graph-provider/pyproject.toml +35 -0
- package/npm-shrinkwrap.json +6045 -0
- package/package.json +99 -0
- package/skills/capturing-session-knowledge/SKILL.md +226 -0
- package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
- package/skills/grilling-project/SKILL.md +129 -0
- package/skills/grilling-project/agents/openai.yaml +4 -0
- package/skills/grilling-project/references/project-profile.md +105 -0
- package/src/agent/candidate-read-service.js +53 -0
- package/src/agent/context-pack-service.js +83 -0
- package/src/agent/public-read-policy.js +39 -0
- package/src/agent/public-read-service.js +69 -0
- package/src/agent/read-projection.js +107 -0
- package/src/agent/search-service.js +61 -0
- package/src/agent/space-boundary.js +33 -0
- package/src/agent-tools/graph-definitions.js +479 -0
- package/src/agent-tools/graph-handlers.js +96 -0
- package/src/agent-tools/schema.js +37 -0
- package/src/agent-tools.js +16 -0
- package/src/app/application-error.js +13 -0
- package/src/app/bootstrap.js +8 -0
- package/src/app/create-application.js +143 -0
- package/src/app/state-service.js +38 -0
- package/src/candidates.js +78 -0
- package/src/classifier.js +63 -0
- package/src/cli/command-arguments.js +14 -0
- package/src/cli/command-registry.js +78 -0
- package/src/cli/commands/candidates.js +17 -0
- package/src/cli/commands/ingestion.js +32 -0
- package/src/cli/commands/reads.js +50 -0
- package/src/cli/commands/spaces.js +20 -0
- package/src/cli/invocation.js +47 -0
- package/src/cli/local-runtime-command.js +94 -0
- package/src/cli/local-runtime-options.js +64 -0
- package/src/cli/migrate-command.js +71 -0
- package/src/cli/migration-destination.js +117 -0
- package/src/cli/setup-agent-selection.js +195 -0
- package/src/cli/setup-command.js +85 -0
- package/src/cli/uninstall-command.js +67 -0
- package/src/cli.js +90 -0
- package/src/context-pack.js +206 -0
- package/src/defaults.js +1 -0
- package/src/extractor.js +77 -0
- package/src/fact-history.js +39 -0
- package/src/facts/fact-service.js +89 -0
- package/src/graphiti/agent-access-policy.js +88 -0
- package/src/graphiti/capture-policy.js +84 -0
- package/src/graphiti/federated-application.js +1292 -0
- package/src/graphiti/personal-context.js +29 -0
- package/src/graphiti/project-path-context.js +159 -0
- package/src/graphiti/provider-client.js +288 -0
- package/src/graphiti/runtime-config.js +85 -0
- package/src/graphiti/setup.js +43 -0
- package/src/graphiti/source-marker.js +142 -0
- package/src/http/api-router.js +133 -0
- package/src/http/bounded-json-array.js +19 -0
- package/src/http/error-mapping.js +36 -0
- package/src/http/graph-api-router.js +257 -0
- package/src/http/lens-route.js +50 -0
- package/src/http/request-policy.js +41 -0
- package/src/http/response.js +29 -0
- package/src/http/static-handler.js +55 -0
- package/src/ingestion.js +161 -0
- package/src/lens/lens-correction-service.js +81 -0
- package/src/lens/lens-fact-writer.js +85 -0
- package/src/lens/lens-input.js +62 -0
- package/src/lens/lens-query-projection.js +87 -0
- package/src/lens/lens-query-ranking.js +62 -0
- package/src/lens/lens-query.js +143 -0
- package/src/lens/lens-resource-projection.js +93 -0
- package/src/lens/lens-resource-selection.js +45 -0
- package/src/lens/lens-resource-service.js +103 -0
- package/src/lens/lens-search-query.js +122 -0
- package/src/lens/lens-service.js +183 -0
- package/src/local-runtime/lifecycle.js +254 -0
- package/src/mcp/create-mcp-server.js +85 -0
- package/src/mcp/instructions.js +10 -0
- package/src/mcp/project-action-preview-tokens.js +72 -0
- package/src/mcp/runtime.js +71 -0
- package/src/mcp/tool-annotations.js +63 -0
- package/src/mcp/tool-result.js +167 -0
- package/src/mcp/tool-schema.js +60 -0
- package/src/mcp-server.js +76 -0
- package/src/models.js +44 -0
- package/src/observer.js +83 -0
- package/src/package-metadata.js +6 -0
- package/src/project-rules.js +33 -0
- package/src/publication/canonical-json.js +39 -0
- package/src/publication/outbox-service.js +46 -0
- package/src/publication/publication-envelope-schema.js +75 -0
- package/src/publication/publication-envelope.js +37 -0
- package/src/publication/publication-policy.js +82 -0
- package/src/publication/publication-service.js +48 -0
- package/src/router.js +58 -0
- package/src/runtime-options.js +139 -0
- package/src/security/episode-visibility.js +14 -0
- package/src/security/fact-visibility.js +15 -0
- package/src/security/sensitive-content.js +18 -0
- package/src/security/source-metadata-policy.js +8 -0
- package/src/server/application-lifecycle.js +34 -0
- package/src/server/listen.js +24 -0
- package/src/server.js +135 -0
- package/src/setup/agent-installation-status.js +97 -0
- package/src/setup/agents.js +123 -0
- package/src/setup/codex-bootstrap.js +118 -0
- package/src/setup/codex-config.js +122 -0
- package/src/setup/config-backup.js +22 -0
- package/src/setup/container-runtime.js +388 -0
- package/src/setup/cursor-config.js +61 -0
- package/src/setup/graph-runtime.js +360 -0
- package/src/setup/node-runtime.js +24 -0
- package/src/setup/options.js +91 -0
- package/src/setup/paths.js +47 -0
- package/src/setup/session-skill.js +91 -0
- package/src/setup/setup.js +85 -0
- package/src/setup/uninstall.js +90 -0
- package/src/storage/episode-evidence-preview.js +100 -0
- package/src/storage/fact-search-order.js +18 -0
- package/src/storage/file/episode-evidence.js +37 -0
- package/src/storage/file/episode-index.js +59 -0
- package/src/storage/file/fact-search.js +60 -0
- package/src/storage/file-store.js +437 -0
- package/src/storage/import-json.js +70 -0
- package/src/storage/json-file.js +41 -0
- package/src/storage/legacy-record-validation.js +152 -0
- package/src/storage/legacy-snapshot-validation.js +32 -0
- package/src/storage/migrate.js +128 -0
- package/src/storage/migrations/001-initial.sql +83 -0
- package/src/storage/migrations/002-storage-integrity.sql +113 -0
- package/src/storage/record-validation.js +46 -0
- package/src/storage/snapshot-defaults.js +54 -0
- package/src/storage/sqlite/candidates-repository.js +84 -0
- package/src/storage/sqlite/episode-evidence-repository.js +154 -0
- package/src/storage/sqlite/episode-privacy-sql.js +33 -0
- package/src/storage/sqlite/episodes-repository.js +52 -0
- package/src/storage/sqlite/fact-search-repository.js +162 -0
- package/src/storage/sqlite/facts-repository.js +146 -0
- package/src/storage/sqlite/imports-repository.js +39 -0
- package/src/storage/sqlite/mapper.js +84 -0
- package/src/storage/sqlite/open-database.js +15 -0
- package/src/storage/sqlite/outbox-repository.js +103 -0
- package/src/storage/sqlite/snapshot-repository.js +69 -0
- package/src/storage/sqlite/spaces-repository.js +52 -0
- package/src/storage/sqlite/subscriptions-repository.js +54 -0
- package/src/storage/sqlite-store.js +159 -0
- package/src/storage/stable-json.js +24 -0
- package/src/storage/store-port.js +20 -0
- package/src/store.js +2 -0
- package/src/workspace-protocol/array-validation.js +19 -0
- package/src/workspace-protocol/base64url-contract.js +27 -0
- package/src/workspace-protocol/constants.js +37 -0
- package/src/workspace-protocol/error-contract.js +24 -0
- package/src/workspace-protocol/fact-contract.js +110 -0
- package/src/workspace-protocol/governance-contract.js +47 -0
- package/src/workspace-protocol/index.js +53 -0
- package/src/workspace-protocol/json-budget.js +19 -0
- package/src/workspace-protocol/manifest-contract.js +186 -0
- package/src/workspace-protocol/publication-contract.js +212 -0
- package/src/workspace-protocol/query-contract.js +149 -0
- package/src/workspace-protocol/scalar-contract.js +192 -0
- package/src/workspace-protocol/sync-contract.js +184 -0
- package/src/workspace-protocol/text-contract.js +39 -0
- package/src/workspace-protocol/validation.js +66 -0
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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, systemDbPath });
|
|
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
|
+
const destination = dbPath.slice(0, -extname(dbPath).length) + '.db';
|
|
40
|
+
throw new RuntimeConfigurationError(migrationMessage(dbPath, destination));
|
|
41
|
+
}
|
|
42
|
+
return openSqliteStore(dbPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function quoteShellArgument(value, platform = process.platform) {
|
|
46
|
+
const argument = String(value);
|
|
47
|
+
if (platform === 'win32') {
|
|
48
|
+
if (argument.includes('"')) {
|
|
49
|
+
throw new TypeError('Windows paths cannot contain a double quote');
|
|
50
|
+
}
|
|
51
|
+
return `"${argument}"`;
|
|
52
|
+
}
|
|
53
|
+
return `'${argument.replaceAll("'", "'\\''")}'`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveDefaultDbPath({ cwd, existsSync, systemDbPath }) {
|
|
57
|
+
if (existsSync(systemDbPath)) return systemDbPath;
|
|
58
|
+
if (existsSync(resolve(cwd, LEGACY_LOCAL_DB_PATH))) return LEGACY_LOCAL_DB_PATH;
|
|
59
|
+
return systemDbPath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function guardImplicitDefaultUpgrade({ cwd, existsSync, systemDbPath }) {
|
|
63
|
+
const legacyPath = resolve(cwd, '.fuli/context.json');
|
|
64
|
+
const localDatabasePath = resolve(cwd, LEGACY_LOCAL_DB_PATH);
|
|
65
|
+
if (existsSync(legacyPath) && !existsSync(localDatabasePath)) {
|
|
66
|
+
throw new RuntimeConfigurationError(migrationMessage(legacyPath, systemDbPath));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function migrationMessage(source, destination) {
|
|
71
|
+
return 'JSON is a legacy import format. Run once: node src/cli.js migrate ' +
|
|
72
|
+
`--from ${quoteShellArgument(source)} --to ${quoteShellArgument(destination)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function openLocalApplication({ dbPath, personalSpaceName = DEFAULT_PERSONAL_SPACE }) {
|
|
76
|
+
const store = resolveStore({ dbPath });
|
|
77
|
+
const app = createApplication({ store, activePersonalSpaceName: personalSpaceName });
|
|
78
|
+
try {
|
|
79
|
+
initializeLocalSpaces(app, store, personalSpaceName);
|
|
80
|
+
return app;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
app.close();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function initializeLocalSpaces(app, store, personalSpaceName) {
|
|
88
|
+
return store.transaction(() => {
|
|
89
|
+
if (store.listSpaces().length === 0) {
|
|
90
|
+
if (personalSpaceName === DEFAULT_PERSONAL_SPACE) {
|
|
91
|
+
app.bootstrap();
|
|
92
|
+
} else {
|
|
93
|
+
const personal = app.createSpace(personalSpaceName, SpaceKind.PERSONAL);
|
|
94
|
+
const project = app.createSpace('工作', SpaceKind.PUBLIC);
|
|
95
|
+
app.subscribe(personal.id, project.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return app.requireActivePersonalSpace();
|
|
99
|
+
}, { mode: 'immediate' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runtimeValue(args, flag) {
|
|
103
|
+
const values = [];
|
|
104
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
105
|
+
if (args[index] !== flag) continue;
|
|
106
|
+
const value = args[index + 1];
|
|
107
|
+
if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
|
|
108
|
+
throw new TypeError(`Missing value for ${flag}`);
|
|
109
|
+
}
|
|
110
|
+
values.push(value);
|
|
111
|
+
}
|
|
112
|
+
if (values.length > 1) throw new TypeError(`Duplicate ${flag}`);
|
|
113
|
+
return values[0] ?? null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function environmentValue(env, name) {
|
|
117
|
+
const value = env?.[name];
|
|
118
|
+
if (value === undefined) return null;
|
|
119
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
120
|
+
throw new TypeError(`${name} must not be empty`);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function openSqliteStore(dbPath) {
|
|
126
|
+
const deadline = Date.now() + STORE_OPEN_TIMEOUT_MS;
|
|
127
|
+
while (true) {
|
|
128
|
+
try {
|
|
129
|
+
return new SqliteStore(dbPath);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!isDatabaseBusy(error) || Date.now() >= deadline) throw error;
|
|
132
|
+
Atomics.wait(RETRY_BUFFER, 0, 0, RETRY_DELAY_MS);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isDatabaseBusy(error) {
|
|
138
|
+
return error?.code === 'SQLITE_BUSY' || error?.code === 'SQLITE_LOCKED';
|
|
139
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const DETECTORS = [
|
|
2
|
+
['private_key', /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/i],
|
|
3
|
+
['api_key', /\b(?:sk-[A-Za-z0-9_-]{12,}|sk_(?:live|test)_[A-Za-z0-9]{12,}|rk_(?:live|test)_[A-Za-z0-9]{12,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{20,}|AKIA[A-Z0-9]{16}|ASIA[A-Z0-9]{16}|xox[baprs]-[A-Za-z0-9-]{20,}|AIza[A-Za-z0-9_-]{20,}|ya29\.[A-Za-z0-9_-]{20,}|SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{32,})\b/],
|
|
4
|
+
['aws_credential_assignment', /\bAWS_(?:SECRET_ACCESS_KEY|SESSION_TOKEN)\s*[:=]\s*(?:"[^"]{16,}"|'[^']{16,}'|[A-Za-z0-9/+=]{16,})/i],
|
|
5
|
+
['bearer_token', /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i],
|
|
6
|
+
['jwt', /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\b/],
|
|
7
|
+
['credential_assignment', /\b(?:password|passwd|passphrase|secret|api[-_ ]?key)\b\s*[:=]\s*(?:"[^"]+"|'[^']+'|[^\s,]+)/i],
|
|
8
|
+
['token_assignment', /\b(?:access[-_ ]?token|auth[-_ ]?token|token)\b\s*[:=]\s*(?:"[^"]{16,}"|'[^']{16,}'|[A-Za-z0-9._~+/=-]{16,})/i]
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function detectSensitiveContent(text) {
|
|
12
|
+
if (typeof text !== 'string') return { restricted: false, reasons: [] };
|
|
13
|
+
|
|
14
|
+
const reasons = DETECTORS
|
|
15
|
+
.filter(([, pattern]) => pattern.test(text))
|
|
16
|
+
.map(([reason]) => reason);
|
|
17
|
+
return { restricted: reasons.length > 0, reasons };
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createApplication } from '../app/create-application.js';
|
|
2
|
+
import { openFederatedGraphApplication } from '../graphiti/federated-application.js';
|
|
3
|
+
import { openLocalApplication } from '../runtime-options.js';
|
|
4
|
+
|
|
5
|
+
export function createServerApplication({
|
|
6
|
+
app,
|
|
7
|
+
store,
|
|
8
|
+
dbPath,
|
|
9
|
+
runtimeConfigPath,
|
|
10
|
+
personalSpaceName,
|
|
11
|
+
closeApplicationOnShutdown
|
|
12
|
+
}) {
|
|
13
|
+
const application = app ?? (store
|
|
14
|
+
? createApplication({ store, activePersonalSpaceName: personalSpaceName })
|
|
15
|
+
: dbPath
|
|
16
|
+
? openLocalApplication({ dbPath, personalSpaceName })
|
|
17
|
+
: openFederatedGraphApplication({ runtimeConfigPath }));
|
|
18
|
+
const owned = closeApplicationOnShutdown ?? (!app && !store);
|
|
19
|
+
return {
|
|
20
|
+
application,
|
|
21
|
+
close: closeOnce(() => {
|
|
22
|
+
if (owned) return application.close();
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function closeOnce(close) {
|
|
28
|
+
let closed = false;
|
|
29
|
+
return () => {
|
|
30
|
+
if (closed) return;
|
|
31
|
+
closed = true;
|
|
32
|
+
return close();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function listenServer(server, port, host = '127.0.0.1') {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const cleanup = () => {
|
|
4
|
+
server.off('error', onError);
|
|
5
|
+
server.off('listening', onListening);
|
|
6
|
+
};
|
|
7
|
+
const onError = (error) => {
|
|
8
|
+
cleanup();
|
|
9
|
+
reject(error);
|
|
10
|
+
};
|
|
11
|
+
const onListening = () => {
|
|
12
|
+
cleanup();
|
|
13
|
+
resolve();
|
|
14
|
+
};
|
|
15
|
+
server.once('error', onError);
|
|
16
|
+
server.once('listening', onListening);
|
|
17
|
+
try {
|
|
18
|
+
server.listen(port, host);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
cleanup();
|
|
21
|
+
reject(error);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { handleApiRequest } from './http/api-router.js';
|
|
6
|
+
import { mapHttpError } from './http/error-mapping.js';
|
|
7
|
+
import { localServerAuthority, rejectDisallowedRequest } from './http/request-policy.js';
|
|
8
|
+
import { sendJson } from './http/response.js';
|
|
9
|
+
import { serveStatic } from './http/static-handler.js';
|
|
10
|
+
import { DEFAULT_FULI_PORT } from './defaults.js';
|
|
11
|
+
import { resolveGraphRuntimeOptions } from './graphiti/runtime-config.js';
|
|
12
|
+
import { createServerApplication } from './server/application-lifecycle.js';
|
|
13
|
+
import { listenServer } from './server/listen.js';
|
|
14
|
+
|
|
15
|
+
const FETCH_BLOCKED_PORTS = new Set([
|
|
16
|
+
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
|
|
17
|
+
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
|
|
18
|
+
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
|
|
19
|
+
995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668,
|
|
20
|
+
6669, 6697, 10080
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export async function createServer(options = {}) {
|
|
24
|
+
const {
|
|
25
|
+
port = DEFAULT_FULI_PORT,
|
|
26
|
+
store,
|
|
27
|
+
app,
|
|
28
|
+
closeApplicationOnShutdown,
|
|
29
|
+
isBlockedPort = isFetchBlockedPort
|
|
30
|
+
} = options;
|
|
31
|
+
if (Number(port) !== 0 && isBlockedPort(port)) {
|
|
32
|
+
throw new Error(`Port ${port} is blocked for local fetch`);
|
|
33
|
+
}
|
|
34
|
+
const legacyRuntime = app || store || Object.hasOwn(options, 'dbPath');
|
|
35
|
+
const localOptions = legacyRuntime
|
|
36
|
+
? { dbPath: options.dbPath, personalSpaceName: options.personalSpaceName ?? '我' }
|
|
37
|
+
: resolveServerGraphOptions(options);
|
|
38
|
+
const runtime = createServerApplication({
|
|
39
|
+
app,
|
|
40
|
+
store,
|
|
41
|
+
...localOptions,
|
|
42
|
+
closeApplicationOnShutdown
|
|
43
|
+
});
|
|
44
|
+
const application = runtime.application;
|
|
45
|
+
|
|
46
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
47
|
+
let authority = null;
|
|
48
|
+
const server = createHttpServer((request, response) => {
|
|
49
|
+
handleRequest({ request, response, app: application, authority }).catch((error) => {
|
|
50
|
+
const mapped = mapHttpError(error);
|
|
51
|
+
sendJson(response, mapped.status, mapped.body);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await listenServer(server, port);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
runtime.close();
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
const address = server.address();
|
|
62
|
+
if (isBlockedPort(address.port)) {
|
|
63
|
+
await new Promise((resolveClose) => server.close(resolveClose));
|
|
64
|
+
if (Number(port) === 0) continue;
|
|
65
|
+
runtime.close();
|
|
66
|
+
throw new Error(`Port ${address.port} is blocked for local fetch`);
|
|
67
|
+
}
|
|
68
|
+
authority = localServerAuthority(address);
|
|
69
|
+
if (!authority) {
|
|
70
|
+
await new Promise((resolveClose) => server.close(resolveClose));
|
|
71
|
+
runtime.close();
|
|
72
|
+
throw new Error('Server did not bind to a valid local authority');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
server.once('close', runtime.close);
|
|
76
|
+
return { server, url: `http://${authority}`, app: application };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
runtime.close();
|
|
80
|
+
throw new Error('Could not find a fetchable local port');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveServerGraphOptions(options) {
|
|
84
|
+
const args = [];
|
|
85
|
+
if (Object.hasOwn(options, 'runtimeConfigPath')) {
|
|
86
|
+
args.push('--runtime-config', options.runtimeConfigPath);
|
|
87
|
+
}
|
|
88
|
+
return resolveGraphRuntimeOptions(args, options.env ?? process.env, {
|
|
89
|
+
setupPaths: { homeDir: options.homeDir, cwd: options.cwd ?? process.cwd() }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isFetchBlockedPort(port) {
|
|
94
|
+
return FETCH_BLOCKED_PORTS.has(Number(port));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function handleRequest({ request, response, app, authority }) {
|
|
98
|
+
if (rejectDisallowedRequest({ request, response, authority })) return;
|
|
99
|
+
if (await handleApiRequest({ request, response, app })) return;
|
|
100
|
+
serveStatic(new URL(request.url, 'http://127.0.0.1').pathname, response);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
104
|
+
runProgram(process.argv.slice(2)).catch((error) => {
|
|
105
|
+
console.error(error.message);
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function runProgram(args) {
|
|
111
|
+
const runtimeOptions = resolveGraphRuntimeOptions(args, process.env);
|
|
112
|
+
const port = numberOption(args, '--port', DEFAULT_FULI_PORT);
|
|
113
|
+
const { server, url } = await createServer({ ...runtimeOptions, port });
|
|
114
|
+
const handlers = new Map();
|
|
115
|
+
const close = () => server.close();
|
|
116
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
117
|
+
const handler = close;
|
|
118
|
+
handlers.set(signal, handler);
|
|
119
|
+
process.once(signal, handler);
|
|
120
|
+
}
|
|
121
|
+
server.once('close', () => {
|
|
122
|
+
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
123
|
+
});
|
|
124
|
+
console.log(`复利工作台 running at ${url}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function numberOption(args, flag, fallback) {
|
|
128
|
+
const index = args.indexOf(flag);
|
|
129
|
+
if (index === -1) return fallback;
|
|
130
|
+
const value = args[index + 1];
|
|
131
|
+
if (!value || value.startsWith('--') || !Number.isFinite(Number(value))) {
|
|
132
|
+
throw new TypeError(`Missing or invalid value for ${flag}`);
|
|
133
|
+
}
|
|
134
|
+
return Number(value);
|
|
135
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { replaceFuliTable } from './codex-config.js';
|
|
4
|
+
import { isCodexBootstrapCurrent } from './codex-bootstrap.js';
|
|
5
|
+
import { isSessionSkillCurrent } from './session-skill.js';
|
|
6
|
+
import { readJsonFile } from '../storage/json-file.js';
|
|
7
|
+
|
|
8
|
+
export function inspectAgentInstallations(agents, context, {
|
|
9
|
+
readText = defaultReadText,
|
|
10
|
+
readJson = defaultReadJson,
|
|
11
|
+
fileExists = existsSync,
|
|
12
|
+
skillCurrent = isSessionSkillCurrent,
|
|
13
|
+
bootstrapCurrent = isCodexBootstrapCurrent
|
|
14
|
+
} = {}) {
|
|
15
|
+
return agents.map((agent) => {
|
|
16
|
+
const registration = inspectRegistration(agent, context, { readText, readJson });
|
|
17
|
+
const skillPairs = [
|
|
18
|
+
[context.sessionSkillPath, agent.skillPath],
|
|
19
|
+
[context.projectSkillPath, agent.projectSkillPath]
|
|
20
|
+
];
|
|
21
|
+
const skillsPresent = skillPairs.some(([, targetPath]) => fileExists(targetPath));
|
|
22
|
+
const skillsCurrent = skillPairs.every(
|
|
23
|
+
([sourcePath, targetPath]) => skillCurrent(sourcePath, targetPath)
|
|
24
|
+
);
|
|
25
|
+
const codexBootstrapCurrent = agent.id !== 'codex' || bootstrapCurrent(
|
|
26
|
+
agent,
|
|
27
|
+
context,
|
|
28
|
+
{ fileExists, readText }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
let integrationStatus = 'not_connected';
|
|
32
|
+
if (registration.current && skillsCurrent && codexBootstrapCurrent) {
|
|
33
|
+
integrationStatus = 'connected';
|
|
34
|
+
}
|
|
35
|
+
else if (registration.present || skillsPresent) integrationStatus = 'update_available';
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...agent,
|
|
39
|
+
integrationStatus,
|
|
40
|
+
integrationDetails: {
|
|
41
|
+
mcp: registration.current ? 'current' : registration.present ? 'outdated' : 'missing',
|
|
42
|
+
skills: skillsCurrent ? 'current' : skillsPresent ? 'outdated' : 'missing',
|
|
43
|
+
...(agent.id === 'codex'
|
|
44
|
+
? { bootstrap: codexBootstrapCurrent ? 'current' : 'outdated' }
|
|
45
|
+
: {})
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inspectRegistration(agent, context, readers) {
|
|
52
|
+
const expected = {
|
|
53
|
+
command: context.nodePath,
|
|
54
|
+
args: [context.mcpServerPath, '--runtime-config', context.runtimeConfigPath]
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
if (agent.id === 'codex') {
|
|
58
|
+
const current = readers.readText(agent.configPath);
|
|
59
|
+
const present = /^\s*\[mcp_servers\.(?:fuli|"fuli"|'fuli')]/m.test(current);
|
|
60
|
+
return {
|
|
61
|
+
present,
|
|
62
|
+
current: present && normalizeText(replaceFuliTable(current, expected)) ===
|
|
63
|
+
normalizeText(current)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const config = readers.readJson(agent.configPath);
|
|
68
|
+
const server = config?.mcpServers?.fuli;
|
|
69
|
+
const present = Boolean(server && typeof server === 'object' && !Array.isArray(server));
|
|
70
|
+
return {
|
|
71
|
+
present,
|
|
72
|
+
current: present &&
|
|
73
|
+
server.command === expected.command &&
|
|
74
|
+
sameStrings(server.args, expected.args)
|
|
75
|
+
};
|
|
76
|
+
} catch {
|
|
77
|
+
return { present: false, current: false };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sameStrings(actual, expected) {
|
|
82
|
+
return Array.isArray(actual) &&
|
|
83
|
+
actual.length === expected.length &&
|
|
84
|
+
actual.every((value, index) => value === expected[index]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeText(value) {
|
|
88
|
+
return value.replaceAll('\r\n', '\n').trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function defaultReadText(path) {
|
|
92
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function defaultReadJson(path) {
|
|
96
|
+
return readJsonFile(path, {});
|
|
97
|
+
}
|