@roomi-fields/notebooklm-mcp 1.5.9 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -37
- package/deployment/docs/03-API.md +12 -3
- package/deployment/docs/12-BATCH-1000.md +165 -0
- package/deployment/docs/13-COMPARE.md +12 -0
- package/deployment/docs/14-RTFM-INTEGRATION.md +288 -0
- package/deployment/docs/openapi.yaml +492 -0
- package/dist/http-wrapper.d.ts.map +1 -1
- package/dist/http-wrapper.js +115 -0
- package/dist/http-wrapper.js.map +1 -1
- package/dist/utils/vault-writer.d.ts +61 -0
- package/dist/utils/vault-writer.d.ts.map +1 -0
- package/dist/utils/vault-writer.js +125 -0
- package/dist/utils/vault-writer.js.map +1 -0
- package/docs/MCP_DIRECTORIES.md +84 -0
- package/package.json +1 -1
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
openapi: 3.1.0
|
|
2
|
+
info:
|
|
3
|
+
title: NotebookLM REST API
|
|
4
|
+
version: 1.5.9
|
|
5
|
+
description: |
|
|
6
|
+
Local HTTP REST API for Google NotebookLM. Citation-backed Q&A, Studio
|
|
7
|
+
content generation (audio, video, infographic, report, presentation,
|
|
8
|
+
data table), notebook library management, multi-account auto-reauth.
|
|
9
|
+
|
|
10
|
+
Companion to the MCP server in the same package
|
|
11
|
+
(`@roomi-fields/notebooklm-mcp`). See
|
|
12
|
+
https://roomi-fields.github.io/notebooklm-mcp/ for the full guide.
|
|
13
|
+
license:
|
|
14
|
+
name: MIT
|
|
15
|
+
url: https://opensource.org/licenses/MIT
|
|
16
|
+
contact:
|
|
17
|
+
name: Romain Peyrichou
|
|
18
|
+
url: https://github.com/roomi-fields/notebooklm-mcp
|
|
19
|
+
|
|
20
|
+
servers:
|
|
21
|
+
- url: http://localhost:3000
|
|
22
|
+
description: Local server (default)
|
|
23
|
+
- url: http://{host}:{port}
|
|
24
|
+
description: Custom host
|
|
25
|
+
variables:
|
|
26
|
+
host:
|
|
27
|
+
default: localhost
|
|
28
|
+
port:
|
|
29
|
+
default: '3000'
|
|
30
|
+
|
|
31
|
+
tags:
|
|
32
|
+
- name: Health
|
|
33
|
+
- name: Auth
|
|
34
|
+
- name: Q&A
|
|
35
|
+
- name: Notebooks
|
|
36
|
+
- name: Sources
|
|
37
|
+
- name: Content
|
|
38
|
+
- name: Sessions
|
|
39
|
+
|
|
40
|
+
paths:
|
|
41
|
+
/health:
|
|
42
|
+
get:
|
|
43
|
+
tags: [Health]
|
|
44
|
+
summary: Server health check
|
|
45
|
+
responses:
|
|
46
|
+
'200':
|
|
47
|
+
description: Server status, uptime, active sessions
|
|
48
|
+
content:
|
|
49
|
+
application/json:
|
|
50
|
+
schema: { $ref: '#/components/schemas/Health' }
|
|
51
|
+
|
|
52
|
+
/setup-auth:
|
|
53
|
+
post:
|
|
54
|
+
tags: [Auth]
|
|
55
|
+
summary: First-time Google authentication (opens visible browser)
|
|
56
|
+
requestBody:
|
|
57
|
+
content:
|
|
58
|
+
application/json:
|
|
59
|
+
schema:
|
|
60
|
+
type: object
|
|
61
|
+
properties:
|
|
62
|
+
show_browser: { type: boolean, default: true }
|
|
63
|
+
account: { type: string }
|
|
64
|
+
responses:
|
|
65
|
+
'200':
|
|
66
|
+
description: Auth completed
|
|
67
|
+
content: { application/json: { schema: { $ref: '#/components/schemas/Result' } } }
|
|
68
|
+
|
|
69
|
+
/de-auth:
|
|
70
|
+
post:
|
|
71
|
+
tags: [Auth]
|
|
72
|
+
summary: Logout, clear all credentials
|
|
73
|
+
responses:
|
|
74
|
+
'200':
|
|
75
|
+
description: Logged out
|
|
76
|
+
content: { application/json: { schema: { $ref: '#/components/schemas/Result' } } }
|
|
77
|
+
|
|
78
|
+
/re-auth:
|
|
79
|
+
post:
|
|
80
|
+
tags: [Auth]
|
|
81
|
+
summary: Re-authenticate or switch account
|
|
82
|
+
requestBody:
|
|
83
|
+
content:
|
|
84
|
+
application/json:
|
|
85
|
+
schema:
|
|
86
|
+
type: object
|
|
87
|
+
properties:
|
|
88
|
+
account: { type: string }
|
|
89
|
+
responses:
|
|
90
|
+
'200':
|
|
91
|
+
description: Re-authenticated
|
|
92
|
+
content: { application/json: { schema: { $ref: '#/components/schemas/Result' } } }
|
|
93
|
+
|
|
94
|
+
/ask:
|
|
95
|
+
post:
|
|
96
|
+
tags: [Q&A]
|
|
97
|
+
summary: Ask a question with citation-backed response
|
|
98
|
+
requestBody:
|
|
99
|
+
required: true
|
|
100
|
+
content:
|
|
101
|
+
application/json:
|
|
102
|
+
schema: { $ref: '#/components/schemas/AskRequest' }
|
|
103
|
+
responses:
|
|
104
|
+
'200':
|
|
105
|
+
description: Answer with citations
|
|
106
|
+
content: { application/json: { schema: { $ref: '#/components/schemas/AskResponse' } } }
|
|
107
|
+
'400':
|
|
108
|
+
description: Missing question
|
|
109
|
+
'500':
|
|
110
|
+
description: Server error
|
|
111
|
+
|
|
112
|
+
/notebooks:
|
|
113
|
+
get:
|
|
114
|
+
tags: [Notebooks]
|
|
115
|
+
summary: List all notebooks in the local library
|
|
116
|
+
responses:
|
|
117
|
+
'200': { description: Notebook list }
|
|
118
|
+
post:
|
|
119
|
+
tags: [Notebooks]
|
|
120
|
+
summary: Manually add a notebook to the library
|
|
121
|
+
requestBody:
|
|
122
|
+
content:
|
|
123
|
+
application/json:
|
|
124
|
+
schema:
|
|
125
|
+
type: object
|
|
126
|
+
required: [id, name, url]
|
|
127
|
+
properties:
|
|
128
|
+
id: { type: string }
|
|
129
|
+
name: { type: string }
|
|
130
|
+
url: { type: string }
|
|
131
|
+
description: { type: string }
|
|
132
|
+
topics: { type: array, items: { type: string } }
|
|
133
|
+
responses:
|
|
134
|
+
'200': { description: Notebook added }
|
|
135
|
+
|
|
136
|
+
/notebooks/scrape:
|
|
137
|
+
get:
|
|
138
|
+
tags: [Notebooks]
|
|
139
|
+
summary: Scrape all notebooks from NotebookLM UI
|
|
140
|
+
responses:
|
|
141
|
+
'200': { description: Live notebook list with ids and names }
|
|
142
|
+
|
|
143
|
+
/notebooks/import-from-scrape:
|
|
144
|
+
post:
|
|
145
|
+
tags: [Notebooks]
|
|
146
|
+
summary: Bulk import scraped notebooks into local library
|
|
147
|
+
responses:
|
|
148
|
+
'200': { description: Import summary }
|
|
149
|
+
|
|
150
|
+
/notebooks/auto-discover:
|
|
151
|
+
post:
|
|
152
|
+
tags: [Notebooks]
|
|
153
|
+
summary: Generate notebook metadata autonomously via NotebookLM queries
|
|
154
|
+
requestBody:
|
|
155
|
+
content:
|
|
156
|
+
application/json:
|
|
157
|
+
schema:
|
|
158
|
+
type: object
|
|
159
|
+
properties:
|
|
160
|
+
notebook_id: { type: string }
|
|
161
|
+
notebook_url: { type: string }
|
|
162
|
+
responses:
|
|
163
|
+
'200': { description: Generated metadata }
|
|
164
|
+
|
|
165
|
+
/notebooks/search:
|
|
166
|
+
get:
|
|
167
|
+
tags: [Notebooks]
|
|
168
|
+
summary: Search the local library by keyword
|
|
169
|
+
parameters:
|
|
170
|
+
- in: query
|
|
171
|
+
name: q
|
|
172
|
+
required: true
|
|
173
|
+
schema: { type: string }
|
|
174
|
+
responses:
|
|
175
|
+
'200': { description: Matching notebooks }
|
|
176
|
+
|
|
177
|
+
/notebooks/stats:
|
|
178
|
+
get:
|
|
179
|
+
tags: [Notebooks]
|
|
180
|
+
summary: Library statistics
|
|
181
|
+
responses:
|
|
182
|
+
'200': { description: Counts and aggregates }
|
|
183
|
+
|
|
184
|
+
/notebooks/bulk-delete:
|
|
185
|
+
delete:
|
|
186
|
+
tags: [Notebooks]
|
|
187
|
+
summary: Delete multiple notebooks at once
|
|
188
|
+
requestBody:
|
|
189
|
+
content:
|
|
190
|
+
application/json:
|
|
191
|
+
schema:
|
|
192
|
+
type: object
|
|
193
|
+
properties:
|
|
194
|
+
ids: { type: array, items: { type: string } }
|
|
195
|
+
responses:
|
|
196
|
+
'200': { description: Deletion summary }
|
|
197
|
+
|
|
198
|
+
/notebooks/{id}:
|
|
199
|
+
get:
|
|
200
|
+
tags: [Notebooks]
|
|
201
|
+
summary: Get notebook details
|
|
202
|
+
parameters:
|
|
203
|
+
- in: path
|
|
204
|
+
name: id
|
|
205
|
+
required: true
|
|
206
|
+
schema: { type: string }
|
|
207
|
+
responses:
|
|
208
|
+
'200': { description: Notebook details }
|
|
209
|
+
put:
|
|
210
|
+
tags: [Notebooks]
|
|
211
|
+
summary: Update notebook metadata
|
|
212
|
+
parameters:
|
|
213
|
+
- in: path
|
|
214
|
+
name: id
|
|
215
|
+
required: true
|
|
216
|
+
schema: { type: string }
|
|
217
|
+
responses:
|
|
218
|
+
'200': { description: Updated }
|
|
219
|
+
delete:
|
|
220
|
+
tags: [Notebooks]
|
|
221
|
+
summary: Delete a notebook from the library
|
|
222
|
+
parameters:
|
|
223
|
+
- in: path
|
|
224
|
+
name: id
|
|
225
|
+
required: true
|
|
226
|
+
schema: { type: string }
|
|
227
|
+
responses:
|
|
228
|
+
'200': { description: Deleted }
|
|
229
|
+
|
|
230
|
+
/notebooks/{id}/activate:
|
|
231
|
+
put:
|
|
232
|
+
tags: [Notebooks]
|
|
233
|
+
summary: Set notebook as the default active notebook
|
|
234
|
+
parameters:
|
|
235
|
+
- in: path
|
|
236
|
+
name: id
|
|
237
|
+
required: true
|
|
238
|
+
schema: { type: string }
|
|
239
|
+
responses:
|
|
240
|
+
'200': { description: Activated }
|
|
241
|
+
|
|
242
|
+
/notebooks/create:
|
|
243
|
+
post:
|
|
244
|
+
tags: [Notebooks]
|
|
245
|
+
summary: Create a new notebook in NotebookLM
|
|
246
|
+
requestBody:
|
|
247
|
+
content:
|
|
248
|
+
application/json:
|
|
249
|
+
schema:
|
|
250
|
+
type: object
|
|
251
|
+
properties:
|
|
252
|
+
name: { type: string }
|
|
253
|
+
description: { type: string }
|
|
254
|
+
responses:
|
|
255
|
+
'200': { description: Created notebook }
|
|
256
|
+
|
|
257
|
+
/sessions:
|
|
258
|
+
get:
|
|
259
|
+
tags: [Sessions]
|
|
260
|
+
summary: List active sessions
|
|
261
|
+
responses:
|
|
262
|
+
'200': { description: Active sessions }
|
|
263
|
+
|
|
264
|
+
/sessions/{id}:
|
|
265
|
+
delete:
|
|
266
|
+
tags: [Sessions]
|
|
267
|
+
summary: Close a session
|
|
268
|
+
parameters:
|
|
269
|
+
- in: path
|
|
270
|
+
name: id
|
|
271
|
+
required: true
|
|
272
|
+
schema: { type: string }
|
|
273
|
+
responses:
|
|
274
|
+
'200': { description: Closed }
|
|
275
|
+
|
|
276
|
+
/sessions/{id}/reset:
|
|
277
|
+
post:
|
|
278
|
+
tags: [Sessions]
|
|
279
|
+
summary: Reset a session's history
|
|
280
|
+
parameters:
|
|
281
|
+
- in: path
|
|
282
|
+
name: id
|
|
283
|
+
required: true
|
|
284
|
+
schema: { type: string }
|
|
285
|
+
responses:
|
|
286
|
+
'200': { description: Reset }
|
|
287
|
+
|
|
288
|
+
/content/sources:
|
|
289
|
+
post:
|
|
290
|
+
tags: [Sources]
|
|
291
|
+
summary: Add a source (file, url, text, youtube, drive) to the active notebook
|
|
292
|
+
requestBody:
|
|
293
|
+
required: true
|
|
294
|
+
content:
|
|
295
|
+
application/json:
|
|
296
|
+
schema: { $ref: '#/components/schemas/AddSourceRequest' }
|
|
297
|
+
responses:
|
|
298
|
+
'200': { description: Source added }
|
|
299
|
+
delete:
|
|
300
|
+
tags: [Sources]
|
|
301
|
+
summary: Delete source by name (query parameter)
|
|
302
|
+
parameters:
|
|
303
|
+
- in: query
|
|
304
|
+
name: name
|
|
305
|
+
required: true
|
|
306
|
+
schema: { type: string }
|
|
307
|
+
responses:
|
|
308
|
+
'200': { description: Deleted }
|
|
309
|
+
|
|
310
|
+
/content/sources/{id}:
|
|
311
|
+
delete:
|
|
312
|
+
tags: [Sources]
|
|
313
|
+
summary: Delete source by id
|
|
314
|
+
parameters:
|
|
315
|
+
- in: path
|
|
316
|
+
name: id
|
|
317
|
+
required: true
|
|
318
|
+
schema: { type: string }
|
|
319
|
+
responses:
|
|
320
|
+
'200': { description: Deleted }
|
|
321
|
+
|
|
322
|
+
/content/generate:
|
|
323
|
+
post:
|
|
324
|
+
tags: [Content]
|
|
325
|
+
summary: Generate Studio content (audio, video, infographic, report, presentation, data table)
|
|
326
|
+
requestBody:
|
|
327
|
+
required: true
|
|
328
|
+
content:
|
|
329
|
+
application/json:
|
|
330
|
+
schema: { $ref: '#/components/schemas/GenerateRequest' }
|
|
331
|
+
responses:
|
|
332
|
+
'200': { description: Generated artifact metadata }
|
|
333
|
+
|
|
334
|
+
/content/download:
|
|
335
|
+
get:
|
|
336
|
+
tags: [Content]
|
|
337
|
+
summary: Download a generated artifact (WAV, MP4, PNG)
|
|
338
|
+
parameters:
|
|
339
|
+
- in: query
|
|
340
|
+
name: content_id
|
|
341
|
+
required: true
|
|
342
|
+
schema: { type: string }
|
|
343
|
+
responses:
|
|
344
|
+
'200': { description: Binary file }
|
|
345
|
+
|
|
346
|
+
/content:
|
|
347
|
+
get:
|
|
348
|
+
tags: [Content]
|
|
349
|
+
summary: List sources and generated content for the active notebook
|
|
350
|
+
responses:
|
|
351
|
+
'200': { description: Content list }
|
|
352
|
+
|
|
353
|
+
/content/notes:
|
|
354
|
+
post:
|
|
355
|
+
tags: [Content]
|
|
356
|
+
summary: Create a note in the active notebook
|
|
357
|
+
responses:
|
|
358
|
+
'200': { description: Note created }
|
|
359
|
+
|
|
360
|
+
/content/chat-to-note:
|
|
361
|
+
post:
|
|
362
|
+
tags: [Content]
|
|
363
|
+
summary: Save the current chat discussion as a note
|
|
364
|
+
responses:
|
|
365
|
+
'200': { description: Note saved }
|
|
366
|
+
|
|
367
|
+
/content/notes/{noteTitle}/to-source:
|
|
368
|
+
post:
|
|
369
|
+
tags: [Content]
|
|
370
|
+
summary: Convert a note into a NotebookLM source
|
|
371
|
+
parameters:
|
|
372
|
+
- in: path
|
|
373
|
+
name: noteTitle
|
|
374
|
+
required: true
|
|
375
|
+
schema: { type: string }
|
|
376
|
+
responses:
|
|
377
|
+
'200': { description: Note converted to source }
|
|
378
|
+
|
|
379
|
+
/cleanup-data:
|
|
380
|
+
post:
|
|
381
|
+
tags: [Auth]
|
|
382
|
+
summary: Wipe all local data (requires confirmation)
|
|
383
|
+
requestBody:
|
|
384
|
+
content:
|
|
385
|
+
application/json:
|
|
386
|
+
schema:
|
|
387
|
+
type: object
|
|
388
|
+
properties:
|
|
389
|
+
confirm: { type: boolean }
|
|
390
|
+
responses:
|
|
391
|
+
'200': { description: Cleaned }
|
|
392
|
+
|
|
393
|
+
components:
|
|
394
|
+
schemas:
|
|
395
|
+
Health:
|
|
396
|
+
type: object
|
|
397
|
+
properties:
|
|
398
|
+
status: { type: string, example: ok }
|
|
399
|
+
uptime_s: { type: number }
|
|
400
|
+
active_sessions: { type: integer }
|
|
401
|
+
version: { type: string, example: 1.5.9 }
|
|
402
|
+
|
|
403
|
+
Result:
|
|
404
|
+
type: object
|
|
405
|
+
properties:
|
|
406
|
+
success: { type: boolean }
|
|
407
|
+
message: { type: string }
|
|
408
|
+
error: { type: string }
|
|
409
|
+
|
|
410
|
+
AskRequest:
|
|
411
|
+
type: object
|
|
412
|
+
required: [question]
|
|
413
|
+
properties:
|
|
414
|
+
question:
|
|
415
|
+
type: string
|
|
416
|
+
example: 'Summarize chapter 3'
|
|
417
|
+
notebook_id:
|
|
418
|
+
type: string
|
|
419
|
+
notebook_url:
|
|
420
|
+
type: string
|
|
421
|
+
session_id:
|
|
422
|
+
type: string
|
|
423
|
+
source_format:
|
|
424
|
+
type: string
|
|
425
|
+
enum: [none, inline, footnotes, json, expanded]
|
|
426
|
+
default: json
|
|
427
|
+
show_browser:
|
|
428
|
+
type: boolean
|
|
429
|
+
default: false
|
|
430
|
+
|
|
431
|
+
AskResponse:
|
|
432
|
+
type: object
|
|
433
|
+
properties:
|
|
434
|
+
success: { type: boolean }
|
|
435
|
+
answer: { type: string }
|
|
436
|
+
citations:
|
|
437
|
+
type: array
|
|
438
|
+
items:
|
|
439
|
+
type: object
|
|
440
|
+
properties:
|
|
441
|
+
id: { type: integer }
|
|
442
|
+
source: { type: string }
|
|
443
|
+
excerpt: { type: string }
|
|
444
|
+
session_id: { type: string }
|
|
445
|
+
|
|
446
|
+
AddSourceRequest:
|
|
447
|
+
type: object
|
|
448
|
+
required: [source_type]
|
|
449
|
+
properties:
|
|
450
|
+
source_type:
|
|
451
|
+
type: string
|
|
452
|
+
enum: [file, url, text, youtube, drive]
|
|
453
|
+
file_path: { type: string }
|
|
454
|
+
url: { type: string }
|
|
455
|
+
text: { type: string }
|
|
456
|
+
title: { type: string }
|
|
457
|
+
notebook_url: { type: string }
|
|
458
|
+
session_id: { type: string }
|
|
459
|
+
|
|
460
|
+
GenerateRequest:
|
|
461
|
+
type: object
|
|
462
|
+
required: [content_type]
|
|
463
|
+
properties:
|
|
464
|
+
content_type:
|
|
465
|
+
type: string
|
|
466
|
+
enum:
|
|
467
|
+
- audio_overview
|
|
468
|
+
- video
|
|
469
|
+
- infographic
|
|
470
|
+
- report
|
|
471
|
+
- presentation
|
|
472
|
+
- data_table
|
|
473
|
+
language: { type: string, example: en }
|
|
474
|
+
custom_instructions: { type: string }
|
|
475
|
+
video_style:
|
|
476
|
+
type: string
|
|
477
|
+
enum: [classroom, documentary, animated, corporate, cinematic, minimalist]
|
|
478
|
+
video_format:
|
|
479
|
+
type: string
|
|
480
|
+
enum: [brief, explainer]
|
|
481
|
+
infographic_format:
|
|
482
|
+
type: string
|
|
483
|
+
enum: [horizontal, vertical]
|
|
484
|
+
report_format:
|
|
485
|
+
type: string
|
|
486
|
+
enum: [summary, detailed]
|
|
487
|
+
presentation_style:
|
|
488
|
+
type: string
|
|
489
|
+
enum: [overview, detailed]
|
|
490
|
+
presentation_length:
|
|
491
|
+
type: string
|
|
492
|
+
enum: [short, medium, long]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-wrapper.d.ts","sourceRoot":"","sources":["../src/http-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"http-wrapper.d.ts","sourceRoot":"","sources":["../src/http-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwBH,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,SAAS,EAAE,MAAM,CAAC;SACnB;KACF;CACF"}
|
package/dist/http-wrapper.js
CHANGED
|
@@ -8,6 +8,8 @@ import express from 'express';
|
|
|
8
8
|
import { randomUUID } from 'crypto';
|
|
9
9
|
import net from 'net';
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
|
+
import { promises as fs } from 'fs';
|
|
12
|
+
import path from 'path';
|
|
11
13
|
import { AuthManager } from './auth/auth-manager.js';
|
|
12
14
|
import { SessionManager } from './session/session-manager.js';
|
|
13
15
|
import { NotebookLibrary } from './library/notebook-library.js';
|
|
@@ -15,6 +17,7 @@ import { ToolHandlers } from './tools/index.js';
|
|
|
15
17
|
import { AutoDiscovery } from './auto-discovery/auto-discovery.js';
|
|
16
18
|
import { StartupManager } from './startup/startup-manager.js';
|
|
17
19
|
import { log } from './utils/logger.js';
|
|
20
|
+
import { formatAnswerJson, formatAnswerMarkdown, makeSlug, } from './utils/vault-writer.js';
|
|
18
21
|
const app = express();
|
|
19
22
|
app.use(express.json({ limit: '10mb' }));
|
|
20
23
|
// Request ID middleware for debugging and log correlation
|
|
@@ -48,6 +51,7 @@ app.get('/', (_req, res) => {
|
|
|
48
51
|
endpoints: {
|
|
49
52
|
health: 'GET /health',
|
|
50
53
|
ask: 'POST /ask',
|
|
54
|
+
batch_to_vault: 'POST /batch-to-vault',
|
|
51
55
|
setup_auth: 'POST /setup-auth',
|
|
52
56
|
notebooks: 'GET /notebooks',
|
|
53
57
|
sessions: 'GET /sessions',
|
|
@@ -94,6 +98,117 @@ app.post('/ask', async (req, res) => {
|
|
|
94
98
|
});
|
|
95
99
|
}
|
|
96
100
|
});
|
|
101
|
+
// Batch-to-vault — run a list of questions and persist each answer as
|
|
102
|
+
// markdown + JSON sidecar files, ready for ingestion by RTFM or any
|
|
103
|
+
// markdown vault indexer. Conforms to the nblm-answer-v1 schema.
|
|
104
|
+
app.post('/batch-to-vault', async (req, res) => {
|
|
105
|
+
const reqId = req.requestId.substring(0, 8);
|
|
106
|
+
try {
|
|
107
|
+
const { questions, notebook_id, notebook_url, vault_dir, slug_prefix = '', source_format = 'json', sleep_between_ms = 0, session_id, } = req.body;
|
|
108
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
109
|
+
return res.status(400).json({
|
|
110
|
+
success: false,
|
|
111
|
+
error: 'Missing or empty required field: questions (non-empty string array)',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (!vault_dir || typeof vault_dir !== 'string') {
|
|
115
|
+
return res.status(400).json({
|
|
116
|
+
success: false,
|
|
117
|
+
error: 'Missing required field: vault_dir (absolute or relative directory path)',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const absVaultDir = path.resolve(vault_dir);
|
|
121
|
+
await fs.mkdir(absVaultDir, { recursive: true });
|
|
122
|
+
log.info(`[${reqId}] /batch-to-vault — ${questions.length} questions → ${absVaultDir}`);
|
|
123
|
+
const results = [];
|
|
124
|
+
let currentSession = session_id;
|
|
125
|
+
const notebookMeta = {};
|
|
126
|
+
for (let i = 0; i < questions.length; i++) {
|
|
127
|
+
const q = questions[i];
|
|
128
|
+
log.info(`[${reqId}] [${i + 1}/${questions.length}] ${String(q).substring(0, 80)}`);
|
|
129
|
+
try {
|
|
130
|
+
const askResult = await toolHandlers.handleAskQuestion({
|
|
131
|
+
question: q,
|
|
132
|
+
session_id: currentSession,
|
|
133
|
+
notebook_id,
|
|
134
|
+
notebook_url,
|
|
135
|
+
source_format,
|
|
136
|
+
}, async () => { });
|
|
137
|
+
if (!askResult?.success || !askResult.data || askResult.data.status !== 'success') {
|
|
138
|
+
const errMsg = askResult?.error ||
|
|
139
|
+
(askResult?.data && 'error' in askResult.data ? askResult.data.error : 'Unknown error');
|
|
140
|
+
results.push({
|
|
141
|
+
question: q,
|
|
142
|
+
md_path: '',
|
|
143
|
+
json_path: '',
|
|
144
|
+
success: false,
|
|
145
|
+
citations_count: 0,
|
|
146
|
+
error: errMsg,
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const data = askResult.data;
|
|
151
|
+
if (data.session_id)
|
|
152
|
+
currentSession = data.session_id;
|
|
153
|
+
if (!notebookMeta.url && data.notebook_url)
|
|
154
|
+
notebookMeta.url = data.notebook_url;
|
|
155
|
+
if (!notebookMeta.id && notebook_id)
|
|
156
|
+
notebookMeta.id = notebook_id;
|
|
157
|
+
const askedAt = new Date().toISOString();
|
|
158
|
+
const slug = makeSlug(q, slug_prefix, i);
|
|
159
|
+
const mdPath = path.join(absVaultDir, `${slug}.md`);
|
|
160
|
+
const jsonPath = path.join(absVaultDir, `${slug}.json`);
|
|
161
|
+
const markdown = formatAnswerMarkdown(data, notebookMeta, askedAt);
|
|
162
|
+
const jsonPayload = formatAnswerJson(data, notebookMeta, askedAt);
|
|
163
|
+
await fs.writeFile(mdPath, markdown, 'utf-8');
|
|
164
|
+
await fs.writeFile(jsonPath, JSON.stringify(jsonPayload, null, 2), 'utf-8');
|
|
165
|
+
results.push({
|
|
166
|
+
question: q,
|
|
167
|
+
md_path: mdPath,
|
|
168
|
+
json_path: jsonPath,
|
|
169
|
+
success: true,
|
|
170
|
+
citations_count: data.sources?.citations.length ?? 0,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
log.error(`[${reqId}] [${i + 1}] failed: ${msg}`);
|
|
176
|
+
results.push({
|
|
177
|
+
question: q,
|
|
178
|
+
md_path: '',
|
|
179
|
+
json_path: '',
|
|
180
|
+
success: false,
|
|
181
|
+
citations_count: 0,
|
|
182
|
+
error: msg,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (sleep_between_ms > 0 && i < questions.length - 1) {
|
|
186
|
+
await new Promise((r) => setTimeout(r, sleep_between_ms));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
190
|
+
log.success(`[${reqId}] /batch-to-vault — ${succeeded}/${questions.length} written to ${absVaultDir}`);
|
|
191
|
+
res.json({
|
|
192
|
+
success: true,
|
|
193
|
+
data: {
|
|
194
|
+
vault_dir: absVaultDir,
|
|
195
|
+
total: questions.length,
|
|
196
|
+
succeeded,
|
|
197
|
+
failed: questions.length - succeeded,
|
|
198
|
+
session_id: currentSession,
|
|
199
|
+
notebook: notebookMeta,
|
|
200
|
+
files: results,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
log.error(`[${reqId}] /batch-to-vault - Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
res.status(500).json({
|
|
207
|
+
success: false,
|
|
208
|
+
error: error instanceof Error ? error.message : String(error),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
});
|
|
97
212
|
// Setup auth
|
|
98
213
|
app.post('/setup-auth', async (req, res) => {
|
|
99
214
|
try {
|