recallnest 2.2.1 → 2.3.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/CLAUDE.md +1 -1
- package/README.md +30 -5
- package/README_CN.md +30 -5
- package/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- package/logs/daily-recall.log +30 -0
- package/logs/ingest-2026-04-13.log +87 -0
- package/package.json +1 -1
- package/src/cli.ts +30 -0
- package/src/connector-types.ts +57 -0
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +31 -1
- package/src/doctor.ts +29 -0
- package/src/ingest.ts +203 -0
- package/src/mcp-server.ts +2 -2
- package/src/obsidian-connector.ts +282 -0
- package/src/session-schema.ts +2 -2
- package/src/source-heartbeat.ts +108 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GB-3: Source Heartbeat Tracking
|
|
3
|
+
*
|
|
4
|
+
* Every connector ingest writes a heartbeat record to data/source-heartbeat.json.
|
|
5
|
+
* data_checkup and doctor --ci read heartbeats to detect stale data sources.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { resolve, dirname } from "node:path";
|
|
10
|
+
import { metaDir } from "./compat.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface SourceHeartbeat {
|
|
17
|
+
source: string;
|
|
18
|
+
lastIngest: string; // ISO 8601
|
|
19
|
+
recordsIngested: number;
|
|
20
|
+
errors: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type HeartbeatFile = Record<string, SourceHeartbeat>;
|
|
24
|
+
|
|
25
|
+
export interface StaleSource {
|
|
26
|
+
source: string;
|
|
27
|
+
lastIngest: string;
|
|
28
|
+
daysSince: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Path
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const HEARTBEAT_PATH = resolve(metaDir(import.meta), "../data/source-heartbeat.json");
|
|
36
|
+
|
|
37
|
+
/** Visible for testing — override the default path. */
|
|
38
|
+
export function heartbeatPath(): string {
|
|
39
|
+
return HEARTBEAT_PATH;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Read / Write
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export function readHeartbeats(path?: string): HeartbeatFile {
|
|
47
|
+
const p = path ?? HEARTBEAT_PATH;
|
|
48
|
+
if (!existsSync(p)) return {};
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(p, "utf-8")) as HeartbeatFile;
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function writeHeartbeat(
|
|
57
|
+
source: string,
|
|
58
|
+
recordsIngested: number,
|
|
59
|
+
errors: string[],
|
|
60
|
+
path?: string,
|
|
61
|
+
): void {
|
|
62
|
+
const p = path ?? HEARTBEAT_PATH;
|
|
63
|
+
const dir = dirname(p);
|
|
64
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
const existing = readHeartbeats(p);
|
|
67
|
+
existing[source] = {
|
|
68
|
+
source,
|
|
69
|
+
lastIngest: new Date().toISOString(),
|
|
70
|
+
recordsIngested,
|
|
71
|
+
errors: errors.slice(0, 10), // cap stored errors
|
|
72
|
+
};
|
|
73
|
+
writeFileSync(p, JSON.stringify(existing, null, 2) + "\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Staleness check
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
const MS_PER_DAY = 86_400_000;
|
|
81
|
+
|
|
82
|
+
export function checkSourceStaleness(maxDays = 7, path?: string): StaleSource[] {
|
|
83
|
+
const heartbeats = readHeartbeats(path);
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const stale: StaleSource[] = [];
|
|
86
|
+
|
|
87
|
+
for (const hb of Object.values(heartbeats)) {
|
|
88
|
+
const lastMs = Date.parse(hb.lastIngest);
|
|
89
|
+
if (Number.isNaN(lastMs)) continue;
|
|
90
|
+
const daysSince = Math.floor((now - lastMs) / MS_PER_DAY);
|
|
91
|
+
if (daysSince > maxDays) {
|
|
92
|
+
stale.push({ source: hb.source, lastIngest: hb.lastIngest, daysSince });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return stale.sort((a, b) => b.daysSince - a.daysSince);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Human-readable age string, e.g. "2h ago", "3d ago". */
|
|
100
|
+
export function formatAge(isoDate: string): string {
|
|
101
|
+
const ms = Date.now() - Date.parse(isoDate);
|
|
102
|
+
if (ms < 0) return "just now";
|
|
103
|
+
const hours = Math.floor(ms / 3_600_000);
|
|
104
|
+
if (hours < 1) return "just now";
|
|
105
|
+
if (hours < 24) return `${hours}h ago`;
|
|
106
|
+
const days = Math.floor(hours / 24);
|
|
107
|
+
return `${days}d ago`;
|
|
108
|
+
}
|