@factlayer/adapter-cognee 0.7.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/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @factlayer/adapter-cognee
2
+
3
+ A [FactLayer](https://github.com/Rubber-Duck-Screaming/factlayer) adapter for [Cognee](https://www.cognee.ai): pulls a dataset's ingested source records, maps them to FactLayer facts, persists them locally (keyed by Cognee's own data id), and checks each one for freshness.
4
+
5
+ Cognee is different from Mem0 and Zep in one important way: it's self-hosted, not a hosted service you just read from. *You* run the Cognee instance and configure your own LLM provider (any OpenAI-compatible endpoint) to do the ingestion and knowledge-graph extraction. This adapter doesn't provide an LLM and doesn't run extraction itself — it only reads records you've already fed into your own Cognee instance, tracking the freshness of that ingested source material.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @factlayer/adapter-cognee
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createCogneeClient, scanCognee } from "@factlayer/adapter-cognee";
17
+
18
+ const client = await createCogneeClient({
19
+ llmModel: "your-model-id",
20
+ llmApiKey: process.env.YOUR_LLM_KEY,
21
+ // llmEndpoint: process.env.YOUR_LLM_ENDPOINT, // only needed for a non-OpenAI, OpenAI-compatible provider
22
+ });
23
+
24
+ // datasetId is Cognee's own dataset UUID, not the dataset name you passed
25
+ // to Cognee's add()/remember() calls -- look it up via the underlying
26
+ // @cognee/cognee-ts SDK's datasets.list() if you only have the name.
27
+ const results = await scanCognee(client, datasetId);
28
+ // each result: { fact, result: { status: "fresh" | "needs-verification", ... } }
29
+ ```
30
+
31
+ See the [main repo](https://github.com/Rubber-Duck-Screaming/factlayer) for the full architecture, the freshness model, and how this adapter fits alongside `@factlayer/core`, `@factlayer/cli`, and `@factlayer/mcp-server`.
@@ -0,0 +1,2 @@
1
+ import type { CogneeClient } from "./types.ts";
2
+ export declare function createCogneeClient(settings?: object | string): Promise<CogneeClient>;
package/dist/client.js ADDED
@@ -0,0 +1,32 @@
1
+ import { Cognee, init } from "@cognee/cognee-ts";
2
+ let initialized = false;
3
+ // Boots the Rust async runtime that every Cognee pipeline call depends on
4
+ // (Cognee's own usage notes call this out as required once per process,
5
+ // before any async op). Guarded here rather than left to callers, so
6
+ // createCogneeClient can be called more than once -- e.g. once per scan --
7
+ // without re-initializing the runtime.
8
+ function ensureInit() {
9
+ if (!initialized) {
10
+ init();
11
+ initialized = true;
12
+ }
13
+ }
14
+ // Wraps the real @cognee/cognee-ts Cognee class so it satisfies the minimal
15
+ // CogneeClient shape scanCognee() depends on. Keeping scan.ts decoupled
16
+ // from the real SDK class is what lets the automated test suite inject a
17
+ // fake client instead of making real (LLM/embedding-backed) calls (see
18
+ // scan.test.ts). This file is exercised only by a manual smoke test
19
+ // against a real Cognee instance, not by `bun test`.
20
+ //
21
+ // Async (unlike adapter-mem0/adapter-zep's sync client factories) because
22
+ // Cognee's own docs require warm() -- which builds the embedding/LLM
23
+ // engines and resolves the default user -- to run once after construction
24
+ // and before any other call.
25
+ export async function createCogneeClient(settings) {
26
+ ensureInit();
27
+ const client = new Cognee(settings);
28
+ await client.warm();
29
+ return {
30
+ listData: (datasetId) => client.datasets.listData(datasetId),
31
+ };
32
+ }
@@ -0,0 +1,6 @@
1
+ export type { CogneeClient, CogneeDataRecord } from "./types.ts";
2
+ export { toFact } from "./mapper";
3
+ export type { CogneeFact } from "./mapper.ts";
4
+ export { scanCognee } from "./scan";
5
+ export type { ScannedFact } from "./scan.ts";
6
+ export { createCogneeClient } from "./client";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { toFact } from "./mapper";
2
+ export { scanCognee } from "./scan";
3
+ export { createCogneeClient } from "./client";
@@ -0,0 +1,6 @@
1
+ import type { Fact } from "@factlayer/core";
2
+ import type { CogneeDataRecord } from "./types.ts";
3
+ export interface CogneeFact extends Fact {
4
+ importanceWeight?: number | null;
5
+ }
6
+ export declare function toFact(record: CogneeDataRecord, now?: number): Promise<CogneeFact>;
package/dist/mapper.js ADDED
@@ -0,0 +1,83 @@
1
+ // Design note: Cognee's search()/recall() -- its extracted knowledge-graph
2
+ // facts -- carry no timestamp fields in @cognee/cognee-ts's current types
3
+ // (CogneeSearchItem is just `{ id, score, payload }`; CogneeRecallItem is
4
+ // just `{ source, content, score }`), making them unusable as a freshness
5
+ // source. This adapter instead uses datasets.listData(datasetId), which
6
+ // returns pre-extraction raw ingested records (CogneeDataRecord) that do
7
+ // carry real timestamps. This means the Cognee adapter tracks freshness of
8
+ // ingested source material, not extracted graph facts -- a real scope
9
+ // difference from adapter-mem0 and adapter-zep, documented here rather
10
+ // than hidden.
11
+ //
12
+ // A second consequence of that same choice: CogneeDataRecord has no inline
13
+ // text-content field. `name` looked like a plausible stand-in at first, but
14
+ // live output against a real Cognee instance confirmed it's actually an
15
+ // internal hash-based filename (e.g.
16
+ // "text_b746e53fca48a4c932a25ac28fbdb0c2"), not the fact's content -- using
17
+ // it as `text` would have made the adapter report gibberish instead of
18
+ // facts. The real content lives at `raw_data_location`, a `file://` URI
19
+ // pointing at a local file on disk, so toFact() reads that file directly.
20
+ //
21
+ // Assumption, not a guarantee: this only works when the FactLayer process
22
+ // can read Cognee's local storage on the same filesystem -- true for our
23
+ // current self-hosted setup (both run on one machine), but it would break
24
+ // if Cognee and FactLayer ever ran on separate machines without a shared
25
+ // mount. Documented here rather than silently assumed.
26
+ import { readFile } from "node:fs/promises";
27
+ import { classify } from "@factlayer/core";
28
+ const FILE_URI_PREFIX = "file://";
29
+ // Cognee's raw_data_location is a `file://` URI, but on Windows the path
30
+ // after the scheme keeps its native backslashes instead of being
31
+ // forward-slash-encoded like a spec-compliant file URI would be (a real
32
+ // one is "file:///C:/Users/...", three slashes, forward-slash-separated;
33
+ // Cognee's own output is "file://C:\\Users\\...\\text_....txt", two
34
+ // slashes, backslash-separated). Parsing that with `new URL()` /
35
+ // `fileURLToPath()` would treat "C:" as the URL's host and silently drop
36
+ // it from the resulting path, corrupting the drive letter. So this strips
37
+ // the "file://" prefix as a plain string instead of URL-parsing it, and
38
+ // hands the remainder to fs as-is -- Node's fs accepts either slash
39
+ // direction on Windows.
40
+ function fileUriToPath(uri) {
41
+ return uri.startsWith(FILE_URI_PREFIX) ? uri.slice(FILE_URI_PREFIX.length) : uri;
42
+ }
43
+ function toMs(value, fallback) {
44
+ if (!value)
45
+ return fallback;
46
+ const ms = Date.parse(value);
47
+ return Number.isNaN(ms) ? fallback : ms;
48
+ }
49
+ // Converts a Cognee data record (a pre-extraction ingested item, not an
50
+ // extracted graph fact -- see the module comment above) into a factlayer
51
+ // Fact.
52
+ //
53
+ // - storedAt comes from created_at (when the item was ingested).
54
+ // - lastVerifiedAt comes from updated_at, falling back to created_at when
55
+ // updated_at is null. Deliberately NOT last_accessed: that field is an
56
+ // access-frequency signal (how often Cognee's own code read the record),
57
+ // not a truth-verification signal -- treating "read recently" as "still
58
+ // true" is exactly the mistake factlayer exists to catch.
59
+ // - text is read from the file at raw_data_location (see the module
60
+ // comment above for why, and the same-filesystem assumption this
61
+ // depends on) -- NOT record.name, which is an internal hash-based
62
+ // filename, not fact content.
63
+ // - category always comes from classify() on that file content -- Cognee
64
+ // doesn't expose a comparable freeform category on data records.
65
+ // - expiresAt is always null: CogneeDataRecord has no expiration-style
66
+ // field, so check() always falls through to the category half-life math.
67
+ // - importanceWeight passes through record.importance_weight as-is
68
+ // (undocumented and nullable in Cognee's own types) for callers who want
69
+ // it, without factoring into check() at all.
70
+ export async function toFact(record, now = Date.now()) {
71
+ const text = (await readFile(fileUriToPath(record.raw_data_location), "utf-8")).trim();
72
+ const storedAt = toMs(record.created_at, now);
73
+ const lastVerifiedAt = toMs(record.updated_at, storedAt);
74
+ return {
75
+ id: record.id,
76
+ text,
77
+ category: await classify(text),
78
+ storedAt,
79
+ lastVerifiedAt,
80
+ expiresAt: null,
81
+ importanceWeight: record.importance_weight,
82
+ };
83
+ }
package/dist/scan.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { CheckResult } from "@factlayer/core";
2
+ import type { CogneeFact } from "./mapper.ts";
3
+ import type { CogneeClient } from "./types.ts";
4
+ export interface ScannedFact {
5
+ fact: CogneeFact;
6
+ result: CheckResult;
7
+ }
8
+ export declare function scanCognee(client: CogneeClient, datasetId: string, now?: number): Promise<ScannedFact[]>;
package/dist/scan.js ADDED
@@ -0,0 +1,17 @@
1
+ import { addFact, check } from "@factlayer/core";
2
+ import { toFact } from "./mapper";
3
+ // Pulls raw ingested records for a dataset via datasets.listData() -- see
4
+ // mapper.ts's design note for why this is source material, not extracted
5
+ // graph facts -- maps each to a Fact, persists it locally (upserted via
6
+ // addFact, keyed by Cognee's own data id so the two systems share one id
7
+ // space), and runs factlayer's check() on it. Never writes anything back to
8
+ // Cognee itself -- only to factlayer's local store, which is what lets
9
+ // mark_verified act on Cognee-sourced facts afterward.
10
+ export async function scanCognee(client, datasetId, now = Date.now()) {
11
+ const records = await client.listData(datasetId);
12
+ return Promise.all(records.map(async (record) => {
13
+ const fact = await toFact(record, now);
14
+ await addFact(fact);
15
+ return { fact, result: check(fact, now) };
16
+ }));
17
+ }
@@ -0,0 +1,12 @@
1
+ export interface CogneeDataRecord {
2
+ id: string;
3
+ name: string;
4
+ raw_data_location: string;
5
+ created_at: string;
6
+ updated_at: string | null;
7
+ last_accessed: string | null;
8
+ importance_weight: number | null;
9
+ }
10
+ export interface CogneeClient {
11
+ listData(datasetId: string): Promise<CogneeDataRecord[]>;
12
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@factlayer/adapter-cognee",
3
+ "version": "0.7.0",
4
+ "description": "Cognee adapter for FactLayer: scans, persists, and checks freshness of Cognee-ingested source records.",
5
+ "keywords": ["ai", "agent-memory", "llm", "freshness", "cognee", "factlayer"],
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": ["dist"],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "test": "bun test",
15
+ "build": "tsc -p tsconfig.json",
16
+ "prepublishOnly": "bun run ../../scripts/check-publish-ready.ts"
17
+ },
18
+ "dependencies": {
19
+ "@factlayer/core": "0.6.0",
20
+ "@cognee/cognee-ts": "^0.2.0"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.6.3"
24
+ }
25
+ }