@manudota/artist-mcp 2.3.0 → 2.3.1-staging.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents.js +1 -20
- package/dist/api.d.ts +0 -23
- package/dist/api.js +0 -36
- package/dist/dispatch.d.ts +0 -4
- package/dist/dispatch.js +1 -4
- package/dist/mail.js +3 -3
- package/dist/notes.d.ts +4 -71
- package/dist/notes.js +94 -233
- package/dist/onenote-patch.js +0 -27
- package/dist/server.js +45 -74
- package/package.json +1 -1
package/dist/agents.js
CHANGED
|
@@ -117,26 +117,7 @@ const resolveRegistry = async () => {
|
|
|
117
117
|
]));
|
|
118
118
|
const localRoot = localAgentRoot();
|
|
119
119
|
if (localRoot) {
|
|
120
|
-
const
|
|
121
|
-
// Replacing rather than layering, where the deployment says so.
|
|
122
|
-
//
|
|
123
|
-
// Layering is right for a musician who has copied out a few playbooks and
|
|
124
|
-
// edited them: the ones they did not copy should keep working. Hosted is
|
|
125
|
-
// the other case — one pack, written for one notebook, and a bundled
|
|
126
|
-
// playbook surviving because the custom pack happens not to name its id is
|
|
127
|
-
// a generic rule in force that nobody chose. Cleared before the local
|
|
128
|
-
// entries go in, so what is served is exactly what is in that directory.
|
|
129
|
-
//
|
|
130
|
-
// Only ever narrows: a mode this does not recognise layers, which is what
|
|
131
|
-
// every install that has never heard of it already does.
|
|
132
|
-
// No emptiness check here, deliberately. `readLocalRegistry` already throws
|
|
133
|
-
// for a directory with no `artist/` and for one holding no Markdown, so by
|
|
134
|
-
// this line `local` cannot be empty and a guard would be unreachable code
|
|
135
|
-
// claiming to protect something. The strictness this mode needs is the
|
|
136
|
-
// strictness the local layer already has.
|
|
137
|
-
if (process.env.ARTIST_MCP_PACK_MODE === 'replace')
|
|
138
|
-
byId.clear();
|
|
139
|
-
for (const entry of local) {
|
|
120
|
+
for (const entry of (await readLocalRegistry(localRoot)).entries) {
|
|
140
121
|
byId.set(entry.id, { ...entry, source: 'local', origin: localRoot });
|
|
141
122
|
}
|
|
142
123
|
}
|
package/dist/api.d.ts
CHANGED
|
@@ -9,29 +9,6 @@
|
|
|
9
9
|
export declare const GRAPH = "https://graph.microsoft.com/v1.0";
|
|
10
10
|
export declare const GMAIL = "https://gmail.googleapis.com/gmail/v1";
|
|
11
11
|
export declare const CALENDAR = "https://www.googleapis.com/calendar/v3";
|
|
12
|
-
/**
|
|
13
|
-
* How many provider requests one tool call may have in flight at once.
|
|
14
|
-
*
|
|
15
|
-
* Jitter above spreads the *retries* of a herd; nothing spread the herd's
|
|
16
|
-
* first arrival, and that is the burst the logs actually show — forty
|
|
17
|
-
* `attempt=1 waited=0s` refusals landing in the same millisecond, because a
|
|
18
|
-
* survey of a forty-page notebook opened forty Graph requests simultaneously.
|
|
19
|
-
* Every one of them then had to win its own retry ladder against a provider
|
|
20
|
-
* that was only busy because of them.
|
|
21
|
-
*
|
|
22
|
-
* Four is chosen to be boring rather than optimal: it is slow enough that
|
|
23
|
-
* OneNote answers instead of refusing, and parallel enough that a survey is
|
|
24
|
-
* still meaningfully faster than a loop. The number that matters is "not N".
|
|
25
|
-
*/
|
|
26
|
-
export declare const FANOUT_LIMIT = 4;
|
|
27
|
-
/**
|
|
28
|
-
* `Promise.all(items.map(fn))` with a ceiling on how many run at once.
|
|
29
|
-
*
|
|
30
|
-
* Results stay in the order of `items`, and a rejection rejects the whole
|
|
31
|
-
* call, so this is a drop-in for the `Promise.all` it replaces: callers that
|
|
32
|
-
* already catch per item keep behaving exactly as they did.
|
|
33
|
-
*/
|
|
34
|
-
export declare const mapWithConcurrency: <T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>) => Promise<R[]>;
|
|
35
12
|
/**
|
|
36
13
|
* What a call needs from the grant, for the one failure that is not a fault.
|
|
37
14
|
*
|
package/dist/api.js
CHANGED
|
@@ -47,42 +47,6 @@ const THROTTLE_BUDGET_MS = 8_000;
|
|
|
47
47
|
* arrivals scattered across the window.
|
|
48
48
|
*/
|
|
49
49
|
const jittered = (ms) => Math.round(ms / 2 + Math.random() * (ms / 2));
|
|
50
|
-
/**
|
|
51
|
-
* How many provider requests one tool call may have in flight at once.
|
|
52
|
-
*
|
|
53
|
-
* Jitter above spreads the *retries* of a herd; nothing spread the herd's
|
|
54
|
-
* first arrival, and that is the burst the logs actually show — forty
|
|
55
|
-
* `attempt=1 waited=0s` refusals landing in the same millisecond, because a
|
|
56
|
-
* survey of a forty-page notebook opened forty Graph requests simultaneously.
|
|
57
|
-
* Every one of them then had to win its own retry ladder against a provider
|
|
58
|
-
* that was only busy because of them.
|
|
59
|
-
*
|
|
60
|
-
* Four is chosen to be boring rather than optimal: it is slow enough that
|
|
61
|
-
* OneNote answers instead of refusing, and parallel enough that a survey is
|
|
62
|
-
* still meaningfully faster than a loop. The number that matters is "not N".
|
|
63
|
-
*/
|
|
64
|
-
export const FANOUT_LIMIT = 4;
|
|
65
|
-
/**
|
|
66
|
-
* `Promise.all(items.map(fn))` with a ceiling on how many run at once.
|
|
67
|
-
*
|
|
68
|
-
* Results stay in the order of `items`, and a rejection rejects the whole
|
|
69
|
-
* call, so this is a drop-in for the `Promise.all` it replaces: callers that
|
|
70
|
-
* already catch per item keep behaving exactly as they did.
|
|
71
|
-
*/
|
|
72
|
-
export const mapWithConcurrency = async (items, limit, fn) => {
|
|
73
|
-
const results = new Array(items.length);
|
|
74
|
-
let next = 0;
|
|
75
|
-
const worker = async () => {
|
|
76
|
-
for (;;) {
|
|
77
|
-
const index = next++;
|
|
78
|
-
if (index >= items.length)
|
|
79
|
-
return;
|
|
80
|
-
results[index] = await fn(items[index], index);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
84
|
-
return results;
|
|
85
|
-
};
|
|
86
50
|
/**
|
|
87
51
|
* What the provider actually asked for, when it says.
|
|
88
52
|
*
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -30,10 +30,6 @@ import { type ProviderName } from './tokens.js';
|
|
|
30
30
|
* fails on any edit to this table so that the change has to be deliberate.
|
|
31
31
|
*/
|
|
32
32
|
export declare const OPERATIONS: {
|
|
33
|
-
readonly list_notebooks: {
|
|
34
|
-
readonly provider: "microsoft";
|
|
35
|
-
readonly effect: "read";
|
|
36
|
-
};
|
|
37
33
|
readonly list_notes: {
|
|
38
34
|
readonly provider: "microsoft";
|
|
39
35
|
readonly effect: "read";
|
package/dist/dispatch.js
CHANGED
|
@@ -16,7 +16,7 @@ import { gmailLoader, mapAttachment, readAttachment } from './attachments.js';
|
|
|
16
16
|
import { oneNoteLoader } from './page-attachments.js';
|
|
17
17
|
import { createEvent, deleteEvent, listCalendars, listEvents, previewDeleteEvent, previewEvent, previewRescheduleEvent, readEvent, rescheduleEvent, } from './calendar.js';
|
|
18
18
|
import { listEmails, readEmail } from './mail.js';
|
|
19
|
-
import {
|
|
19
|
+
import { listNotes, mapNotes, readNote } from './notes.js';
|
|
20
20
|
import { applyEdit, previewEdit } from './onenote-patch.js';
|
|
21
21
|
import { createPage, previewPage } from './onenote-write.js';
|
|
22
22
|
import { accessTokenFor } from './oauth.js';
|
|
@@ -39,7 +39,6 @@ import { loadTokens } from './tokens.js';
|
|
|
39
39
|
* fails on any edit to this table so that the change has to be deliberate.
|
|
40
40
|
*/
|
|
41
41
|
export const OPERATIONS = {
|
|
42
|
-
list_notebooks: { provider: 'microsoft', effect: 'read' },
|
|
43
42
|
list_notes: { provider: 'microsoft', effect: 'read' },
|
|
44
43
|
map_notes: { provider: 'microsoft', effect: 'read' },
|
|
45
44
|
read_note: { provider: 'microsoft', effect: 'read' },
|
|
@@ -112,8 +111,6 @@ export const connectedProviders = async () => {
|
|
|
112
111
|
export const dispatchWith = (resolve, record = recordWrite) => async (op, params = {}) => {
|
|
113
112
|
const token = await resolve(PROVIDER_FOR[op]);
|
|
114
113
|
switch (op) {
|
|
115
|
-
case 'list_notebooks':
|
|
116
|
-
return (await listNotebooks(token));
|
|
117
114
|
case 'list_notes':
|
|
118
115
|
return (await listNotes(token));
|
|
119
116
|
case 'map_notes':
|
package/dist/mail.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* thing being worked on. Ported from the edge function with behaviour intact.
|
|
6
6
|
*/
|
|
7
7
|
import { GraphError } from './client.js';
|
|
8
|
-
import {
|
|
8
|
+
import { gmailGet } from './api.js';
|
|
9
9
|
import { htmlToText } from './notes.js';
|
|
10
10
|
/** Gmail ids are opaque hex-ish strings; anything else never reaches a URL. */
|
|
11
11
|
export const GMAIL_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
@@ -119,7 +119,7 @@ export const listEmails = async (token, rawQuery) => {
|
|
|
119
119
|
const ids = ((await listRes.json()).messages ?? [])
|
|
120
120
|
.map((m) => m.id)
|
|
121
121
|
.filter((id) => typeof id === 'string' && GMAIL_ID.test(id));
|
|
122
|
-
const emails = await
|
|
122
|
+
const emails = await Promise.all(ids.map(async (id) => {
|
|
123
123
|
const res = await gmailGet(`/users/me/messages/${encodeURIComponent(id)}` +
|
|
124
124
|
'?format=metadata&metadataHeaders=Subject&metadataHeaders=From' +
|
|
125
125
|
'&metadataHeaders=To&metadataHeaders=Date', token);
|
|
@@ -133,7 +133,7 @@ export const listEmails = async (token, rawQuery) => {
|
|
|
133
133
|
date: header(msg.payload?.headers, 'Date'),
|
|
134
134
|
snippet: msg.snippet ?? null,
|
|
135
135
|
};
|
|
136
|
-
});
|
|
136
|
+
}));
|
|
137
137
|
return { emails };
|
|
138
138
|
};
|
|
139
139
|
export const readEmail = async (token, emailId) => {
|
package/dist/notes.d.ts
CHANGED
|
@@ -8,25 +8,6 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { type PageResource } from './page-attachments.js';
|
|
10
10
|
import { type EditablePart } from './onenote-patch.js';
|
|
11
|
-
/**
|
|
12
|
-
* A short value proving a caller has actually seen this account's notebook list.
|
|
13
|
-
*
|
|
14
|
-
* The gate it serves used to be a module-level boolean — "has anything listed
|
|
15
|
-
* the notebooks yet" — which is a fair approximation of one CLI session and
|
|
16
|
-
* nonsense on hosted, where every request may land on a different instance and
|
|
17
|
-
* the flag is shared by every user on the one it lands on. It refused real
|
|
18
|
-
* selections on a cold instance and admitted unseen ones on a warm one.
|
|
19
|
-
*
|
|
20
|
-
* Derived from the notebook names rather than stored, so it needs no session
|
|
21
|
-
* and no shared storage: the same account yields the same key on any instance,
|
|
22
|
-
* a client that never saw the list cannot produce it, and a key from an account
|
|
23
|
-
* with different notebooks does not match. It is not a secret and does not need
|
|
24
|
-
* to be — it proves the list was seen, nothing more.
|
|
25
|
-
*
|
|
26
|
-
* Order and case are normalised so the key does not change when Graph returns
|
|
27
|
-
* the same notebooks in a different order.
|
|
28
|
-
*/
|
|
29
|
-
export declare const notebookKeyFor: (names: readonly string[]) => string;
|
|
30
11
|
export type NoteSummary = {
|
|
31
12
|
id: string;
|
|
32
13
|
title: string;
|
|
@@ -84,31 +65,6 @@ export declare const pageDatesAreCreationDates: (sections: readonly {
|
|
|
84
65
|
* both are marked.
|
|
85
66
|
*/
|
|
86
67
|
export declare const htmlToText: (html: string) => string;
|
|
87
|
-
/**
|
|
88
|
-
* Which notebooks this account has, and nothing else.
|
|
89
|
-
*
|
|
90
|
-
* One Graph request. `listNotes` answers the same question as a side effect of
|
|
91
|
-
* fetching every page of every section — a hundred requests on an organised
|
|
92
|
-
* account — and the notebook question is asked far more often than the page
|
|
93
|
-
* one: every call that has to ask "which notebook?" paid for the pages of all
|
|
94
|
-
* of them to print a list of names.
|
|
95
|
-
*
|
|
96
|
-
* That is what was throttling the account. Graph refused with 20166, "the app
|
|
97
|
-
* has issued too many requests on behalf of this user", against a listing whose
|
|
98
|
-
* answer was already contained in the single request that starts it, since
|
|
99
|
-
* `parentNotebook` expands inline on the sections call.
|
|
100
|
-
*
|
|
101
|
-
* Counted in sections rather than pages, because a page count is exactly the
|
|
102
|
-
* thing that cannot be known without the hundred requests. A number that costs
|
|
103
|
-
* a hundred requests to be slightly more familiar is not worth it, and the
|
|
104
|
-
* count is only there to help the user recognise which notebook is which.
|
|
105
|
-
*/
|
|
106
|
-
export declare const listNotebooks: (token: string) => Promise<{
|
|
107
|
-
notebooks: {
|
|
108
|
-
name: string;
|
|
109
|
-
sections: number;
|
|
110
|
-
}[];
|
|
111
|
-
}>;
|
|
112
68
|
/**
|
|
113
69
|
* `/me/onenote/pages` looks like the obvious call and works right up until the
|
|
114
70
|
* account has too many sections, at which point Graph fails the whole request
|
|
@@ -184,31 +140,6 @@ export type NoteSketch = NoteSummary & {
|
|
|
184
140
|
chars_total: number | null;
|
|
185
141
|
error: string | null;
|
|
186
142
|
};
|
|
187
|
-
/**
|
|
188
|
-
* How long a survey may spend before it returns what it has.
|
|
189
|
-
*
|
|
190
|
-
* The hosted route dies at sixty seconds (`maxDuration` in the MCP route), and
|
|
191
|
-
* dying there is the worst available outcome: the caller waits the full minute
|
|
192
|
-
* and receives nothing, not even the pages that were already sketched. Three
|
|
193
|
-
* of those in a row is what a chat client's retries look like from the logs.
|
|
194
|
-
*
|
|
195
|
-
* Forty-five leaves room for the answer to be assembled and sent after the
|
|
196
|
-
* last sketch lands. The number is a floor on usefulness rather than a tuning
|
|
197
|
-
* knob: whatever the notebook looks like, the survey now returns.
|
|
198
|
-
*/
|
|
199
|
-
export declare const MAP_DEADLINE_MS = 45000;
|
|
200
|
-
type MapNotesOptions = {
|
|
201
|
-
deadlineMs?: number;
|
|
202
|
-
/** Injectable so a test can reach the deadline without spending the time. */
|
|
203
|
-
now?: () => number;
|
|
204
|
-
};
|
|
205
|
-
export type MapNotesResult = {
|
|
206
|
-
sketches: NoteSketch[];
|
|
207
|
-
read_in_full: number;
|
|
208
|
-
/** Pages the deadline cut off. Unsurveyed, and reported as such. */
|
|
209
|
-
not_reached: number;
|
|
210
|
-
elapsed_ms: number;
|
|
211
|
-
};
|
|
212
143
|
/**
|
|
213
144
|
* Sketch every page in a chosen notebook, cheaply, so a notebook can be triaged
|
|
214
145
|
* without reading all of it.
|
|
@@ -225,7 +156,10 @@ export type MapNotesResult = {
|
|
|
225
156
|
*
|
|
226
157
|
* Scoped to one notebook, as intake requires. Nothing maps across scopes.
|
|
227
158
|
*/
|
|
228
|
-
export declare const mapNotes: (token: string, pages: NoteSummary[]
|
|
159
|
+
export declare const mapNotes: (token: string, pages: NoteSummary[]) => Promise<{
|
|
160
|
+
sketches: NoteSketch[];
|
|
161
|
+
read_in_full: number;
|
|
162
|
+
}>;
|
|
229
163
|
/** A page attachment as `read_note` reports it: named, never read. */
|
|
230
164
|
export type PageAttachment = Omit<PageResource, 'position'>;
|
|
231
165
|
export type NoteContent = {
|
|
@@ -273,4 +207,3 @@ export type NoteContent = {
|
|
|
273
207
|
export declare const readNote: (token: string, noteId: unknown, fromPart?: unknown, { withEditIds }?: {
|
|
274
208
|
withEditIds?: boolean;
|
|
275
209
|
}) => Promise<NoteContent>;
|
|
276
|
-
export {};
|
package/dist/notes.js
CHANGED
|
@@ -6,34 +6,11 @@
|
|
|
6
6
|
* enumeration below, which exists for a reason that is not obvious from the
|
|
7
7
|
* code alone.
|
|
8
8
|
*/
|
|
9
|
-
import { createHash } from 'node:crypto';
|
|
10
9
|
import { MAX_TEXT_CHARS } from './attachments.js';
|
|
11
10
|
import { GraphError } from './client.js';
|
|
12
|
-
import {
|
|
11
|
+
import { graphGet } from './api.js';
|
|
13
12
|
import { pageResources } from './page-attachments.js';
|
|
14
13
|
import { editablePartsFrom } from './onenote-patch.js';
|
|
15
|
-
/**
|
|
16
|
-
* A short value proving a caller has actually seen this account's notebook list.
|
|
17
|
-
*
|
|
18
|
-
* The gate it serves used to be a module-level boolean — "has anything listed
|
|
19
|
-
* the notebooks yet" — which is a fair approximation of one CLI session and
|
|
20
|
-
* nonsense on hosted, where every request may land on a different instance and
|
|
21
|
-
* the flag is shared by every user on the one it lands on. It refused real
|
|
22
|
-
* selections on a cold instance and admitted unseen ones on a warm one.
|
|
23
|
-
*
|
|
24
|
-
* Derived from the notebook names rather than stored, so it needs no session
|
|
25
|
-
* and no shared storage: the same account yields the same key on any instance,
|
|
26
|
-
* a client that never saw the list cannot produce it, and a key from an account
|
|
27
|
-
* with different notebooks does not match. It is not a secret and does not need
|
|
28
|
-
* to be — it proves the list was seen, nothing more.
|
|
29
|
-
*
|
|
30
|
-
* Order and case are normalised so the key does not change when Graph returns
|
|
31
|
-
* the same notebooks in a different order.
|
|
32
|
-
*/
|
|
33
|
-
export const notebookKeyFor = (names) => createHash('sha256')
|
|
34
|
-
.update([...names].map((n) => n.trim().toLowerCase()).sort().join('\n'))
|
|
35
|
-
.digest('hex')
|
|
36
|
-
.slice(0, 8);
|
|
37
14
|
/** Graph ids are opaque, but they are concatenated into URLs, so they are checked like any other value. */
|
|
38
15
|
const ONENOTE_ID = /^[A-Za-z0-9!._~-]{1,300}$/;
|
|
39
16
|
/**
|
|
@@ -112,40 +89,6 @@ export const htmlToText = (html) => html
|
|
|
112
89
|
.join('\n')
|
|
113
90
|
.replace(/\n{3,}/g, '\n\n')
|
|
114
91
|
.trim();
|
|
115
|
-
/**
|
|
116
|
-
* Which notebooks this account has, and nothing else.
|
|
117
|
-
*
|
|
118
|
-
* One Graph request. `listNotes` answers the same question as a side effect of
|
|
119
|
-
* fetching every page of every section — a hundred requests on an organised
|
|
120
|
-
* account — and the notebook question is asked far more often than the page
|
|
121
|
-
* one: every call that has to ask "which notebook?" paid for the pages of all
|
|
122
|
-
* of them to print a list of names.
|
|
123
|
-
*
|
|
124
|
-
* That is what was throttling the account. Graph refused with 20166, "the app
|
|
125
|
-
* has issued too many requests on behalf of this user", against a listing whose
|
|
126
|
-
* answer was already contained in the single request that starts it, since
|
|
127
|
-
* `parentNotebook` expands inline on the sections call.
|
|
128
|
-
*
|
|
129
|
-
* Counted in sections rather than pages, because a page count is exactly the
|
|
130
|
-
* thing that cannot be known without the hundred requests. A number that costs
|
|
131
|
-
* a hundred requests to be slightly more familiar is not worth it, and the
|
|
132
|
-
* count is only there to help the user recognise which notebook is which.
|
|
133
|
-
*/
|
|
134
|
-
export const listNotebooks = async (token) => {
|
|
135
|
-
const res = await graphGet('/me/onenote/sections?$select=id,displayName' +
|
|
136
|
-
'&$expand=parentNotebook($select=displayName)&$top=100', token);
|
|
137
|
-
const sections = (await res.json()).value ?? [];
|
|
138
|
-
const counts = new Map();
|
|
139
|
-
for (const section of sections) {
|
|
140
|
-
if (typeof section.id !== 'string' || !ONENOTE_ID.test(section.id))
|
|
141
|
-
continue;
|
|
142
|
-
const name = section.parentNotebook?.displayName ?? '(unnamed notebook)';
|
|
143
|
-
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
notebooks: [...counts].map(([name, sectionCount]) => ({ name, sections: sectionCount })),
|
|
147
|
-
};
|
|
148
|
-
};
|
|
149
92
|
/**
|
|
150
93
|
* `/me/onenote/pages` looks like the obvious call and works right up until the
|
|
151
94
|
* account has too many sections, at which point Graph fails the whole request
|
|
@@ -156,68 +99,46 @@ export const listNotebooks = async (token) => {
|
|
|
156
99
|
* Do not "simplify" this back to the single call.
|
|
157
100
|
*/
|
|
158
101
|
export const listNotes = async (token) => {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const pages = (await res.json()).value ?? [];
|
|
178
|
-
return pages.map((p) => ({
|
|
179
|
-
id: p.id,
|
|
180
|
-
title: p.title ?? '(untitled)',
|
|
181
|
-
section: section.displayName ?? null,
|
|
182
|
-
notebook: section.parentNotebook?.displayName ?? null,
|
|
183
|
-
last_modified: p.lastModifiedDateTime ?? null,
|
|
184
|
-
}));
|
|
185
|
-
});
|
|
186
|
-
const sectionSummaries = usable.map((section, i) => ({
|
|
187
|
-
id: section.id,
|
|
188
|
-
name: section.displayName ?? '(unnamed section)',
|
|
102
|
+
const sectionsRes = await graphGet(
|
|
103
|
+
// lastModifiedDateTime is what the detector reads, and it is the field
|
|
104
|
+
// that actually tracks change — unlike the one on a page. Selected here
|
|
105
|
+
// rather than fetched separately: the request is made either way.
|
|
106
|
+
'/me/onenote/sections?$select=id,displayName,lastModifiedDateTime' +
|
|
107
|
+
'&$expand=parentNotebook($select=displayName)&$top=100', token);
|
|
108
|
+
const sections = (await sectionsRes.json()).value ?? [];
|
|
109
|
+
const usable = sections.filter((s) => typeof s.id === 'string' && ONENOTE_ID.test(s.id));
|
|
110
|
+
const perSection = await Promise.all(usable.map(async (section) => {
|
|
111
|
+
const res = await graphGet(`/me/onenote/sections/${section.id}/pages` +
|
|
112
|
+
// createdDateTime is selected only so the two can be compared. It
|
|
113
|
+
// costs nothing — the request is made either way.
|
|
114
|
+
'?$select=id,title,createdDateTime,lastModifiedDateTime&$top=100', token);
|
|
115
|
+
const pages = (await res.json()).value ?? [];
|
|
116
|
+
return pages.map((p) => ({
|
|
117
|
+
id: p.id,
|
|
118
|
+
title: p.title ?? '(untitled)',
|
|
119
|
+
section: section.displayName ?? null,
|
|
189
120
|
notebook: section.parentNotebook?.displayName ?? null,
|
|
190
|
-
last_modified:
|
|
191
|
-
pages: (perSection[i] ?? []).length,
|
|
121
|
+
last_modified: p.lastModifiedDateTime ?? null,
|
|
192
122
|
}));
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// The survey's own line sat after its await, so a run that threw logged
|
|
213
|
-
// nothing — and a throttled listing throws HERE, before the survey is ever
|
|
214
|
-
// reached, which is why a failed map produced nineteen 429s and no
|
|
215
|
-
// evidence. A measurement that exists only when nothing went wrong
|
|
216
|
-
// measures the wrong runs.
|
|
217
|
-
console.warn(`[artist-mcp] list_notes sections=${sectionCount} ` +
|
|
218
|
-
`elapsed=${Date.now() - startedAt}ms` +
|
|
219
|
-
(failure === null ? '' : ` failed=${failure}`));
|
|
220
|
-
}
|
|
123
|
+
}));
|
|
124
|
+
const sectionSummaries = usable.map((section, i) => ({
|
|
125
|
+
id: section.id,
|
|
126
|
+
name: section.displayName ?? '(unnamed section)',
|
|
127
|
+
notebook: section.parentNotebook?.displayName ?? null,
|
|
128
|
+
last_modified: section.lastModifiedDateTime ?? null,
|
|
129
|
+
pages: (perSection[i] ?? []).length,
|
|
130
|
+
}));
|
|
131
|
+
const creationDates = pageDatesAreCreationDates(sectionSummaries, perSection);
|
|
132
|
+
// Newest first, matching the order the single-call version happened to
|
|
133
|
+
// return; pages with no timestamp sort last rather than jumping to the top.
|
|
134
|
+
const notes = perSection
|
|
135
|
+
.flat()
|
|
136
|
+
.map((n) => ({ ...n, date_is_creation: creationDates }))
|
|
137
|
+
.sort((a, b) => (b.last_modified ?? '').localeCompare(a.last_modified ?? ''));
|
|
138
|
+
// Sections newest first for the same reason, and because when page dates are
|
|
139
|
+
// useless this is the ordering the caller actually reads.
|
|
140
|
+
const sectionList = sectionSummaries.sort((a, b) => (b.last_modified ?? '').localeCompare(a.last_modified ?? ''));
|
|
141
|
+
return { notes, sections: sectionList, page_dates_are_creation_dates: creationDates };
|
|
221
142
|
};
|
|
222
143
|
/**
|
|
223
144
|
* The sections that changed within a window.
|
|
@@ -330,25 +251,6 @@ const previewOf = async (token, id) => {
|
|
|
330
251
|
const { previewText } = (await res.json());
|
|
331
252
|
return (previewText ?? '').trim();
|
|
332
253
|
};
|
|
333
|
-
/**
|
|
334
|
-
* How long a survey may spend before it returns what it has.
|
|
335
|
-
*
|
|
336
|
-
* The hosted route dies at sixty seconds (`maxDuration` in the MCP route), and
|
|
337
|
-
* dying there is the worst available outcome: the caller waits the full minute
|
|
338
|
-
* and receives nothing, not even the pages that were already sketched. Three
|
|
339
|
-
* of those in a row is what a chat client's retries look like from the logs.
|
|
340
|
-
*
|
|
341
|
-
* Forty-five leaves room for the answer to be assembled and sent after the
|
|
342
|
-
* last sketch lands. The number is a floor on usefulness rather than a tuning
|
|
343
|
-
* knob: whatever the notebook looks like, the survey now returns.
|
|
344
|
-
*/
|
|
345
|
-
export const MAP_DEADLINE_MS = 45_000;
|
|
346
|
-
/**
|
|
347
|
-
* A page the deadline was reached before. Not an error and not an empty page —
|
|
348
|
-
* distinguishing it from both is the whole point, because a page that was never
|
|
349
|
-
* looked at must never read as a page with nothing on it.
|
|
350
|
-
*/
|
|
351
|
-
const NOT_REACHED = Symbol('not reached');
|
|
352
254
|
/**
|
|
353
255
|
* Sketch every page in a chosen notebook, cheaply, so a notebook can be triaged
|
|
354
256
|
* without reading all of it.
|
|
@@ -365,102 +267,61 @@ const NOT_REACHED = Symbol('not reached');
|
|
|
365
267
|
*
|
|
366
268
|
* Scoped to one notebook, as intake requires. Nothing maps across scopes.
|
|
367
269
|
*/
|
|
368
|
-
export const mapNotes = async (token, pages
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
preview
|
|
390
|
-
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
// Both routes failed. Reported as a gap, never dropped from the map:
|
|
426
|
-
// a page missing from a survey reads as a page that is not there.
|
|
427
|
-
return {
|
|
428
|
-
...base,
|
|
429
|
-
sketch: null,
|
|
430
|
-
source: 'none',
|
|
431
|
-
fell_back: reason,
|
|
432
|
-
more: false,
|
|
433
|
-
error: err instanceof Error ? err.message : String(err),
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
const sketches = outcomes.filter((o) => o !== NOT_REACHED);
|
|
438
|
-
sketched = sketches.length;
|
|
439
|
-
readInFull = sketches.filter((s) => s.source === 'page').length;
|
|
440
|
-
return {
|
|
441
|
-
sketches,
|
|
442
|
-
read_in_full: readInFull,
|
|
443
|
-
not_reached: notReached,
|
|
444
|
-
elapsed_ms: now() - mapStartedAt,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
catch (err) {
|
|
448
|
-
mapFailure = err instanceof Error ? err.message : String(err);
|
|
449
|
-
throw err;
|
|
450
|
-
}
|
|
451
|
-
finally {
|
|
452
|
-
// The line that says where the time went — on every run, the failed ones
|
|
453
|
-
// included, because those are the runs actually worth measuring.
|
|
454
|
-
//
|
|
455
|
-
// Fallbacks are the suspect: a preview costs one request, a fallback costs
|
|
456
|
-
// two plus the page's whole HTML, and arithmetic says forty previews at
|
|
457
|
-
// four concurrent should take about five seconds rather than sixty. This
|
|
458
|
-
// settles it with evidence instead.
|
|
459
|
-
console.warn(`[artist-mcp] map_notes pages=${pages.length} sketched=${sketched} ` +
|
|
460
|
-
`read_in_full=${readInFull} not_reached=${notReached} ` +
|
|
461
|
-
`elapsed=${now() - mapStartedAt}ms` +
|
|
462
|
-
(mapFailure === null ? '' : ` failed=${mapFailure}`));
|
|
463
|
-
}
|
|
270
|
+
export const mapNotes = async (token, pages) => {
|
|
271
|
+
const sketches = await Promise.all(pages.map(async (page) => {
|
|
272
|
+
const base = { ...page, chars_total: null, error: null };
|
|
273
|
+
let preview = '';
|
|
274
|
+
let reason = null;
|
|
275
|
+
try {
|
|
276
|
+
preview = await previewOf(token, page.id);
|
|
277
|
+
// A preview this thin cannot separate a working unit from a stray
|
|
278
|
+
// note, which is the one thing the map exists to do.
|
|
279
|
+
if (preview === '')
|
|
280
|
+
reason = 'the page has no preview text';
|
|
281
|
+
else if (preview.length < PREVIEW_FLOOR)
|
|
282
|
+
reason = `the preview was only ${preview.length} characters`;
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
reason = `the preview call failed (${err instanceof Error ? err.message : String(err)})`;
|
|
286
|
+
}
|
|
287
|
+
if (reason === null) {
|
|
288
|
+
return {
|
|
289
|
+
...base,
|
|
290
|
+
sketch: preview,
|
|
291
|
+
source: 'preview',
|
|
292
|
+
fell_back: null,
|
|
293
|
+
// Graph gives no page length and does not say it truncated, so a
|
|
294
|
+
// preview at full length is the only sign there is more. Inferred,
|
|
295
|
+
// and reported as an inference rather than as a fact.
|
|
296
|
+
more: preview.length >= PREVIEW_LIKELY_CAP,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
// Case by case, and only for the pages that need it.
|
|
300
|
+
try {
|
|
301
|
+
const { text, chars_total } = await readNote(token, page.id);
|
|
302
|
+
return {
|
|
303
|
+
...base,
|
|
304
|
+
sketch: text.slice(0, DERIVED_SKETCH_CHARS),
|
|
305
|
+
source: 'page',
|
|
306
|
+
fell_back: reason,
|
|
307
|
+
more: chars_total > DERIVED_SKETCH_CHARS,
|
|
308
|
+
chars_total,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
// Both routes failed. Reported as a gap, never dropped from the map:
|
|
313
|
+
// a page missing from a survey reads as a page that is not there.
|
|
314
|
+
return {
|
|
315
|
+
...base,
|
|
316
|
+
sketch: null,
|
|
317
|
+
source: 'none',
|
|
318
|
+
fell_back: reason,
|
|
319
|
+
more: false,
|
|
320
|
+
error: err instanceof Error ? err.message : String(err),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}));
|
|
324
|
+
return { sketches, read_in_full: sketches.filter((s) => s.source === 'page').length };
|
|
464
325
|
};
|
|
465
326
|
/**
|
|
466
327
|
* Read one page, in parts only when it is too long for a single answer.
|
package/dist/onenote-patch.js
CHANGED
|
@@ -236,27 +236,7 @@ const ATTRS_BY_TAG = {
|
|
|
236
236
|
td: new Set(['colspan', 'rowspan', 'align', 'valign', 'width']),
|
|
237
237
|
th: new Set(['colspan', 'rowspan', 'align', 'valign', 'width']),
|
|
238
238
|
a: new Set(['href']),
|
|
239
|
-
// A checkbox is a `data-tag` on the paragraph, so without this a task can be
|
|
240
|
-
// read and never written: `htmlToText` has rendered these as `[ ]` and `[x]`
|
|
241
|
-
// since before anything could write, and a patch carrying one was refused.
|
|
242
|
-
// Per-tag rather than global, because a tag on a table cell or a heading is
|
|
243
|
-
// not a task and OneNote renders it somewhere nobody asked for a checkbox.
|
|
244
|
-
p: new Set(['data-tag']),
|
|
245
|
-
li: new Set(['data-tag']),
|
|
246
239
|
};
|
|
247
|
-
/**
|
|
248
|
-
* The only `data-tag` values this tool will write.
|
|
249
|
-
*
|
|
250
|
-
* OneNote defines dozens — `important`, `question`, `remember-for-later` and
|
|
251
|
-
* the rest — and every one of them is a claim about the line it marks. These
|
|
252
|
-
* two are the only ones with an agreed meaning here: a task, and a task that is
|
|
253
|
-
* done. Anything else is a model deciding that something is important, which is
|
|
254
|
-
* exactly the kind of judgement that must not arrive as page markup.
|
|
255
|
-
*
|
|
256
|
-
* Narrow on purpose, and cheap to widen if a use turns up that is really asked
|
|
257
|
-
* for rather than merely available.
|
|
258
|
-
*/
|
|
259
|
-
const TASK_TAGS = new Set(['to-do', 'to-do:completed']);
|
|
260
240
|
/**
|
|
261
241
|
* What may appear in a `style`, which OneNote's own tables lean on heavily for
|
|
262
242
|
* borders and widths — so forbidding it outright would mean every replaced
|
|
@@ -294,13 +274,6 @@ const checkAttributes = (tag, raw) => {
|
|
|
294
274
|
'OneNote. Nothing was written. Keep the markup to tables, rows, cells, ' +
|
|
295
275
|
'paragraphs, lists and simple emphasis.');
|
|
296
276
|
}
|
|
297
|
-
if (name === 'data-tag' && !TASK_TAGS.has(value)) {
|
|
298
|
-
throw failure(`<${tag}> carries data-tag="${value}", which this tool will not send to ` +
|
|
299
|
-
'OneNote. Nothing was written. Only "to-do" and "to-do:completed" are ' +
|
|
300
|
-
'written: an open task and a done one. Every other OneNote tag states ' +
|
|
301
|
-
'something about the line — that it is important, or a question — and ' +
|
|
302
|
-
'that is the musician\'s to say, not this tool\'s.');
|
|
303
|
-
}
|
|
304
277
|
if (name === 'style' && !SAFE_STYLE.test(value)) {
|
|
305
278
|
throw failure(`The style on <${tag}> contains something this tool will not send: ` +
|
|
306
279
|
`"${value}". Nothing was written. Plain declarations such as ` +
|
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { WRITE_CAPABILITIES, isGranted } from "./grants.js";
|
|
|
6
6
|
import { listAgentWorkflows, loadAgentWorkflow } from "./agents.js";
|
|
7
7
|
import { GraphError } from "./client.js";
|
|
8
8
|
import { call as localCall } from "./dispatch.js";
|
|
9
|
-
import { narrowNotes, narrowSections
|
|
9
|
+
import { narrowNotes, narrowSections } from "./notes.js";
|
|
10
10
|
/** One call per page, so a notebook nobody put a number on does not become hundreds of requests. */
|
|
11
11
|
const DEFAULT_MAP_PAGES = 40;
|
|
12
12
|
/** Times are stated with their zone; the calendar's zone need not be the reader's. */
|
|
@@ -26,6 +26,20 @@ const describeSize = (bytes) => {
|
|
|
26
26
|
return `${Math.round(bytes / 1024)} KB`;
|
|
27
27
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Whether this server has ever handed back the list of notebooks.
|
|
31
|
+
*
|
|
32
|
+
* The one thing the server can actually know about where a notebook name came
|
|
33
|
+
* from. It cannot see who typed it — but a name arriving before the list was
|
|
34
|
+
* ever served cannot have come from the tool, so it came from somewhere outside
|
|
35
|
+
* the conversation. That is the case worth catching: a session inferred a
|
|
36
|
+
* notebook from saved context and answered about the wrong one, correctly and
|
|
37
|
+
* without saying which.
|
|
38
|
+
*
|
|
39
|
+
* Only ever set to true, and only by serving the list. It says nothing after a
|
|
40
|
+
* session has seen the notebooks once, which is the honest limit of it.
|
|
41
|
+
*/
|
|
42
|
+
let notebooksHaveBeenListed = false;
|
|
29
43
|
/**
|
|
30
44
|
* Settle which notebook is being worked in, before anything reads a page.
|
|
31
45
|
*
|
|
@@ -34,63 +48,46 @@ const describeSize = (bytes) => {
|
|
|
34
48
|
* intake depends on, and two copies of it would eventually disagree. The caller
|
|
35
49
|
* names itself so the instruction says which tool to call again.
|
|
36
50
|
*/
|
|
37
|
-
const selectNotebook = async (call, notebook,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// `list_notes` answers it as a side effect of fetching every page of every
|
|
41
|
-
// section — a hundred Graph requests on an organised account — and most calls
|
|
42
|
-
// that reach here only need the names: to ask which notebook, or to check
|
|
43
|
-
// that a supplied one exists. Paying for every page of every notebook to
|
|
44
|
-
// print a list of names is what had Graph refusing with 20166.
|
|
45
|
-
const { notebooks } = await call("list_notebooks");
|
|
46
|
-
if (notebooks.length === 0)
|
|
51
|
+
const selectNotebook = async (call, notebook, tool) => {
|
|
52
|
+
const { notes, sections = [], page_dates_are_creation_dates = false, } = await call("list_notes");
|
|
53
|
+
if (notes.length === 0)
|
|
47
54
|
return { message: "No notes found." };
|
|
48
|
-
const names =
|
|
55
|
+
const names = [...new Set(notes.map((n) => n.notebook ?? "(unnamed notebook)"))];
|
|
49
56
|
// Handing back every page across every notebook invites work on the wrong
|
|
50
57
|
// one. With a choice to be made and nothing chosen, the pages are withheld
|
|
51
58
|
// until the user has actually made it.
|
|
52
59
|
// A name that arrives before this session has ever seen the list did not come
|
|
53
60
|
// from the tool, so it is either the user's or a guess — and the two are
|
|
54
61
|
// indistinguishable from here. Ask, the same way an omitted name asks.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if ((!notebook || unproven) && names.length > 1) {
|
|
63
|
-
// Sections rather than pages: a page count is precisely the thing that
|
|
64
|
-
// cannot be known without the hundred requests this path exists to avoid,
|
|
65
|
-
// and the number is only here to help the user tell one notebook from
|
|
66
|
-
// another.
|
|
67
|
-
const counts = notebooks.map(({ name, sections: n }) => `- ${name} — ${n} section${n === 1 ? "" : "s"}`);
|
|
62
|
+
const unseenName = notebook !== undefined && !notebooksHaveBeenListed;
|
|
63
|
+
if ((!notebook || unseenName) && names.length > 1) {
|
|
64
|
+
notebooksHaveBeenListed = true;
|
|
65
|
+
const counts = names.map((name) => {
|
|
66
|
+
const total = notes.filter((n) => (n.notebook ?? "(unnamed notebook)") === name).length;
|
|
67
|
+
return `- ${name} — ${total} page${total === 1 ? "" : "s"}`;
|
|
68
|
+
});
|
|
68
69
|
return {
|
|
69
70
|
message: `This account has ${names.length} notebooks:\n${counts.join("\n")}\n\n` +
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
"guess here produces an answer that is correct about the wrong pages."
|
|
71
|
+
(unseenName
|
|
72
|
+
? `You asked for "${notebook}", but nothing in this conversation has ` +
|
|
73
|
+
"named a notebook yet. Ask the user which one they mean — including " +
|
|
74
|
+
"whether it is that one — and call " +
|
|
75
|
+
`${tool} again once they have said. A notebook you know of from ` +
|
|
76
|
+
"elsewhere is a guess, and a guess here produces an answer that is " +
|
|
77
|
+
"correct about the wrong pages."
|
|
78
78
|
: `Ask the user which notebook to work in, then call ${tool} again ` +
|
|
79
|
-
"with that name
|
|
80
|
-
"
|
|
79
|
+
"with that name. Do not guess, and do not work across notebooks " +
|
|
80
|
+
"unless the user asks for it."),
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
notebooksHaveBeenListed = true;
|
|
83
84
|
const wanted = notebook?.trim().toLowerCase();
|
|
84
|
-
// Checked before the expensive call, not after it: a misspelled notebook
|
|
85
|
-
// should cost one request to refuse, not a hundred.
|
|
86
|
-
if (wanted !== undefined && !names.some((name) => name.trim().toLowerCase() === wanted)) {
|
|
87
|
-
return { message: `No notebook named "${notebook}". Available: ${names.join(", ")}.` };
|
|
88
|
-
}
|
|
89
|
-
// Settled. Only now are the pages worth what they cost.
|
|
90
|
-
const { notes, sections = [], page_dates_are_creation_dates = false, } = await call("list_notes");
|
|
91
85
|
const pages = wanted
|
|
92
86
|
? notes.filter((n) => (n.notebook ?? "").trim().toLowerCase() === wanted)
|
|
93
87
|
: notes;
|
|
88
|
+
if (wanted && pages.length === 0) {
|
|
89
|
+
return { message: `No notebook named "${notebook}". Available: ${names.join(", ")}.` };
|
|
90
|
+
}
|
|
94
91
|
// A name that was supplied walks straight past the question above, so a
|
|
95
92
|
// guessed one is indistinguishable from a chosen one. Found in use: asked
|
|
96
93
|
// about "this notebook" in a fresh chat, a session inferred one from saved
|
|
@@ -176,7 +173,7 @@ const renderChangedSections = (chosen, notebook, since) => {
|
|
|
176
173
|
* the only way OneNote allows a cell to change.
|
|
177
174
|
*/
|
|
178
175
|
const INDEX_ENTRIES = 12;
|
|
179
|
-
const serverVersion = '2.3.
|
|
176
|
+
const serverVersion = '2.3.1-staging.112'; // x-release-please-version
|
|
180
177
|
const errorResult = (err) => {
|
|
181
178
|
const message = err instanceof GraphError ? err.message : `Unexpected error: ${err}`;
|
|
182
179
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -718,14 +715,6 @@ const createServer = async (call, grants = []) => {
|
|
|
718
715
|
"notebook you happen to know the user has: a plausible guess here " +
|
|
719
716
|
"is indistinguishable from their choice and produces a confident " +
|
|
720
717
|
"answer about the wrong notebook."),
|
|
721
|
-
notebook_key: z
|
|
722
|
-
.string()
|
|
723
|
-
.optional()
|
|
724
|
-
.describe("The notebook_key printed with the notebook list, passed back exactly " +
|
|
725
|
-
"as it was given. Required alongside `notebook` on an account with " +
|
|
726
|
-
"more than one notebook: it is how this tool knows the name came " +
|
|
727
|
-
"from the list rather than from somewhere outside the conversation. " +
|
|
728
|
-
"Never invent one, and never reuse one from an earlier session."),
|
|
729
718
|
since: z
|
|
730
719
|
.string()
|
|
731
720
|
.optional()
|
|
@@ -745,9 +734,9 @@ const createServer = async (call, grants = []) => {
|
|
|
745
734
|
.describe("Cap the number of pages returned, newest first. The reply says how " +
|
|
746
735
|
"many matched, so a capped list is never mistaken for the whole " +
|
|
747
736
|
"notebook."),
|
|
748
|
-
}, async ({ notebook,
|
|
737
|
+
}, async ({ notebook, since, limit }) => {
|
|
749
738
|
try {
|
|
750
|
-
const chosen = await selectNotebook(call, notebook,
|
|
739
|
+
const chosen = await selectNotebook(call, notebook, "list_notes");
|
|
751
740
|
if ("message" in chosen) {
|
|
752
741
|
return { content: [{ type: "text", text: chosen.message }] };
|
|
753
742
|
}
|
|
@@ -843,14 +832,6 @@ const createServer = async (call, grants = []) => {
|
|
|
843
832
|
"this conversation, or named by the user in it. Omit when they have " +
|
|
844
833
|
"not chosen one. Never fill it in from saved context or an earlier " +
|
|
845
834
|
"session."),
|
|
846
|
-
notebook_key: z
|
|
847
|
-
.string()
|
|
848
|
-
.optional()
|
|
849
|
-
.describe("The notebook_key printed with the notebook list, passed back exactly " +
|
|
850
|
-
"as it was given. Required alongside `notebook` on an account with " +
|
|
851
|
-
"more than one notebook: it is how this tool knows the name came " +
|
|
852
|
-
"from the list rather than from somewhere outside the conversation. " +
|
|
853
|
-
"Never invent one, and never reuse one from an earlier session."),
|
|
854
835
|
since: z
|
|
855
836
|
.string()
|
|
856
837
|
.optional()
|
|
@@ -863,9 +844,9 @@ const createServer = async (call, grants = []) => {
|
|
|
863
844
|
.min(1)
|
|
864
845
|
.optional()
|
|
865
846
|
.describe(`Cap how many pages are sketched, newest first. Defaults to ${DEFAULT_MAP_PAGES}.`),
|
|
866
|
-
}, async ({ notebook,
|
|
847
|
+
}, async ({ notebook, since, limit }) => {
|
|
867
848
|
try {
|
|
868
|
-
const chosen = await selectNotebook(call, notebook,
|
|
849
|
+
const chosen = await selectNotebook(call, notebook, "map_notes");
|
|
869
850
|
if ("message" in chosen) {
|
|
870
851
|
return { content: [{ type: "text", text: chosen.message }] };
|
|
871
852
|
}
|
|
@@ -917,7 +898,7 @@ const createServer = async (call, grants = []) => {
|
|
|
917
898
|
],
|
|
918
899
|
};
|
|
919
900
|
}
|
|
920
|
-
const { sketches, read_in_full
|
|
901
|
+
const { sketches, read_in_full } = await call("map_notes", { pages });
|
|
921
902
|
const blocks = sketches.map((s) => {
|
|
922
903
|
const location = [s.notebook, s.section].filter(Boolean).join(" / ");
|
|
923
904
|
const head = `## ${s.title}${location ? ` (${location})` : ""} — dated ${s.last_modified ?? "unknown"}` +
|
|
@@ -949,16 +930,6 @@ const createServer = async (call, grants = []) => {
|
|
|
949
930
|
"usable preview and were read in full instead, so those sketches " +
|
|
950
931
|
"cover more of the page than the rest.");
|
|
951
932
|
}
|
|
952
|
-
if (not_reached > 0) {
|
|
953
|
-
// Said before the other truncations, because it is the one the
|
|
954
|
-
// caller did not ask for: `limit` is their own cap and this is the
|
|
955
|
-
// clock running out. A partial survey that does not say so is a
|
|
956
|
-
// survey the caller will read as complete.
|
|
957
|
-
caveats.push(`Stopped after ${sketches.length} of ${sketches.length + not_reached} pages: the ` +
|
|
958
|
-
"survey ran out of time before the rest were reached. Those pages are " +
|
|
959
|
-
"UNSURVEYED, not empty — call map_notes again with a smaller `limit` to " +
|
|
960
|
-
"cover them, or read_note the ones you already know you need.");
|
|
961
|
-
}
|
|
962
933
|
if (pages.length < matched) {
|
|
963
934
|
caveats.push(`Sketched the ${pages.length} newest by date of ${matched} pages. ` +
|
|
964
935
|
"Raise `limit` or narrow with `since` for the rest.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manudota/artist-mcp",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1-staging.112",
|
|
4
4
|
"description": "MCP server that reads your OneNote notes in Claude Desktop or Codex, with optional Gmail and Calendar as supporting evidence. Signs in on your own machine; writes are opt-in per install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|