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.
Files changed (141) hide show
  1. package/.prettierrc +8 -0
  2. package/Dockerfile +59 -0
  3. package/RAW_SKILL.md +219 -0
  4. package/README.md +277 -0
  5. package/SKILL.md +58 -0
  6. package/bin/opm.js +268 -0
  7. package/data/openmemory.sqlite +0 -0
  8. package/data/openmemory.sqlite-shm +0 -0
  9. package/data/openmemory.sqlite-wal +0 -0
  10. package/dist/ai/graph.js +293 -0
  11. package/dist/ai/mcp.js +397 -0
  12. package/dist/cli.js +78 -0
  13. package/dist/core/cfg.js +87 -0
  14. package/dist/core/db.js +636 -0
  15. package/dist/core/memory.js +116 -0
  16. package/dist/core/migrate.js +227 -0
  17. package/dist/core/models.js +105 -0
  18. package/dist/core/telemetry.js +57 -0
  19. package/dist/core/types.js +2 -0
  20. package/dist/core/vector/postgres.js +52 -0
  21. package/dist/core/vector/valkey.js +246 -0
  22. package/dist/core/vector_store.js +2 -0
  23. package/dist/index.js +44 -0
  24. package/dist/memory/decay.js +301 -0
  25. package/dist/memory/embed.js +675 -0
  26. package/dist/memory/hsg.js +959 -0
  27. package/dist/memory/reflect.js +131 -0
  28. package/dist/memory/user_summary.js +99 -0
  29. package/dist/migrate.js +9 -0
  30. package/dist/ops/compress.js +255 -0
  31. package/dist/ops/dynamics.js +189 -0
  32. package/dist/ops/extract.js +333 -0
  33. package/dist/ops/ingest.js +214 -0
  34. package/dist/server/index.js +109 -0
  35. package/dist/server/middleware/auth.js +137 -0
  36. package/dist/server/routes/auth.js +186 -0
  37. package/dist/server/routes/compression.js +108 -0
  38. package/dist/server/routes/dashboard.js +399 -0
  39. package/dist/server/routes/docs.js +241 -0
  40. package/dist/server/routes/dynamics.js +312 -0
  41. package/dist/server/routes/ide.js +280 -0
  42. package/dist/server/routes/index.js +33 -0
  43. package/dist/server/routes/keys.js +132 -0
  44. package/dist/server/routes/langgraph.js +61 -0
  45. package/dist/server/routes/memory.js +213 -0
  46. package/dist/server/routes/sources.js +140 -0
  47. package/dist/server/routes/system.js +63 -0
  48. package/dist/server/routes/temporal.js +293 -0
  49. package/dist/server/routes/users.js +101 -0
  50. package/dist/server/routes/vercel.js +57 -0
  51. package/dist/server/server.js +211 -0
  52. package/dist/server.js +3 -0
  53. package/dist/sources/base.js +223 -0
  54. package/dist/sources/github.js +171 -0
  55. package/dist/sources/google_drive.js +166 -0
  56. package/dist/sources/google_sheets.js +112 -0
  57. package/dist/sources/google_slides.js +139 -0
  58. package/dist/sources/index.js +34 -0
  59. package/dist/sources/notion.js +165 -0
  60. package/dist/sources/onedrive.js +143 -0
  61. package/dist/sources/web_crawler.js +166 -0
  62. package/dist/temporal_graph/index.js +20 -0
  63. package/dist/temporal_graph/query.js +240 -0
  64. package/dist/temporal_graph/store.js +116 -0
  65. package/dist/temporal_graph/timeline.js +241 -0
  66. package/dist/temporal_graph/types.js +2 -0
  67. package/dist/utils/chunking.js +60 -0
  68. package/dist/utils/index.js +31 -0
  69. package/dist/utils/keyword.js +94 -0
  70. package/dist/utils/text.js +120 -0
  71. package/nodemon.json +7 -0
  72. package/package.json +50 -0
  73. package/references/api_reference.md +66 -0
  74. package/references/examples.md +45 -0
  75. package/src/ai/graph.ts +363 -0
  76. package/src/ai/mcp.ts +494 -0
  77. package/src/cli.ts +94 -0
  78. package/src/core/cfg.ts +110 -0
  79. package/src/core/db.ts +1052 -0
  80. package/src/core/memory.ts +99 -0
  81. package/src/core/migrate.ts +302 -0
  82. package/src/core/models.ts +107 -0
  83. package/src/core/telemetry.ts +47 -0
  84. package/src/core/types.ts +130 -0
  85. package/src/core/vector/postgres.ts +61 -0
  86. package/src/core/vector/valkey.ts +261 -0
  87. package/src/core/vector_store.ts +9 -0
  88. package/src/index.ts +5 -0
  89. package/src/memory/decay.ts +427 -0
  90. package/src/memory/embed.ts +707 -0
  91. package/src/memory/hsg.ts +1245 -0
  92. package/src/memory/reflect.ts +158 -0
  93. package/src/memory/user_summary.ts +110 -0
  94. package/src/migrate.ts +8 -0
  95. package/src/ops/compress.ts +296 -0
  96. package/src/ops/dynamics.ts +272 -0
  97. package/src/ops/extract.ts +360 -0
  98. package/src/ops/ingest.ts +286 -0
  99. package/src/server/index.ts +159 -0
  100. package/src/server/middleware/auth.ts +156 -0
  101. package/src/server/routes/auth.ts +223 -0
  102. package/src/server/routes/compression.ts +106 -0
  103. package/src/server/routes/dashboard.ts +420 -0
  104. package/src/server/routes/docs.ts +380 -0
  105. package/src/server/routes/dynamics.ts +516 -0
  106. package/src/server/routes/ide.ts +283 -0
  107. package/src/server/routes/index.ts +32 -0
  108. package/src/server/routes/keys.ts +131 -0
  109. package/src/server/routes/langgraph.ts +71 -0
  110. package/src/server/routes/memory.ts +440 -0
  111. package/src/server/routes/sources.ts +111 -0
  112. package/src/server/routes/system.ts +68 -0
  113. package/src/server/routes/temporal.ts +335 -0
  114. package/src/server/routes/users.ts +111 -0
  115. package/src/server/routes/vercel.ts +55 -0
  116. package/src/server/server.js +215 -0
  117. package/src/server.ts +1 -0
  118. package/src/sources/base.ts +257 -0
  119. package/src/sources/github.ts +156 -0
  120. package/src/sources/google_drive.ts +144 -0
  121. package/src/sources/google_sheets.ts +85 -0
  122. package/src/sources/google_slides.ts +115 -0
  123. package/src/sources/index.ts +19 -0
  124. package/src/sources/notion.ts +148 -0
  125. package/src/sources/onedrive.ts +131 -0
  126. package/src/sources/web_crawler.ts +161 -0
  127. package/src/temporal_graph/index.ts +4 -0
  128. package/src/temporal_graph/query.ts +299 -0
  129. package/src/temporal_graph/store.ts +156 -0
  130. package/src/temporal_graph/timeline.ts +319 -0
  131. package/src/temporal_graph/types.ts +41 -0
  132. package/src/utils/chunking.ts +66 -0
  133. package/src/utils/index.ts +25 -0
  134. package/src/utils/keyword.ts +137 -0
  135. package/src/utils/text.ts +115 -0
  136. package/tests/test_api_workspace_management.ts +413 -0
  137. package/tests/test_bulk_delete.ts +267 -0
  138. package/tests/test_omnibus.ts +166 -0
  139. package/tests/test_workspace_management.ts +278 -0
  140. package/tests/verify.ts +104 -0
  141. 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
+ }