ahok-skill 1.3.1
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/.prettierrc +8 -0
- package/Dockerfile +59 -0
- package/RAW_SKILL.md +219 -0
- package/README.md +277 -0
- package/SKILL.md +58 -0
- package/bin/opm.js +268 -0
- package/data/openmemory.sqlite +0 -0
- package/data/openmemory.sqlite-shm +0 -0
- package/data/openmemory.sqlite-wal +0 -0
- package/dist/ai/graph.js +293 -0
- package/dist/ai/mcp.js +397 -0
- package/dist/cli.js +78 -0
- package/dist/core/cfg.js +87 -0
- package/dist/core/db.js +636 -0
- package/dist/core/memory.js +116 -0
- package/dist/core/migrate.js +227 -0
- package/dist/core/models.js +105 -0
- package/dist/core/telemetry.js +57 -0
- package/dist/core/types.js +2 -0
- package/dist/core/vector/postgres.js +52 -0
- package/dist/core/vector/valkey.js +246 -0
- package/dist/core/vector_store.js +2 -0
- package/dist/index.js +44 -0
- package/dist/memory/decay.js +301 -0
- package/dist/memory/embed.js +675 -0
- package/dist/memory/hsg.js +959 -0
- package/dist/memory/reflect.js +131 -0
- package/dist/memory/user_summary.js +99 -0
- package/dist/migrate.js +9 -0
- package/dist/ops/compress.js +255 -0
- package/dist/ops/dynamics.js +189 -0
- package/dist/ops/extract.js +333 -0
- package/dist/ops/ingest.js +214 -0
- package/dist/server/index.js +109 -0
- package/dist/server/middleware/auth.js +137 -0
- package/dist/server/routes/auth.js +186 -0
- package/dist/server/routes/compression.js +108 -0
- package/dist/server/routes/dashboard.js +399 -0
- package/dist/server/routes/docs.js +241 -0
- package/dist/server/routes/dynamics.js +312 -0
- package/dist/server/routes/ide.js +280 -0
- package/dist/server/routes/index.js +33 -0
- package/dist/server/routes/keys.js +132 -0
- package/dist/server/routes/langgraph.js +61 -0
- package/dist/server/routes/memory.js +213 -0
- package/dist/server/routes/sources.js +140 -0
- package/dist/server/routes/system.js +63 -0
- package/dist/server/routes/temporal.js +293 -0
- package/dist/server/routes/users.js +101 -0
- package/dist/server/routes/vercel.js +57 -0
- package/dist/server/server.js +211 -0
- package/dist/server.js +3 -0
- package/dist/sources/base.js +223 -0
- package/dist/sources/github.js +171 -0
- package/dist/sources/google_drive.js +166 -0
- package/dist/sources/google_sheets.js +112 -0
- package/dist/sources/google_slides.js +139 -0
- package/dist/sources/index.js +34 -0
- package/dist/sources/notion.js +165 -0
- package/dist/sources/onedrive.js +143 -0
- package/dist/sources/web_crawler.js +166 -0
- package/dist/temporal_graph/index.js +20 -0
- package/dist/temporal_graph/query.js +240 -0
- package/dist/temporal_graph/store.js +116 -0
- package/dist/temporal_graph/timeline.js +241 -0
- package/dist/temporal_graph/types.js +2 -0
- package/dist/utils/chunking.js +60 -0
- package/dist/utils/index.js +31 -0
- package/dist/utils/keyword.js +94 -0
- package/dist/utils/text.js +120 -0
- package/nodemon.json +7 -0
- package/package.json +50 -0
- package/references/api_reference.md +66 -0
- package/references/examples.md +45 -0
- package/src/ai/graph.ts +363 -0
- package/src/ai/mcp.ts +494 -0
- package/src/cli.ts +94 -0
- package/src/core/cfg.ts +110 -0
- package/src/core/db.ts +1052 -0
- package/src/core/memory.ts +99 -0
- package/src/core/migrate.ts +302 -0
- package/src/core/models.ts +107 -0
- package/src/core/telemetry.ts +47 -0
- package/src/core/types.ts +130 -0
- package/src/core/vector/postgres.ts +61 -0
- package/src/core/vector/valkey.ts +261 -0
- package/src/core/vector_store.ts +9 -0
- package/src/index.ts +5 -0
- package/src/memory/decay.ts +427 -0
- package/src/memory/embed.ts +707 -0
- package/src/memory/hsg.ts +1245 -0
- package/src/memory/reflect.ts +158 -0
- package/src/memory/user_summary.ts +110 -0
- package/src/migrate.ts +8 -0
- package/src/ops/compress.ts +296 -0
- package/src/ops/dynamics.ts +272 -0
- package/src/ops/extract.ts +360 -0
- package/src/ops/ingest.ts +286 -0
- package/src/server/index.ts +159 -0
- package/src/server/middleware/auth.ts +156 -0
- package/src/server/routes/auth.ts +223 -0
- package/src/server/routes/compression.ts +106 -0
- package/src/server/routes/dashboard.ts +420 -0
- package/src/server/routes/docs.ts +380 -0
- package/src/server/routes/dynamics.ts +516 -0
- package/src/server/routes/ide.ts +283 -0
- package/src/server/routes/index.ts +32 -0
- package/src/server/routes/keys.ts +131 -0
- package/src/server/routes/langgraph.ts +71 -0
- package/src/server/routes/memory.ts +440 -0
- package/src/server/routes/sources.ts +111 -0
- package/src/server/routes/system.ts +68 -0
- package/src/server/routes/temporal.ts +335 -0
- package/src/server/routes/users.ts +111 -0
- package/src/server/routes/vercel.ts +55 -0
- package/src/server/server.js +215 -0
- package/src/server.ts +1 -0
- package/src/sources/base.ts +257 -0
- package/src/sources/github.ts +156 -0
- package/src/sources/google_drive.ts +144 -0
- package/src/sources/google_sheets.ts +85 -0
- package/src/sources/google_slides.ts +115 -0
- package/src/sources/index.ts +19 -0
- package/src/sources/notion.ts +148 -0
- package/src/sources/onedrive.ts +131 -0
- package/src/sources/web_crawler.ts +161 -0
- package/src/temporal_graph/index.ts +4 -0
- package/src/temporal_graph/query.ts +299 -0
- package/src/temporal_graph/store.ts +156 -0
- package/src/temporal_graph/timeline.ts +319 -0
- package/src/temporal_graph/types.ts +41 -0
- package/src/utils/chunking.ts +66 -0
- package/src/utils/index.ts +25 -0
- package/src/utils/keyword.ts +137 -0
- package/src/utils/text.ts +115 -0
- package/tests/test_api_workspace_management.ts +413 -0
- package/tests/test_bulk_delete.ts +267 -0
- package/tests/test_omnibus.ts +166 -0
- package/tests/test_workspace_management.ts +278 -0
- package/tests/verify.ts +104 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
*--------------------------------------------------------------------------------
|
|
4
|
+
* ______ _ _ _ _ _
|
|
5
|
+
* | ____| | | | | | | || |
|
|
6
|
+
* | |__ _ _| |__ ___| | |_ __ _| || |_
|
|
7
|
+
* | __| | | | '_ \ / _ \ | __| \ \ / /__ _|
|
|
8
|
+
* | | | |_| | |_) | __/ | |_ \ V / | |
|
|
9
|
+
* |_| \__,_|_.__/ \___|_|\__| \_/ |_|
|
|
10
|
+
*--------------------------------------------------------------------------------
|
|
11
|
+
*
|
|
12
|
+
* @website - https:
|
|
13
|
+
* @github - https:
|
|
14
|
+
* @discord - https:
|
|
15
|
+
*
|
|
16
|
+
* @author - Cavira
|
|
17
|
+
* @copyright - 2025 Cavira OSS
|
|
18
|
+
* @version - 4.0.0
|
|
19
|
+
*
|
|
20
|
+
*--------------------------------------------------------------------------------
|
|
21
|
+
* server.js - Application webserver.
|
|
22
|
+
*--------------------------------------------------------------------------------
|
|
23
|
+
**/
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const http = require('http');
|
|
27
|
+
const WebSocket = require('ws');
|
|
28
|
+
const { parse } = require('url');
|
|
29
|
+
function server(config = {}) {
|
|
30
|
+
const ROUTES = [];
|
|
31
|
+
const WARES = [];
|
|
32
|
+
const WS_ROUTES = [];
|
|
33
|
+
const wss = new WebSocket.Server({ noServer: true });
|
|
34
|
+
const SERVER = http.createServer((req, res) => {
|
|
35
|
+
let u = parse(req.url, true);
|
|
36
|
+
req.query = u.query || {};
|
|
37
|
+
req.path = u.pathname;
|
|
38
|
+
req.hostname = (req.headers.host || '').split(':')[0].replace(/[^\w.-]/g, '');
|
|
39
|
+
req.ip = (req.socket.remoteAddress || '').replace(/[^\w.:]/g, '');
|
|
40
|
+
res.statusCode = 200;
|
|
41
|
+
res.status = (x) => {
|
|
42
|
+
res.statusCode = x;
|
|
43
|
+
return res;
|
|
44
|
+
};
|
|
45
|
+
res.json = (x) => {
|
|
46
|
+
res.writeHead(res.statusCode || 200, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(JSON.stringify(x));
|
|
48
|
+
};
|
|
49
|
+
res.send = (x) => {
|
|
50
|
+
if (x === undefined || x === null)
|
|
51
|
+
x = '';
|
|
52
|
+
if (typeof x === 'object')
|
|
53
|
+
return res.json(x);
|
|
54
|
+
res.writeHead(res.statusCode || 200, { 'Content-Type': 'text/plain' });
|
|
55
|
+
res.end(String(x));
|
|
56
|
+
};
|
|
57
|
+
res.set = (k, v) => { res.setHeader(k, v); return res; };
|
|
58
|
+
let r = matchRoute(req.method.toUpperCase(), req.path);
|
|
59
|
+
req.params = r ? r.params : {};
|
|
60
|
+
let fns = [...WARES];
|
|
61
|
+
fns.push(r ? (req, res, next) => r.handler(req, res, next) : (_req, res) => res.status(404).end('404: Not Found'));
|
|
62
|
+
let i = 0;
|
|
63
|
+
let next = () => {
|
|
64
|
+
if (i < fns.length)
|
|
65
|
+
fns[i++](req, res, next);
|
|
66
|
+
};
|
|
67
|
+
next();
|
|
68
|
+
});
|
|
69
|
+
SERVER.on('upgrade', (req, socket, head) => {
|
|
70
|
+
let u = parse(req.url || '', true);
|
|
71
|
+
let path = u.pathname;
|
|
72
|
+
if (!path || path.includes('..') || /[\0-\x1F\x7F]/.test(path)) {
|
|
73
|
+
socket.destroy();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (let i = 0; i < WS_ROUTES.length; i++) {
|
|
77
|
+
let r = WS_ROUTES[i];
|
|
78
|
+
if (r.path === path) {
|
|
79
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
80
|
+
ws.req = req;
|
|
81
|
+
r.handler(ws, req);
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
socket.destroy();
|
|
87
|
+
});
|
|
88
|
+
const matchRoute = (a, b) => {
|
|
89
|
+
for (let i = 0; i < ROUTES.length; i++) {
|
|
90
|
+
let r = ROUTES[i];
|
|
91
|
+
if (r.method !== a && r.method !== 'ALL')
|
|
92
|
+
continue;
|
|
93
|
+
let p = r.path.split('/').filter(Boolean);
|
|
94
|
+
let u = b.split('/').filter(Boolean);
|
|
95
|
+
if (p.length !== u.length)
|
|
96
|
+
continue;
|
|
97
|
+
let params = {};
|
|
98
|
+
let matched = true;
|
|
99
|
+
for (let j = 0; j < p.length; j++) {
|
|
100
|
+
if (p[j].startsWith(':')) {
|
|
101
|
+
params[p[j].slice(1)] = decodeURIComponent(u[j]);
|
|
102
|
+
}
|
|
103
|
+
else if (p[j] !== u[j]) {
|
|
104
|
+
matched = false;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (matched)
|
|
109
|
+
return { handler: r.handler, params };
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
const add = (a, b, c) => { ROUTES.push({ method: a.toUpperCase(), path: b, handler: c }); };
|
|
114
|
+
const use = (a) => { WARES.push(a); };
|
|
115
|
+
const listen = (a, b) => { SERVER.setTimeout(10000); SERVER.listen(a, b); };
|
|
116
|
+
const all = (a, b) => { add('ALL', a, b); };
|
|
117
|
+
const getRoutes = () => ROUTES.reduce((acc, { method, path }) => ((acc[method] = acc[method] || []).push(path), acc), {});
|
|
118
|
+
const serverStatic = (endpoint, dir) => {
|
|
119
|
+
const a = path.resolve(dir);
|
|
120
|
+
if (!fs.existsSync(a) || !fs.statSync(a).isDirectory()) {
|
|
121
|
+
console.error(`[STATIC] Directory not found or is not a directory: ${a}`);
|
|
122
|
+
return (req, res, next) => next();
|
|
123
|
+
}
|
|
124
|
+
let b = (endpoint.endsWith('/') ? endpoint : endpoint + '/');
|
|
125
|
+
return function staticMiddleware(req, res, next) {
|
|
126
|
+
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
127
|
+
return next();
|
|
128
|
+
if (!req.path.startsWith(b))
|
|
129
|
+
return next();
|
|
130
|
+
let c = path.join(a, req.path.substring(b.length));
|
|
131
|
+
let d = path.relative(a, c);
|
|
132
|
+
if (!(d && !d.startsWith('..') && !path.isAbsolute(d)))
|
|
133
|
+
return next();
|
|
134
|
+
fs.stat(c, (err, stats) => {
|
|
135
|
+
if (err || !stats.isFile())
|
|
136
|
+
return next();
|
|
137
|
+
res.setHeader('Content-Type', getContentType(c));
|
|
138
|
+
fs.createReadStream(c).pipe(res);
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
function getContentType(a) {
|
|
142
|
+
switch (path.extname(a).toLowerCase()) {
|
|
143
|
+
case '.html': return 'text/html';
|
|
144
|
+
case '.js': return 'text/javascript';
|
|
145
|
+
case '.css': return 'text/css';
|
|
146
|
+
case '.json': return 'application/json';
|
|
147
|
+
case '.txt': return 'text/plain';
|
|
148
|
+
case '.ico': return 'image/x-icon';
|
|
149
|
+
case '.png': return 'image/png';
|
|
150
|
+
case '.webp': return 'image/webp';
|
|
151
|
+
case '.jpg': return 'image/jpeg';
|
|
152
|
+
case '.jpeg': return 'image/jpeg';
|
|
153
|
+
case '.gif': return 'image/gif';
|
|
154
|
+
case '.svg': return 'image/svg+xml';
|
|
155
|
+
default: return 'application/octet-stream';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
use((req, res, next) => {
|
|
160
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
161
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
|
162
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key');
|
|
163
|
+
if (req.method === 'OPTIONS') return res.status(204).send();
|
|
164
|
+
next();
|
|
165
|
+
});
|
|
166
|
+
use((req, res, next) => {
|
|
167
|
+
if (req.headers['content-type']?.includes('application/json')) {
|
|
168
|
+
let d = '';
|
|
169
|
+
let max = config.max_payload_size || 1_000_000;
|
|
170
|
+
req.on('data', e => {
|
|
171
|
+
d += e;
|
|
172
|
+
if (d.length > max) {
|
|
173
|
+
res.status(413).end('Payload Too Large');
|
|
174
|
+
req.destroy();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
req.on('end', () => {
|
|
178
|
+
req.rawBody = d;
|
|
179
|
+
try {
|
|
180
|
+
req.body = JSON.parse(d);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
req.body = null;
|
|
184
|
+
}
|
|
185
|
+
next();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
next();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
use,
|
|
194
|
+
listen,
|
|
195
|
+
all,
|
|
196
|
+
serverStatic,
|
|
197
|
+
routes: ROUTES,
|
|
198
|
+
getRoutes,
|
|
199
|
+
get: (a, b) => add('GET', a, b),
|
|
200
|
+
post: (a, b) => add('POST', a, b),
|
|
201
|
+
put: (a, b) => add('PUT', a, b),
|
|
202
|
+
delete: (a, b) => add('DELETE', a, b),
|
|
203
|
+
patch: (a, b) => add('PATCH', a, b),
|
|
204
|
+
options: (a, b) => add('OPTIONS', a, b),
|
|
205
|
+
head: (a, b) => add('HEAD', a, b),
|
|
206
|
+
all: (a, b) => add('ALL', a, b),
|
|
207
|
+
ws: (a, b) => WS_ROUTES.push({ path: a, handler: b })
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
module.exports = server;
|
|
211
|
+
/**
|
|
212
|
+
*--------------------------------------------------------------------------------
|
|
213
|
+
* @EOF - End Of File
|
|
214
|
+
*--------------------------------------------------------------------------------
|
|
215
|
+
**/
|
package/src/server.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./server/index.js";
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* base source class for openmemory data sources - production grade
|
|
3
|
+
*
|
|
4
|
+
* features:
|
|
5
|
+
* - custom exception hierarchy
|
|
6
|
+
* - logging
|
|
7
|
+
* - retry logic with exponential backoff
|
|
8
|
+
* - rate limiting
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// -- exceptions --
|
|
12
|
+
|
|
13
|
+
export class source_error extends Error {
|
|
14
|
+
source?: string;
|
|
15
|
+
cause?: Error;
|
|
16
|
+
|
|
17
|
+
constructor(msg: string, source?: string, cause?: Error) {
|
|
18
|
+
super(source ? `[${source}] ${msg}` : msg);
|
|
19
|
+
this.name = 'source_error';
|
|
20
|
+
this.source = source;
|
|
21
|
+
this.cause = cause;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class source_auth_error extends source_error {
|
|
26
|
+
constructor(msg: string, source?: string, cause?: Error) {
|
|
27
|
+
super(msg, source, cause);
|
|
28
|
+
this.name = 'source_auth_error';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class source_config_error extends source_error {
|
|
33
|
+
constructor(msg: string, source?: string, cause?: Error) {
|
|
34
|
+
super(msg, source, cause);
|
|
35
|
+
this.name = 'source_config_error';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class source_rate_limit_error extends source_error {
|
|
40
|
+
retry_after?: number;
|
|
41
|
+
|
|
42
|
+
constructor(msg: string, retry_after?: number, source?: string) {
|
|
43
|
+
super(msg, source);
|
|
44
|
+
this.name = 'source_rate_limit_error';
|
|
45
|
+
this.retry_after = retry_after;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class source_fetch_error extends source_error {
|
|
50
|
+
constructor(msg: string, source?: string, cause?: Error) {
|
|
51
|
+
super(msg, source, cause);
|
|
52
|
+
this.name = 'source_fetch_error';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// -- types --
|
|
57
|
+
|
|
58
|
+
export interface source_item {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
type: string;
|
|
62
|
+
[key: string]: any;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface source_content {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
type: string;
|
|
69
|
+
text: string;
|
|
70
|
+
data: string | Buffer;
|
|
71
|
+
meta: Record<string, any>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface source_config {
|
|
75
|
+
max_retries?: number;
|
|
76
|
+
requests_per_second?: number;
|
|
77
|
+
log_level?: 'debug' | 'info' | 'warn' | 'error';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// -- rate limiter --
|
|
81
|
+
|
|
82
|
+
export class rate_limiter {
|
|
83
|
+
private rps: number;
|
|
84
|
+
private tokens: number;
|
|
85
|
+
private last_update: number;
|
|
86
|
+
|
|
87
|
+
constructor(requests_per_second: number = 10) {
|
|
88
|
+
this.rps = requests_per_second;
|
|
89
|
+
this.tokens = requests_per_second;
|
|
90
|
+
this.last_update = Date.now();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async acquire(): Promise<void> {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const elapsed = (now - this.last_update) / 1000;
|
|
96
|
+
this.tokens = Math.min(this.rps, this.tokens + elapsed * this.rps);
|
|
97
|
+
this.last_update = now;
|
|
98
|
+
|
|
99
|
+
if (this.tokens < 1) {
|
|
100
|
+
const wait_time = ((1 - this.tokens) / this.rps) * 1000;
|
|
101
|
+
await new Promise(r => setTimeout(r, wait_time));
|
|
102
|
+
this.tokens = 0;
|
|
103
|
+
} else {
|
|
104
|
+
this.tokens -= 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -- retry helper --
|
|
110
|
+
|
|
111
|
+
export async function with_retry<T>(
|
|
112
|
+
fn: () => Promise<T>,
|
|
113
|
+
max_attempts: number = 3,
|
|
114
|
+
base_delay: number = 1000,
|
|
115
|
+
max_delay: number = 60000
|
|
116
|
+
): Promise<T> {
|
|
117
|
+
let last_err: Error | null = null;
|
|
118
|
+
|
|
119
|
+
for (let attempt = 0; attempt < max_attempts; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
return await fn();
|
|
122
|
+
} catch (e: any) {
|
|
123
|
+
last_err = e;
|
|
124
|
+
|
|
125
|
+
if (e instanceof source_auth_error) {
|
|
126
|
+
throw e; // don't retry auth errors
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (attempt < max_attempts - 1) {
|
|
130
|
+
const delay = e instanceof source_rate_limit_error && e.retry_after
|
|
131
|
+
? e.retry_after * 1000
|
|
132
|
+
: Math.min(base_delay * Math.pow(2, attempt), max_delay);
|
|
133
|
+
|
|
134
|
+
console.warn(`[retry] attempt ${attempt + 1}/${max_attempts} failed: ${e.message}, retrying in ${delay}ms`);
|
|
135
|
+
await new Promise(r => setTimeout(r, delay));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw last_err;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// -- base source --
|
|
144
|
+
|
|
145
|
+
export abstract class base_source {
|
|
146
|
+
name: string = 'base';
|
|
147
|
+
user_id: string;
|
|
148
|
+
protected _connected: boolean = false;
|
|
149
|
+
protected _max_retries: number;
|
|
150
|
+
protected _rate_limiter: rate_limiter;
|
|
151
|
+
|
|
152
|
+
constructor(user_id?: string, config?: source_config) {
|
|
153
|
+
this.user_id = user_id || 'anonymous';
|
|
154
|
+
this._max_retries = config?.max_retries || 3;
|
|
155
|
+
this._rate_limiter = new rate_limiter(config?.requests_per_second || 10);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get connected(): boolean {
|
|
159
|
+
return this._connected;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async connect(creds?: Record<string, any>): Promise<boolean> {
|
|
163
|
+
console.log(`[${this.name}] connecting...`);
|
|
164
|
+
try {
|
|
165
|
+
const result = await this._connect(creds || {});
|
|
166
|
+
this._connected = result;
|
|
167
|
+
if (result) {
|
|
168
|
+
console.log(`[${this.name}] connected`);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
} catch (e: any) {
|
|
172
|
+
console.error(`[${this.name}] connection failed: ${e.message}`);
|
|
173
|
+
throw new source_auth_error(e.message, this.name, e);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async disconnect(): Promise<void> {
|
|
178
|
+
this._connected = false;
|
|
179
|
+
console.log(`[${this.name}] disconnected`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async list_items(filters?: Record<string, any>): Promise<source_item[]> {
|
|
183
|
+
if (!this._connected) {
|
|
184
|
+
await this.connect();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await this._rate_limiter.acquire();
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const items = await with_retry(
|
|
191
|
+
() => this._list_items(filters || {}),
|
|
192
|
+
this._max_retries
|
|
193
|
+
);
|
|
194
|
+
console.log(`[${this.name}] found ${items.length} items`);
|
|
195
|
+
return items;
|
|
196
|
+
} catch (e: any) {
|
|
197
|
+
throw new source_fetch_error(e.message, this.name, e);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async fetch_item(item_id: string): Promise<source_content> {
|
|
202
|
+
if (!this._connected) {
|
|
203
|
+
await this.connect();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await this._rate_limiter.acquire();
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
return await with_retry(
|
|
210
|
+
() => this._fetch_item(item_id),
|
|
211
|
+
this._max_retries
|
|
212
|
+
);
|
|
213
|
+
} catch (e: any) {
|
|
214
|
+
throw new source_fetch_error(e.message, this.name, e);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async ingest_all(filters?: Record<string, any>): Promise<string[]> {
|
|
219
|
+
const { ingestDocument } = await import('../ops/ingest');
|
|
220
|
+
|
|
221
|
+
const items = await this.list_items(filters);
|
|
222
|
+
const ids: string[] = [];
|
|
223
|
+
const errors: { id: string; error: string }[] = [];
|
|
224
|
+
|
|
225
|
+
console.log(`[${this.name}] ingesting ${items.length} items...`);
|
|
226
|
+
|
|
227
|
+
for (let i = 0; i < items.length; i++) {
|
|
228
|
+
const item = items[i];
|
|
229
|
+
try {
|
|
230
|
+
const content = await this.fetch_item(item.id);
|
|
231
|
+
const result = await ingestDocument(
|
|
232
|
+
content.type || 'text',
|
|
233
|
+
content.data || content.text || '',
|
|
234
|
+
{ source: this.name, ...content.meta },
|
|
235
|
+
undefined,
|
|
236
|
+
this.user_id
|
|
237
|
+
);
|
|
238
|
+
ids.push(result.root_memory_id);
|
|
239
|
+
} catch (e: any) {
|
|
240
|
+
console.warn(`[${this.name}] failed to ingest ${item.id}: ${e.message}`);
|
|
241
|
+
errors.push({ id: item.id, error: e.message });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(`[${this.name}] ingested ${ids.length} items, ${errors.length} errors`);
|
|
246
|
+
return ids;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
protected _get_env(key: string, default_val?: string): string | undefined {
|
|
250
|
+
return process.env[key] || default_val;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// abstract methods for subclasses
|
|
254
|
+
protected abstract _connect(creds: Record<string, any>): Promise<boolean>;
|
|
255
|
+
protected abstract _list_items(filters: Record<string, any>): Promise<source_item[]>;
|
|
256
|
+
protected abstract _fetch_item(item_id: string): Promise<source_content>;
|
|
257
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* github source for openmemory - production grade
|
|
3
|
+
* requires: @octokit/rest
|
|
4
|
+
* env vars: GITHUB_TOKEN
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { base_source, source_config_error, source_item, source_content } from './base';
|
|
8
|
+
|
|
9
|
+
export class github_source extends base_source {
|
|
10
|
+
name = 'github';
|
|
11
|
+
private octokit: any = null;
|
|
12
|
+
|
|
13
|
+
async _connect(creds: Record<string, any>): Promise<boolean> {
|
|
14
|
+
let Octokit: any;
|
|
15
|
+
try {
|
|
16
|
+
Octokit = await import('@octokit/rest').then(m => m.Octokit);
|
|
17
|
+
} catch {
|
|
18
|
+
throw new source_config_error('missing deps: npm install @octokit/rest', this.name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const token = creds.token || process.env.GITHUB_TOKEN;
|
|
22
|
+
|
|
23
|
+
if (!token) {
|
|
24
|
+
throw new source_config_error('no credentials: set GITHUB_TOKEN', this.name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.octokit = new Octokit({ auth: token });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async _list_items(filters: Record<string, any>): Promise<source_item[]> {
|
|
32
|
+
if (!filters.repo) {
|
|
33
|
+
throw new source_config_error('repo is required (format: owner/repo)', this.name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [owner, repo] = filters.repo.split('/');
|
|
37
|
+
const path = filters.path?.replace(/^\//, '') || '';
|
|
38
|
+
const include_issues = filters.include_issues || false;
|
|
39
|
+
|
|
40
|
+
const results: source_item[] = [];
|
|
41
|
+
|
|
42
|
+
// list files
|
|
43
|
+
try {
|
|
44
|
+
const resp = await this.octokit.repos.getContent({ owner, repo, path });
|
|
45
|
+
const contents = Array.isArray(resp.data) ? resp.data : [resp.data];
|
|
46
|
+
|
|
47
|
+
for (const content of contents) {
|
|
48
|
+
results.push({
|
|
49
|
+
id: `${filters.repo}:${content.path}`,
|
|
50
|
+
name: content.name,
|
|
51
|
+
type: content.type === 'dir' ? 'dir' : content.encoding || 'file',
|
|
52
|
+
path: content.path,
|
|
53
|
+
size: content.size || 0,
|
|
54
|
+
sha: content.sha
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch (e: any) {
|
|
58
|
+
console.warn(`[github] failed to list ${path}: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// list issues if requested
|
|
62
|
+
if (include_issues) {
|
|
63
|
+
try {
|
|
64
|
+
const resp = await this.octokit.issues.listForRepo({ owner, repo, state: 'all', per_page: 50 });
|
|
65
|
+
|
|
66
|
+
for (const issue of resp.data) {
|
|
67
|
+
results.push({
|
|
68
|
+
id: `${filters.repo}:issue:${issue.number}`,
|
|
69
|
+
name: issue.title,
|
|
70
|
+
type: 'issue',
|
|
71
|
+
number: issue.number,
|
|
72
|
+
state: issue.state,
|
|
73
|
+
labels: issue.labels.map((l: any) => l.name)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch (e: any) {
|
|
77
|
+
console.warn(`[github] failed to list issues: ${e.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async _fetch_item(item_id: string): Promise<source_content> {
|
|
85
|
+
const parts = item_id.split(':');
|
|
86
|
+
const repo_full = parts[0];
|
|
87
|
+
const [owner, repo] = repo_full.split('/');
|
|
88
|
+
|
|
89
|
+
// issue
|
|
90
|
+
if (parts.length >= 3 && parts[1] === 'issue') {
|
|
91
|
+
const issue_num = parseInt(parts[2]);
|
|
92
|
+
const issue = await this.octokit.issues.get({ owner, repo, issue_number: issue_num });
|
|
93
|
+
|
|
94
|
+
const comments = await this.octokit.issues.listComments({ owner, repo, issue_number: issue_num });
|
|
95
|
+
|
|
96
|
+
const text_parts = [
|
|
97
|
+
`# ${issue.data.title}`,
|
|
98
|
+
`**State:** ${issue.data.state}`,
|
|
99
|
+
`**Labels:** ${issue.data.labels.map((l: any) => l.name).join(', ')}`,
|
|
100
|
+
'',
|
|
101
|
+
issue.data.body || ''
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const comment of comments.data) {
|
|
105
|
+
text_parts.push(`\n---\n**${comment.user?.login}:** ${comment.body}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const text = text_parts.join('\n');
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
id: item_id,
|
|
112
|
+
name: issue.data.title,
|
|
113
|
+
type: 'issue',
|
|
114
|
+
text,
|
|
115
|
+
data: text,
|
|
116
|
+
meta: { source: 'github', repo: repo_full, issue_number: issue_num, state: issue.data.state }
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// file
|
|
121
|
+
const path = parts.slice(1).join(':');
|
|
122
|
+
const resp = await this.octokit.repos.getContent({ owner, repo, path });
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(resp.data)) {
|
|
125
|
+
const text = resp.data.map((c: any) => `- ${c.path}`).join('\n');
|
|
126
|
+
return {
|
|
127
|
+
id: item_id,
|
|
128
|
+
name: path || repo_full,
|
|
129
|
+
type: 'directory',
|
|
130
|
+
text,
|
|
131
|
+
data: text,
|
|
132
|
+
meta: { source: 'github', repo: repo_full, path }
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const content = resp.data;
|
|
137
|
+
let text = '';
|
|
138
|
+
let data: string | Buffer = '';
|
|
139
|
+
|
|
140
|
+
if (content.content) {
|
|
141
|
+
data = Buffer.from(content.content, 'base64');
|
|
142
|
+
try {
|
|
143
|
+
text = data.toString('utf-8');
|
|
144
|
+
} catch { }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
id: item_id,
|
|
149
|
+
name: content.name,
|
|
150
|
+
type: content.encoding || 'file',
|
|
151
|
+
text,
|
|
152
|
+
data,
|
|
153
|
+
meta: { source: 'github', repo: repo_full, path: content.path, sha: content.sha, size: content.size }
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|