@inerrata/channel 0.3.5 → 0.3.7
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/dist/api-client.d.ts +14 -0
- package/dist/api-client.js +96 -0
- package/dist/cache.d.ts +64 -0
- package/dist/cache.js +169 -0
- package/dist/heartbeat.d.ts +3 -0
- package/dist/heartbeat.js +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -366
- package/dist/mcp-server.d.ts +39 -0
- package/dist/mcp-server.js +124 -0
- package/dist/notification-buffer.d.ts +18 -0
- package/dist/notification-buffer.js +118 -0
- package/dist/openclaw.js +1 -1
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.js +70 -0
- package/dist/stream-relay.d.ts +4 -0
- package/dist/stream-relay.js +356 -0
- package/package.json +5 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API fetch helper — wraps fetch with auth headers for the inErrata API.
|
|
3
|
+
* GET requests are cached with an LRU strategy + per-prefix TTLs.
|
|
4
|
+
*/
|
|
5
|
+
declare const MCP_BASE: string;
|
|
6
|
+
declare const API_KEY: string;
|
|
7
|
+
export { MCP_BASE, API_KEY };
|
|
8
|
+
/** Invalidate cache entries matching a base resource path. */
|
|
9
|
+
export declare function invalidateCache(path?: string): void;
|
|
10
|
+
/** Cache stats for diagnostics. */
|
|
11
|
+
export declare function cacheStats(): {
|
|
12
|
+
size: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function apiFetch(path: string, init?: RequestInit): Promise<Response>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API fetch helper — wraps fetch with auth headers for the inErrata API.
|
|
3
|
+
* GET requests are cached with an LRU strategy + per-prefix TTLs.
|
|
4
|
+
*/
|
|
5
|
+
const MCP_BASE = (process.env.ERRATA_API_URL ?? 'https://inerrata-production.up.railway.app').replace('/api/v1', '');
|
|
6
|
+
const API_BASE = MCP_BASE + '/api/v1';
|
|
7
|
+
const API_KEY = process.env.ERRATA_API_KEY ?? '';
|
|
8
|
+
export { MCP_BASE, API_KEY };
|
|
9
|
+
const MAX_CACHE_ENTRIES = 100;
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
/** TTL in ms by path prefix (longest prefix wins). Fallback: 30 min */
|
|
12
|
+
const TTL_BY_PREFIX = [
|
|
13
|
+
['/messages', 60_000], // 1 min — messages change fast
|
|
14
|
+
['/search', 15 * 60_000], // 15 min
|
|
15
|
+
['/graph', 10 * 60_000], // 10 min
|
|
16
|
+
['/questions', 60 * 60_000], // 60 min — questions are stable
|
|
17
|
+
['/me', 5 * 60_000], // 5 min
|
|
18
|
+
['/messages/connections', 60_000], // 1 min — presence changes
|
|
19
|
+
];
|
|
20
|
+
const DEFAULT_TTL = 30 * 60_000;
|
|
21
|
+
function ttlFor(path) {
|
|
22
|
+
for (const [prefix, ttl] of TTL_BY_PREFIX) {
|
|
23
|
+
if (path.startsWith(prefix))
|
|
24
|
+
return ttl;
|
|
25
|
+
}
|
|
26
|
+
return DEFAULT_TTL;
|
|
27
|
+
}
|
|
28
|
+
function evictOldest() {
|
|
29
|
+
if (cache.size < MAX_CACHE_ENTRIES)
|
|
30
|
+
return;
|
|
31
|
+
const oldest = cache.keys().next().value;
|
|
32
|
+
if (oldest !== undefined)
|
|
33
|
+
cache.delete(oldest);
|
|
34
|
+
}
|
|
35
|
+
/** Invalidate cache entries matching a base resource path. */
|
|
36
|
+
export function invalidateCache(path) {
|
|
37
|
+
if (!path) {
|
|
38
|
+
cache.clear();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const base = '/' + path.split('/').filter(Boolean).slice(0, 1).join('/');
|
|
42
|
+
for (const key of cache.keys()) {
|
|
43
|
+
if (key.startsWith(base))
|
|
44
|
+
cache.delete(key);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Cache stats for diagnostics. */
|
|
48
|
+
export function cacheStats() {
|
|
49
|
+
return { size: cache.size };
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Core fetch
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
export async function apiFetch(path, init) {
|
|
55
|
+
const method = (init?.method ?? 'GET').toUpperCase();
|
|
56
|
+
const isGet = method === 'GET';
|
|
57
|
+
// Check cache for GETs
|
|
58
|
+
if (isGet) {
|
|
59
|
+
const entry = cache.get(path);
|
|
60
|
+
if (entry && Date.now() < entry.expiresAt) {
|
|
61
|
+
return new Response(entry.body, {
|
|
62
|
+
status: entry.status,
|
|
63
|
+
headers: entry.headers,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (entry)
|
|
67
|
+
cache.delete(path); // expired
|
|
68
|
+
}
|
|
69
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
70
|
+
...init,
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
74
|
+
...(init?.headers ?? {}),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
// Cache successful GET responses
|
|
78
|
+
if (isGet && res.ok) {
|
|
79
|
+
const body = await res.text();
|
|
80
|
+
const headers = [...res.headers.entries()];
|
|
81
|
+
evictOldest();
|
|
82
|
+
cache.set(path, {
|
|
83
|
+
body,
|
|
84
|
+
status: res.status,
|
|
85
|
+
headers,
|
|
86
|
+
expiresAt: Date.now() + ttlFor(path),
|
|
87
|
+
});
|
|
88
|
+
// Return a new Response from the cached body (original stream consumed)
|
|
89
|
+
return new Response(body, { status: res.status, headers });
|
|
90
|
+
}
|
|
91
|
+
// Invalidate related cache on writes
|
|
92
|
+
if (!isGet && res.ok) {
|
|
93
|
+
invalidateCache(path);
|
|
94
|
+
}
|
|
95
|
+
return res;
|
|
96
|
+
}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface CacheEntry<T = unknown> {
|
|
2
|
+
key: string;
|
|
3
|
+
value: T;
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
}
|
|
7
|
+
export interface LocalCacheOptions {
|
|
8
|
+
maxEntries?: number;
|
|
9
|
+
defaultTtlMs?: number;
|
|
10
|
+
persistPath?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class LocalCache {
|
|
13
|
+
private entries;
|
|
14
|
+
private maxEntries;
|
|
15
|
+
private defaultTtlMs;
|
|
16
|
+
private persistPath;
|
|
17
|
+
constructor(opts?: LocalCacheOptions);
|
|
18
|
+
/**
|
|
19
|
+
* Get a cached value. Returns undefined on miss or expiry.
|
|
20
|
+
*/
|
|
21
|
+
get<T = unknown>(key: string): T | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Store a value with optional custom TTL.
|
|
24
|
+
*/
|
|
25
|
+
set<T = unknown>(key: string, value: T, ttlMs?: number): void;
|
|
26
|
+
/**
|
|
27
|
+
* Check if a key exists and is not expired.
|
|
28
|
+
*/
|
|
29
|
+
has(key: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Remove a specific key.
|
|
32
|
+
*/
|
|
33
|
+
delete(key: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Clear all entries.
|
|
36
|
+
*/
|
|
37
|
+
clear(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Get cache stats.
|
|
40
|
+
*/
|
|
41
|
+
stats(): {
|
|
42
|
+
size: number;
|
|
43
|
+
maxEntries: number;
|
|
44
|
+
hitRate?: number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Remove all expired entries.
|
|
48
|
+
*/
|
|
49
|
+
prune(): number;
|
|
50
|
+
/**
|
|
51
|
+
* Persist cache to disk (if persistPath configured).
|
|
52
|
+
*/
|
|
53
|
+
saveToDisk(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Load cache from disk (if persistPath configured).
|
|
56
|
+
*/
|
|
57
|
+
private loadFromDisk;
|
|
58
|
+
}
|
|
59
|
+
/** Search result cache — 30 min TTL, 500 entries */
|
|
60
|
+
export declare function createSearchCache(persistPath?: string): LocalCache;
|
|
61
|
+
/** Question/answer cache — 60 min TTL, 200 entries */
|
|
62
|
+
export declare function createQuestionCache(persistPath?: string): LocalCache;
|
|
63
|
+
/** Graph traversal cache — 15 min TTL, 100 entries (graph changes more often) */
|
|
64
|
+
export declare function createGraphCache(persistPath?: string): LocalCache;
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local result cache for the MCP channel client.
|
|
3
|
+
*
|
|
4
|
+
* Caches search results and question lookups in memory with optional
|
|
5
|
+
* filesystem persistence. Agents get instant responses for repeated
|
|
6
|
+
* queries without network round-trips.
|
|
7
|
+
*
|
|
8
|
+
* Design:
|
|
9
|
+
* - LRU eviction (default 500 entries)
|
|
10
|
+
* - TTL per entry (default 30 min for search, 60 min for questions)
|
|
11
|
+
* - Optional disk persistence via JSON file (survives agent restarts)
|
|
12
|
+
* - Cache-first: hit → return cached. Miss → fetch → cache → return.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
|
+
import { dirname } from 'node:path';
|
|
16
|
+
export class LocalCache {
|
|
17
|
+
entries = new Map();
|
|
18
|
+
maxEntries;
|
|
19
|
+
defaultTtlMs;
|
|
20
|
+
persistPath;
|
|
21
|
+
constructor(opts = {}) {
|
|
22
|
+
this.maxEntries = opts.maxEntries ?? 500;
|
|
23
|
+
this.defaultTtlMs = opts.defaultTtlMs ?? 30 * 60 * 1000; // 30 min
|
|
24
|
+
this.persistPath = opts.persistPath ?? null;
|
|
25
|
+
if (this.persistPath) {
|
|
26
|
+
this.loadFromDisk();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get a cached value. Returns undefined on miss or expiry.
|
|
31
|
+
*/
|
|
32
|
+
get(key) {
|
|
33
|
+
const entry = this.entries.get(key);
|
|
34
|
+
if (!entry)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (Date.now() > entry.expiresAt) {
|
|
37
|
+
this.entries.delete(key);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
// Move to end (LRU refresh)
|
|
41
|
+
this.entries.delete(key);
|
|
42
|
+
this.entries.set(key, entry);
|
|
43
|
+
return entry.value;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Store a value with optional custom TTL.
|
|
47
|
+
*/
|
|
48
|
+
set(key, value, ttlMs) {
|
|
49
|
+
// Evict oldest if at capacity
|
|
50
|
+
if (this.entries.size >= this.maxEntries) {
|
|
51
|
+
const oldest = this.entries.keys().next().value;
|
|
52
|
+
if (oldest !== undefined)
|
|
53
|
+
this.entries.delete(oldest);
|
|
54
|
+
}
|
|
55
|
+
this.entries.set(key, {
|
|
56
|
+
key,
|
|
57
|
+
value,
|
|
58
|
+
createdAt: Date.now(),
|
|
59
|
+
expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if a key exists and is not expired.
|
|
64
|
+
*/
|
|
65
|
+
has(key) {
|
|
66
|
+
return this.get(key) !== undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Remove a specific key.
|
|
70
|
+
*/
|
|
71
|
+
delete(key) {
|
|
72
|
+
return this.entries.delete(key);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Clear all entries.
|
|
76
|
+
*/
|
|
77
|
+
clear() {
|
|
78
|
+
this.entries.clear();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get cache stats.
|
|
82
|
+
*/
|
|
83
|
+
stats() {
|
|
84
|
+
return {
|
|
85
|
+
size: this.entries.size,
|
|
86
|
+
maxEntries: this.maxEntries,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Remove all expired entries.
|
|
91
|
+
*/
|
|
92
|
+
prune() {
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
let pruned = 0;
|
|
95
|
+
for (const [key, entry] of this.entries) {
|
|
96
|
+
if (now > entry.expiresAt) {
|
|
97
|
+
this.entries.delete(key);
|
|
98
|
+
pruned++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return pruned;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Persist cache to disk (if persistPath configured).
|
|
105
|
+
*/
|
|
106
|
+
saveToDisk() {
|
|
107
|
+
if (!this.persistPath)
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
const dir = dirname(this.persistPath);
|
|
111
|
+
if (!existsSync(dir))
|
|
112
|
+
mkdirSync(dir, { recursive: true });
|
|
113
|
+
// Only persist non-expired entries
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const data = Array.from(this.entries.values()).filter(e => e.expiresAt > now);
|
|
116
|
+
writeFileSync(this.persistPath, JSON.stringify(data), 'utf-8');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Disk persistence is best-effort
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Load cache from disk (if persistPath configured).
|
|
124
|
+
*/
|
|
125
|
+
loadFromDisk() {
|
|
126
|
+
if (!this.persistPath || !existsSync(this.persistPath))
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
const raw = readFileSync(this.persistPath, 'utf-8');
|
|
130
|
+
const data = JSON.parse(raw);
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
for (const entry of data) {
|
|
133
|
+
if (entry.expiresAt > now) {
|
|
134
|
+
this.entries.set(entry.key, entry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Corrupted cache file — ignore
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Pre-configured caches for common use cases
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
/** Search result cache — 30 min TTL, 500 entries */
|
|
147
|
+
export function createSearchCache(persistPath) {
|
|
148
|
+
return new LocalCache({
|
|
149
|
+
maxEntries: 500,
|
|
150
|
+
defaultTtlMs: 30 * 60 * 1000,
|
|
151
|
+
persistPath,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** Question/answer cache — 60 min TTL, 200 entries */
|
|
155
|
+
export function createQuestionCache(persistPath) {
|
|
156
|
+
return new LocalCache({
|
|
157
|
+
maxEntries: 200,
|
|
158
|
+
defaultTtlMs: 60 * 60 * 1000,
|
|
159
|
+
persistPath,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Graph traversal cache — 15 min TTL, 100 entries (graph changes more often) */
|
|
163
|
+
export function createGraphCache(persistPath) {
|
|
164
|
+
return new LocalCache({
|
|
165
|
+
maxEntries: 100,
|
|
166
|
+
defaultTtlMs: 15 * 60 * 1000,
|
|
167
|
+
persistPath,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel heartbeat — tells the server this agent's channel plugin is alive.
|
|
3
|
+
* The server uses this to fire online/offline status notifications to the
|
|
4
|
+
* agent's confirmed connections.
|
|
5
|
+
*/
|
|
6
|
+
import { apiFetch } from './api-client.js';
|
|
7
|
+
const HEARTBEAT_INTERVAL_MS = 60_000;
|
|
8
|
+
let heartbeatTimer;
|
|
9
|
+
export function startHeartbeat() {
|
|
10
|
+
if (heartbeatTimer)
|
|
11
|
+
return;
|
|
12
|
+
// Fire immediately, then repeat
|
|
13
|
+
sendHeartbeat();
|
|
14
|
+
heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
|
|
15
|
+
}
|
|
16
|
+
export function stopHeartbeat() {
|
|
17
|
+
if (heartbeatTimer) {
|
|
18
|
+
clearInterval(heartbeatTimer);
|
|
19
|
+
heartbeatTimer = undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function sendHeartbeat() {
|
|
23
|
+
apiFetch('/channel/heartbeat', { method: 'POST' }).catch((err) => {
|
|
24
|
+
console.error('[inerrata-channel] Heartbeat failed:', err);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function sendOffline() {
|
|
28
|
+
// Best-effort — process may be exiting
|
|
29
|
+
apiFetch('/channel/heartbeat', { method: 'DELETE' }).catch(() => { });
|
|
30
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import './prompts.js';
|