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,144 @@
1
+ /**
2
+ * google drive source for openmemory - production grade
3
+ * requires: googleapis
4
+ * env vars: GOOGLE_SERVICE_ACCOUNT_FILE or GOOGLE_CREDENTIALS_JSON
5
+ */
6
+
7
+ import { base_source, source_config_error, source_item, source_content } from './base';
8
+
9
+ export class google_drive_source extends base_source {
10
+ name = 'google_drive';
11
+ private service: any = null;
12
+ private auth: any = null;
13
+
14
+ async _connect(creds: Record<string, any>): Promise<boolean> {
15
+ let google: any;
16
+ try {
17
+ google = await import('googleapis').then(m => m.google);
18
+ } catch {
19
+ throw new source_config_error('missing deps: npm install googleapis', this.name);
20
+ }
21
+
22
+ const scopes = ['https://www.googleapis.com/auth/drive.readonly'];
23
+
24
+ if (creds.credentials_json) {
25
+ this.auth = new google.auth.GoogleAuth({
26
+ credentials: creds.credentials_json,
27
+ scopes
28
+ });
29
+ } else if (creds.service_account_file) {
30
+ this.auth = new google.auth.GoogleAuth({
31
+ keyFile: creds.service_account_file,
32
+ scopes
33
+ });
34
+ } else if (process.env.GOOGLE_CREDENTIALS_JSON) {
35
+ this.auth = new google.auth.GoogleAuth({
36
+ credentials: JSON.parse(process.env.GOOGLE_CREDENTIALS_JSON),
37
+ scopes
38
+ });
39
+ } else if (process.env.GOOGLE_SERVICE_ACCOUNT_FILE) {
40
+ this.auth = new google.auth.GoogleAuth({
41
+ keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_FILE,
42
+ scopes
43
+ });
44
+ } else {
45
+ throw new source_config_error(
46
+ 'no credentials: set GOOGLE_SERVICE_ACCOUNT_FILE or GOOGLE_CREDENTIALS_JSON',
47
+ this.name
48
+ );
49
+ }
50
+
51
+ this.service = google.drive({ version: 'v3', auth: this.auth });
52
+ return true;
53
+ }
54
+
55
+ async _list_items(filters: Record<string, any>): Promise<source_item[]> {
56
+ const q_parts = ['trashed=false'];
57
+
58
+ if (filters.folder_id) {
59
+ q_parts.push(`'${filters.folder_id}' in parents`);
60
+ }
61
+
62
+ if (filters.mime_types?.length) {
63
+ const mime_q = filters.mime_types.map((m: string) => `mimeType='${m}'`).join(' or ');
64
+ q_parts.push(`(${mime_q})`);
65
+ }
66
+
67
+ const query = q_parts.join(' and ');
68
+ const results: source_item[] = [];
69
+ let page_token: string | undefined;
70
+
71
+ do {
72
+ const resp = await this.service.files.list({
73
+ q: query,
74
+ spaces: 'drive',
75
+ fields: 'nextPageToken, files(id, name, mimeType, modifiedTime, size)',
76
+ pageToken: page_token,
77
+ pageSize: 100
78
+ });
79
+
80
+ for (const f of resp.data.files || []) {
81
+ results.push({
82
+ id: f.id!,
83
+ name: f.name!,
84
+ type: f.mimeType!,
85
+ modified: f.modifiedTime,
86
+ size: f.size
87
+ });
88
+ }
89
+
90
+ page_token = resp.data.nextPageToken;
91
+ } while (page_token);
92
+
93
+ return results;
94
+ }
95
+
96
+ async _fetch_item(item_id: string): Promise<source_content> {
97
+ const meta = await this.service.files.get({
98
+ fileId: item_id,
99
+ fields: 'id,name,mimeType'
100
+ });
101
+
102
+ const mime = meta.data.mimeType;
103
+ let text = '';
104
+ let data: string | Buffer = '';
105
+
106
+ // google docs -> export as text
107
+ if (mime === 'application/vnd.google-apps.document') {
108
+ const resp = await this.service.files.export({ fileId: item_id, mimeType: 'text/plain' });
109
+ text = resp.data;
110
+ data = text;
111
+ }
112
+ // google sheets -> export as csv
113
+ else if (mime === 'application/vnd.google-apps.spreadsheet') {
114
+ const resp = await this.service.files.export({ fileId: item_id, mimeType: 'text/csv' });
115
+ text = resp.data;
116
+ data = text;
117
+ }
118
+ // google slides -> export as plain text
119
+ else if (mime === 'application/vnd.google-apps.presentation') {
120
+ const resp = await this.service.files.export({ fileId: item_id, mimeType: 'text/plain' });
121
+ text = resp.data;
122
+ data = text;
123
+ }
124
+ // other files -> download raw
125
+ else {
126
+ const resp = await this.service.files.get({ fileId: item_id, alt: 'media' }, { responseType: 'arraybuffer' });
127
+ data = Buffer.from(resp.data);
128
+ try {
129
+ text = data.toString('utf-8');
130
+ } catch {
131
+ text = '';
132
+ }
133
+ }
134
+
135
+ return {
136
+ id: item_id,
137
+ name: meta.data.name!,
138
+ type: mime!,
139
+ text,
140
+ data,
141
+ meta: { source: 'google_drive', file_id: item_id, mime_type: mime }
142
+ };
143
+ }
144
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * google sheets source for openmemory - production grade
3
+ * requires: googleapis
4
+ * env vars: GOOGLE_SERVICE_ACCOUNT_FILE or GOOGLE_CREDENTIALS_JSON
5
+ */
6
+
7
+ import { base_source, source_config_error, source_item, source_content } from './base';
8
+
9
+ export class google_sheets_source extends base_source {
10
+ name = 'google_sheets';
11
+ private service: any = null;
12
+ private auth: any = null;
13
+
14
+ async _connect(creds: Record<string, any>): Promise<boolean> {
15
+ let google: any;
16
+ try {
17
+ google = await import('googleapis').then(m => m.google);
18
+ } catch {
19
+ throw new source_config_error('missing deps: npm install googleapis', this.name);
20
+ }
21
+
22
+ const scopes = ['https://www.googleapis.com/auth/spreadsheets.readonly'];
23
+
24
+ if (creds.credentials_json) {
25
+ this.auth = new google.auth.GoogleAuth({ credentials: creds.credentials_json, scopes });
26
+ } else if (creds.service_account_file) {
27
+ this.auth = new google.auth.GoogleAuth({ keyFile: creds.service_account_file, scopes });
28
+ } else if (process.env.GOOGLE_CREDENTIALS_JSON) {
29
+ this.auth = new google.auth.GoogleAuth({ credentials: JSON.parse(process.env.GOOGLE_CREDENTIALS_JSON), scopes });
30
+ } else if (process.env.GOOGLE_SERVICE_ACCOUNT_FILE) {
31
+ this.auth = new google.auth.GoogleAuth({ keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_FILE, scopes });
32
+ } else {
33
+ throw new source_config_error('no credentials: set GOOGLE_SERVICE_ACCOUNT_FILE or GOOGLE_CREDENTIALS_JSON', this.name);
34
+ }
35
+
36
+ this.service = google.sheets({ version: 'v4', auth: this.auth });
37
+ return true;
38
+ }
39
+
40
+ async _list_items(filters: Record<string, any>): Promise<source_item[]> {
41
+ if (!filters.spreadsheet_id) {
42
+ throw new source_config_error('spreadsheet_id is required', this.name);
43
+ }
44
+
45
+ const meta = await this.service.spreadsheets.get({ spreadsheetId: filters.spreadsheet_id });
46
+
47
+ return (meta.data.sheets || []).map((sheet: any, i: number) => ({
48
+ id: `${filters.spreadsheet_id}!${sheet.properties?.title || 'Sheet1'}`,
49
+ name: sheet.properties?.title || 'Sheet1',
50
+ type: 'sheet',
51
+ index: i,
52
+ spreadsheet_id: filters.spreadsheet_id
53
+ }));
54
+ }
55
+
56
+ async _fetch_item(item_id: string): Promise<source_content> {
57
+ const [spreadsheet_id, sheet_range] = item_id.includes('!')
58
+ ? item_id.split('!', 2)
59
+ : [item_id, 'A:ZZ'];
60
+
61
+ const result = await this.service.spreadsheets.values.get({
62
+ spreadsheetId: spreadsheet_id,
63
+ range: sheet_range
64
+ });
65
+
66
+ const values = result.data.values || [];
67
+
68
+ // convert to markdown table
69
+ const lines = values.map((row: any[], i: number) => {
70
+ const line = row.map(String).join(' | ');
71
+ return i === 0 ? `${line}\n${row.map(() => '---').join(' | ')}` : line;
72
+ });
73
+
74
+ const text = lines.join('\n');
75
+
76
+ return {
77
+ id: item_id,
78
+ name: sheet_range,
79
+ type: 'spreadsheet',
80
+ text,
81
+ data: text,
82
+ meta: { source: 'google_sheets', spreadsheet_id, range: sheet_range, row_count: values.length }
83
+ };
84
+ }
85
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * google slides source for openmemory - production grade
3
+ * requires: googleapis
4
+ * env vars: GOOGLE_SERVICE_ACCOUNT_FILE or GOOGLE_CREDENTIALS_JSON
5
+ */
6
+
7
+ import { base_source, source_config_error, source_item, source_content } from './base';
8
+
9
+ export class google_slides_source extends base_source {
10
+ name = 'google_slides';
11
+ private service: any = null;
12
+ private auth: any = null;
13
+
14
+ async _connect(creds: Record<string, any>): Promise<boolean> {
15
+ let google: any;
16
+ try {
17
+ google = await import('googleapis').then(m => m.google);
18
+ } catch {
19
+ throw new source_config_error('missing deps: npm install googleapis', this.name);
20
+ }
21
+
22
+ const scopes = ['https://www.googleapis.com/auth/presentations.readonly'];
23
+
24
+ if (creds.credentials_json) {
25
+ this.auth = new google.auth.GoogleAuth({ credentials: creds.credentials_json, scopes });
26
+ } else if (creds.service_account_file) {
27
+ this.auth = new google.auth.GoogleAuth({ keyFile: creds.service_account_file, scopes });
28
+ } else if (process.env.GOOGLE_CREDENTIALS_JSON) {
29
+ this.auth = new google.auth.GoogleAuth({ credentials: JSON.parse(process.env.GOOGLE_CREDENTIALS_JSON), scopes });
30
+ } else if (process.env.GOOGLE_SERVICE_ACCOUNT_FILE) {
31
+ this.auth = new google.auth.GoogleAuth({ keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_FILE, scopes });
32
+ } else {
33
+ throw new source_config_error('no credentials: set GOOGLE_SERVICE_ACCOUNT_FILE or GOOGLE_CREDENTIALS_JSON', this.name);
34
+ }
35
+
36
+ this.service = google.slides({ version: 'v1', auth: this.auth });
37
+ return true;
38
+ }
39
+
40
+ async _list_items(filters: Record<string, any>): Promise<source_item[]> {
41
+ if (!filters.presentation_id) {
42
+ throw new source_config_error('presentation_id is required', this.name);
43
+ }
44
+
45
+ const pres = await this.service.presentations.get({ presentationId: filters.presentation_id });
46
+
47
+ return (pres.data.slides || []).map((slide: any, i: number) => ({
48
+ id: `${filters.presentation_id}#${slide.objectId}`,
49
+ name: `Slide ${i + 1}`,
50
+ type: 'slide',
51
+ index: i,
52
+ presentation_id: filters.presentation_id,
53
+ object_id: slide.objectId
54
+ }));
55
+ }
56
+
57
+ async _fetch_item(item_id: string): Promise<source_content> {
58
+ const [presentation_id, slide_id] = item_id.includes('#')
59
+ ? item_id.split('#', 2)
60
+ : [item_id, null];
61
+
62
+ const pres = await this.service.presentations.get({ presentationId: presentation_id });
63
+
64
+ const extract_text = (element: any): string => {
65
+ const texts: string[] = [];
66
+
67
+ if (element.shape?.text) {
68
+ for (const te of element.shape.text.textElements || []) {
69
+ if (te.textRun) texts.push(te.textRun.content || '');
70
+ }
71
+ }
72
+
73
+ if (element.table) {
74
+ for (const row of element.table.tableRows || []) {
75
+ for (const cell of row.tableCells || []) {
76
+ if (cell.text) {
77
+ for (const te of cell.text.textElements || []) {
78
+ if (te.textRun) texts.push(te.textRun.content || '');
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ return texts.join('');
86
+ };
87
+
88
+ const all_text: string[] = [];
89
+
90
+ for (let i = 0; i < (pres.data.slides || []).length; i++) {
91
+ const slide = pres.data.slides![i];
92
+ if (slide_id && slide.objectId !== slide_id) continue;
93
+
94
+ const slide_texts = [`## Slide ${i + 1}`];
95
+
96
+ for (const element of slide.pageElements || []) {
97
+ const txt = extract_text(element);
98
+ if (txt.trim()) slide_texts.push(txt.trim());
99
+ }
100
+
101
+ all_text.push(...slide_texts);
102
+ }
103
+
104
+ const text = all_text.join('\n\n');
105
+
106
+ return {
107
+ id: item_id,
108
+ name: pres.data.title || 'Untitled Presentation',
109
+ type: 'presentation',
110
+ text,
111
+ data: text,
112
+ meta: { source: 'google_slides', presentation_id, slide_count: pres.data.slides?.length || 0 }
113
+ };
114
+ }
115
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * openmemory sources - production-grade data source integrations
3
+ *
4
+ * provides connectors for external data sources:
5
+ * - google drive, sheets, slides
6
+ * - notion
7
+ * - onedrive
8
+ * - github
9
+ * - web crawler
10
+ */
11
+
12
+ export * from './base';
13
+ export * from './google_drive';
14
+ export * from './google_sheets';
15
+ export * from './google_slides';
16
+ export * from './notion';
17
+ export * from './onedrive';
18
+ export * from './github';
19
+ export * from './web_crawler';
@@ -0,0 +1,148 @@
1
+ /**
2
+ * notion source for openmemory - production grade
3
+ * requires: @notionhq/client
4
+ * env vars: NOTION_API_KEY
5
+ */
6
+
7
+ import { base_source, source_config_error, source_item, source_content } from './base';
8
+
9
+ export class notion_source extends base_source {
10
+ name = 'notion';
11
+ private client: any = null;
12
+
13
+ async _connect(creds: Record<string, any>): Promise<boolean> {
14
+ let Client: any;
15
+ try {
16
+ Client = await import('@notionhq/client').then(m => m.Client);
17
+ } catch {
18
+ throw new source_config_error('missing deps: npm install @notionhq/client', this.name);
19
+ }
20
+
21
+ const api_key = creds.api_key || process.env.NOTION_API_KEY;
22
+
23
+ if (!api_key) {
24
+ throw new source_config_error('no credentials: set NOTION_API_KEY', this.name);
25
+ }
26
+
27
+ this.client = new Client({ auth: api_key });
28
+ return true;
29
+ }
30
+
31
+ private extract_title(page: any): string {
32
+ const props = page.properties || {};
33
+ for (const prop of Object.values(props) as any[]) {
34
+ if (prop.type === 'title' && prop.title?.[0]) {
35
+ return prop.title[0].plain_text || '';
36
+ }
37
+ }
38
+ return '';
39
+ }
40
+
41
+ async _list_items(filters: Record<string, any>): Promise<source_item[]> {
42
+ const results: source_item[] = [];
43
+
44
+ if (filters.database_id) {
45
+ let has_more = true;
46
+ let start_cursor: string | undefined;
47
+
48
+ while (has_more) {
49
+ const resp = await this.client.databases.query({
50
+ database_id: filters.database_id,
51
+ start_cursor
52
+ });
53
+
54
+ for (const page of resp.results) {
55
+ results.push({
56
+ id: page.id,
57
+ name: this.extract_title(page) || 'Untitled',
58
+ type: 'page',
59
+ url: page.url || '',
60
+ last_edited: page.last_edited_time
61
+ });
62
+ }
63
+
64
+ has_more = resp.has_more;
65
+ start_cursor = resp.next_cursor;
66
+ }
67
+ } else {
68
+ const resp = await this.client.search({ filter: { property: 'object', value: 'page' } });
69
+
70
+ for (const page of resp.results) {
71
+ results.push({
72
+ id: page.id,
73
+ name: this.extract_title(page) || 'Untitled',
74
+ type: 'page',
75
+ url: page.url || '',
76
+ last_edited: page.last_edited_time
77
+ });
78
+ }
79
+ }
80
+
81
+ return results;
82
+ }
83
+
84
+ private block_to_text(block: any): string {
85
+ const texts: string[] = [];
86
+ const type = block.type;
87
+
88
+ const text_blocks = ['paragraph', 'heading_1', 'heading_2', 'heading_3',
89
+ 'bulleted_list_item', 'numbered_list_item', 'quote', 'callout'];
90
+
91
+ if (text_blocks.includes(type)) {
92
+ const rich_text = block[type]?.rich_text || [];
93
+ for (const rt of rich_text) {
94
+ texts.push(rt.plain_text || '');
95
+ }
96
+ } else if (type === 'code') {
97
+ const rich_text = block.code?.rich_text || [];
98
+ const lang = block.code?.language || '';
99
+ const code = rich_text.map((rt: any) => rt.plain_text || '').join('');
100
+ texts.push(`\`\`\`${lang}\n${code}\n\`\`\``);
101
+ } else if (type === 'to_do') {
102
+ const checked = block.to_do?.checked || false;
103
+ const rich_text = block.to_do?.rich_text || [];
104
+ const prefix = checked ? '[x] ' : '[ ] ';
105
+ texts.push(prefix + rich_text.map((rt: any) => rt.plain_text || '').join(''));
106
+ }
107
+
108
+ return texts.join('');
109
+ }
110
+
111
+ async _fetch_item(item_id: string): Promise<source_content> {
112
+ const page = await this.client.pages.retrieve({ page_id: item_id });
113
+ const title = this.extract_title(page);
114
+
115
+ // get all blocks
116
+ const blocks: any[] = [];
117
+ let has_more = true;
118
+ let start_cursor: string | undefined;
119
+
120
+ while (has_more) {
121
+ const resp = await this.client.blocks.children.list({
122
+ block_id: item_id,
123
+ start_cursor
124
+ });
125
+ blocks.push(...resp.results);
126
+ has_more = resp.has_more;
127
+ start_cursor = resp.next_cursor;
128
+ }
129
+
130
+ const text_parts = title ? [`# ${title}`] : [];
131
+
132
+ for (const block of blocks) {
133
+ const txt = this.block_to_text(block);
134
+ if (txt.trim()) text_parts.push(txt);
135
+ }
136
+
137
+ const text = text_parts.join('\n\n');
138
+
139
+ return {
140
+ id: item_id,
141
+ name: title || 'Untitled',
142
+ type: 'notion_page',
143
+ text,
144
+ data: text,
145
+ meta: { source: 'notion', page_id: item_id, url: page.url || '', block_count: blocks.length }
146
+ };
147
+ }
148
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * onedrive source for openmemory - production grade
3
+ * requires: @azure/msal-node
4
+ * env vars: AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID
5
+ */
6
+
7
+ import { base_source, source_config_error, source_auth_error, source_item, source_content } from './base';
8
+
9
+ export class onedrive_source extends base_source {
10
+ name = 'onedrive';
11
+ private access_token: string | null = null;
12
+ private graph_url = 'https://graph.microsoft.com/v1.0';
13
+
14
+ async _connect(creds: Record<string, any>): Promise<boolean> {
15
+ if (creds.access_token) {
16
+ this.access_token = creds.access_token;
17
+ return true;
18
+ }
19
+
20
+ let msal: any;
21
+ try {
22
+ msal = await import('@azure/msal-node');
23
+ } catch {
24
+ throw new source_config_error('missing deps: npm install @azure/msal-node', this.name);
25
+ }
26
+
27
+ const client_id = creds.client_id || process.env.AZURE_CLIENT_ID;
28
+ const client_secret = creds.client_secret || process.env.AZURE_CLIENT_SECRET;
29
+ const tenant_id = creds.tenant_id || process.env.AZURE_TENANT_ID;
30
+
31
+ if (!client_id || !client_secret || !tenant_id) {
32
+ throw new source_config_error(
33
+ 'no credentials: set AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID',
34
+ this.name
35
+ );
36
+ }
37
+
38
+ const app = new msal.ConfidentialClientApplication({
39
+ auth: {
40
+ clientId: client_id,
41
+ clientSecret: client_secret,
42
+ authority: `https://login.microsoftonline.com/${tenant_id}`
43
+ }
44
+ });
45
+
46
+ const result = await app.acquireTokenByClientCredential({
47
+ scopes: ['https://graph.microsoft.com/.default']
48
+ });
49
+
50
+ if (result?.accessToken) {
51
+ this.access_token = result.accessToken;
52
+ return true;
53
+ }
54
+
55
+ throw new source_auth_error('auth failed: no access token returned', this.name);
56
+ }
57
+
58
+ async _list_items(filters: Record<string, any>): Promise<source_item[]> {
59
+ const folder_path = filters.folder_path || '/';
60
+ const user_principal = filters.user_principal;
61
+
62
+ const base = user_principal
63
+ ? `${this.graph_url}/users/${user_principal}/drive`
64
+ : `${this.graph_url}/me/drive`;
65
+
66
+ const url = folder_path === '/'
67
+ ? `${base}/root/children`
68
+ : `${base}/root:/${folder_path.replace(/^\/|\/$/g, '')}:/children`;
69
+
70
+ const results: source_item[] = [];
71
+ let next_url: string | null = url;
72
+
73
+ while (next_url) {
74
+ const resp: Response = await fetch(next_url, {
75
+ headers: { Authorization: `Bearer ${this.access_token}` }
76
+ });
77
+
78
+ if (!resp.ok) throw new Error(`http ${resp.status}: ${resp.statusText}`);
79
+
80
+ const data: any = await resp.json();
81
+
82
+ for (const item of data.value || []) {
83
+ results.push({
84
+ id: item.id,
85
+ name: item.name,
86
+ type: 'folder' in item ? 'folder' : item.file?.mimeType || 'file',
87
+ size: item.size || 0,
88
+ modified: item.lastModifiedDateTime,
89
+ path: item.parentReference?.path || ''
90
+ });
91
+ }
92
+
93
+ next_url = data['@odata.nextLink'] || null;
94
+ }
95
+
96
+ return results;
97
+ }
98
+
99
+ async _fetch_item(item_id: string): Promise<source_content> {
100
+ const base = `${this.graph_url}/me/drive`;
101
+
102
+ const meta_resp = await fetch(`${base}/items/${item_id}`, {
103
+ headers: { Authorization: `Bearer ${this.access_token}` }
104
+ });
105
+
106
+ if (!meta_resp.ok) throw new Error(`http ${meta_resp.status}`);
107
+ const meta = await meta_resp.json();
108
+
109
+ const content_resp = await fetch(`${base}/items/${item_id}/content`, {
110
+ headers: { Authorization: `Bearer ${this.access_token}` },
111
+ redirect: 'follow'
112
+ });
113
+
114
+ if (!content_resp.ok) throw new Error(`http ${content_resp.status}`);
115
+ const data = Buffer.from(await content_resp.arrayBuffer());
116
+
117
+ let text = '';
118
+ try {
119
+ text = data.toString('utf-8');
120
+ } catch { }
121
+
122
+ return {
123
+ id: item_id,
124
+ name: meta.name || 'unknown',
125
+ type: meta.file?.mimeType || 'unknown',
126
+ text,
127
+ data,
128
+ meta: { source: 'onedrive', item_id, size: meta.size || 0, mime_type: meta.file?.mimeType || '' }
129
+ };
130
+ }
131
+ }