@rockhopper-co/mcp-server 2.1.82-staging.65cfbf4 → 2.1.84-staging.bac34fe

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 (92) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/account-link-providers.d.ts +42 -0
  3. package/dist/account-link-providers.d.ts.map +1 -0
  4. package/dist/account-link-providers.js +47 -0
  5. package/dist/account-link-providers.js.map +1 -0
  6. package/dist/api-client.d.ts +16 -1
  7. package/dist/api-client.d.ts.map +1 -1
  8. package/dist/api-client.js +26 -1
  9. package/dist/api-client.js.map +1 -1
  10. package/dist/cell-history-unavailable.d.ts +7 -0
  11. package/dist/cell-history-unavailable.d.ts.map +1 -1
  12. package/dist/cell-history-unavailable.js +28 -0
  13. package/dist/cell-history-unavailable.js.map +1 -1
  14. package/dist/comment-location.d.ts +18 -0
  15. package/dist/comment-location.d.ts.map +1 -0
  16. package/dist/comment-location.js +29 -0
  17. package/dist/comment-location.js.map +1 -0
  18. package/dist/document-changes.d.ts +11 -1
  19. package/dist/document-changes.d.ts.map +1 -1
  20. package/dist/document-changes.js +1 -1
  21. package/dist/document-changes.js.map +1 -1
  22. package/dist/drive-search.d.ts +4 -3
  23. package/dist/drive-search.d.ts.map +1 -1
  24. package/dist/drive-search.js +7 -7
  25. package/dist/drive-search.js.map +1 -1
  26. package/dist/file-changes.d.ts +52 -0
  27. package/dist/file-changes.d.ts.map +1 -0
  28. package/dist/file-changes.js +82 -0
  29. package/dist/file-changes.js.map +1 -0
  30. package/dist/prompts/change-sections.d.ts +11 -0
  31. package/dist/prompts/change-sections.d.ts.map +1 -0
  32. package/dist/prompts/change-sections.js +47 -0
  33. package/dist/prompts/change-sections.js.map +1 -0
  34. package/dist/prompts/index.d.ts.map +1 -1
  35. package/dist/prompts/index.js +18 -34
  36. package/dist/prompts/index.js.map +1 -1
  37. package/dist/resources/changes.d.ts.map +1 -1
  38. package/dist/resources/changes.js +19 -10
  39. package/dist/resources/changes.js.map +1 -1
  40. package/dist/resources/orchestration-guide.md +1 -1
  41. package/dist/tools/{connect-microsoft.d.ts → account-link.d.ts} +9 -3
  42. package/dist/tools/account-link.d.ts.map +1 -0
  43. package/dist/tools/account-link.js +155 -0
  44. package/dist/tools/account-link.js.map +1 -0
  45. package/dist/tools/anchor-history-read.d.ts +25 -0
  46. package/dist/tools/anchor-history-read.d.ts.map +1 -0
  47. package/dist/tools/anchor-history-read.js +73 -0
  48. package/dist/tools/anchor-history-read.js.map +1 -0
  49. package/dist/tools/drive-search.contract.d.ts +7 -2
  50. package/dist/tools/drive-search.contract.d.ts.map +1 -1
  51. package/dist/tools/drive-search.contract.js +15 -7
  52. package/dist/tools/drive-search.contract.js.map +1 -1
  53. package/dist/tools/drive-search.d.ts.map +1 -1
  54. package/dist/tools/drive-search.js +16 -17
  55. package/dist/tools/drive-search.js.map +1 -1
  56. package/dist/tools/get-cell-history.d.ts +15 -0
  57. package/dist/tools/get-cell-history.d.ts.map +1 -1
  58. package/dist/tools/get-cell-history.js +67 -18
  59. package/dist/tools/get-cell-history.js.map +1 -1
  60. package/dist/tools/get-comments.d.ts.map +1 -1
  61. package/dist/tools/get-comments.js +2 -1
  62. package/dist/tools/get-comments.js.map +1 -1
  63. package/dist/tools/index.d.ts.map +1 -1
  64. package/dist/tools/index.js +5 -4
  65. package/dist/tools/index.js.map +1 -1
  66. package/dist/tools/list-unenrolled-files.d.ts.map +1 -1
  67. package/dist/tools/list-unenrolled-files.js +18 -178
  68. package/dist/tools/list-unenrolled-files.js.map +1 -1
  69. package/dist/tools/list-unenrolled-render.d.ts +4 -0
  70. package/dist/tools/list-unenrolled-render.d.ts.map +1 -0
  71. package/dist/tools/list-unenrolled-render.js +219 -0
  72. package/dist/tools/list-unenrolled-render.js.map +1 -0
  73. package/dist/tools/search.d.ts.map +1 -1
  74. package/dist/tools/search.js +27 -57
  75. package/dist/tools/search.js.map +1 -1
  76. package/dist/tools/write-comments.d.ts.map +1 -1
  77. package/dist/tools/write-comments.js +58 -2
  78. package/dist/tools/write-comments.js.map +1 -1
  79. package/dist/types.d.ts +79 -1
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/zod-schemas.d.ts +57 -0
  82. package/dist/zod-schemas.d.ts.map +1 -1
  83. package/dist/zod-schemas.js +24 -1
  84. package/dist/zod-schemas.js.map +1 -1
  85. package/package.json +1 -1
  86. package/dist/tools/connect-microsoft.d.ts.map +0 -1
  87. package/dist/tools/connect-microsoft.js +0 -149
  88. package/dist/tools/connect-microsoft.js.map +0 -1
  89. package/dist/tools/document-changes-read.d.ts +0 -28
  90. package/dist/tools/document-changes-read.d.ts.map +0 -1
  91. package/dist/tools/document-changes-read.js +0 -34
  92. package/dist/tools/document-changes-read.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { GRAPH_LINK_FAILURE_TEXT, graphLinkFailureFrom, } from '../graph-link-failure.js';
2
+ import { renderEmpty, renderFound } from './list-unenrolled-render.js';
3
3
  /**
4
4
  * ENG-2785 — "which of my workbooks are NOT in Rockhopper yet?"
5
5
  *
@@ -37,27 +37,23 @@ import { GRAPH_LINK_FAILURE_TEXT, graphLinkFailureFrom, } from '../graph-link-fa
37
37
  */
38
38
  export function registerListUnenrolledFilesTool(server, api) {
39
39
  server.registerTool('list_unenrolled_files', {
40
- title: 'List Workbooks Not Yet in Rockhopper',
41
- description: 'List the spreadsheets Rockhopper has seen for this user that are ' +
42
- 'NOT enrolled — the answer to "what could I add?". Use this for ' +
43
- 'browsing, when the user cannot name a specific file; use ' +
44
- '`search_drive_files` when they can. Read-only: it enrolls nothing ' +
45
- 'and asks nothing. The answer comes from stored records rather than a ' +
46
- 'live Microsoft read, so it is dated — pass a listed file\'s `msId` ' +
47
- 'and `driveMsId` to `enroll_file` to add it. ' +
48
- // ENG-4283. Stated up front, in `enroll_file`'s words, because the
49
- // model has to know the scope BEFORE it picks the tool: offered as the
50
- // answer to a Google Workspace customer's "what could I add?", this
51
- // returns an empty list whose emptiness means nothing about their
52
- // drive, and the tool cannot un-ask the question afterwards.
53
- 'MICROSOFT ONLY — it lists OneDrive and SharePoint workbooks and ' +
54
- 'covers no other storage. An account with no Microsoft link gets ' +
55
- 'nothing from it, which is not a statement about what that user has. ' +
56
- // ENG-4958: the remedy, not just the limit. `search_drive_files` now
57
- // reaches Google Drive, so a model that lands here for a Google file
58
- // has somewhere to go instead of reporting an empty drive.
59
- 'For a Google file, call `search_drive_files` with ' +
60
- 'provider="google" instead. ' +
40
+ title: 'List Files Not Yet in Rockhopper',
41
+ description: 'List the files Rockhopper has seen for this user that are NOT ' +
42
+ 'enrolled — the answer to "what could I add?". Use this for browsing, ' +
43
+ 'when the user cannot name a specific file; use `search_drive_files` ' +
44
+ 'when they can. Read-only: it enrolls nothing and asks nothing. The ' +
45
+ 'answer comes from stored records rather than a live read of the ' +
46
+ 'user\'s drive, so it is dated. ' +
47
+ // ENG-6410 — scope stated up front, because the model has to know it
48
+ // BEFORE it picks the tool. ENG-4283 made this 'MICROSOFT ONLY' and
49
+ // ENG-4958 sent Google callers to `search_drive_files`; ENG-6409 gave
50
+ // the inventory a Google lane, so both are now false.
51
+ 'It covers OneDrive and SharePoint (Microsoft) and Google Drive, for ' +
52
+ 'whichever of those the user has connected, and every row names its ' +
53
+ 'provider. To add a Microsoft row, pass its `msId` and `driveMsId` to ' +
54
+ '`enroll_file`; to add a Google row, pass its link to `enroll_file` as ' +
55
+ '`url`. An account with neither connected gets nothing from it, which ' +
56
+ 'is not a statement about what that user has. ' +
61
57
  // ENG-4271: an organisation's automatic-enrollment scope governs only
62
58
  // which files its own background sweep enrolls on its own; it never
63
59
  // stops `enroll_file` from adding any file listed here by hand.
@@ -130,160 +126,4 @@ export function registerListUnenrolledFilesTool(server, api) {
130
126
  }
131
127
  });
132
128
  }
133
- /**
134
- * ENG-2814 — the "there is more" line, and it is never optional when a cursor
135
- * came back.
136
- *
137
- * Written to the MODEL, not the user: it names the exact next call, because a
138
- * hint that only says "more available" gets summarised away and the walk stops
139
- * one page in.
140
- */
141
- function moreToCome(nextCursor) {
142
- if (!nextCursor)
143
- return '';
144
- return (`\n\nMORE FILES REMAIN — this is not the whole list. Call ` +
145
- `\`list_unenrolled_files\` again with \`cursor="${nextCursor}"\`, and ` +
146
- 'keep going until a response comes back with no cursor. Do not tell the ' +
147
- 'user what they do or do not have until then. The cursor stops working 30 ' +
148
- 'minutes after the first page; if it is refused as expired, start again ' +
149
- 'from the first page rather than retrying it.');
150
- }
151
- function renderFound(items, freshness, nextCursor = null) {
152
- const lines = items.map((item) => {
153
- const where = item.parentPath ? ` in ${item.parentPath}` : '';
154
- const modified = item.lastModifiedAt
155
- ? `, modified ${item.lastModifiedAt}`
156
- : '';
157
- // `hidden` means the user REMOVED this file from Rockhopper before. Its
158
- // history is still held, so enrolling restores rather than duplicates —
159
- // a different next step from a file that was never added, and the only
160
- // thing distinguishing the two.
161
- const previously = item.enrollmentState === 'hidden' ? ' [previously removed]' : '';
162
- return (`- **${item.name}**${where}${modified}${previously}\n` +
163
- ` msId: ${item.msId}, driveMsId: ${item.driveMsId}`);
164
- });
165
- // "on this page", never a bare count: with a cursor outstanding the number is
166
- // a page size, and calling it a total is the same lie as the old 200-row
167
- // ceiling that reported nothing about itself.
168
- const counted = nextCursor
169
- ? `${items.length} workbook(s) not yet in Rockhopper on this page:`
170
- : `${items.length} workbook(s) not yet in Rockhopper:`;
171
- return (`${counted}\n\n` +
172
- `${lines.join('\n')}\n\n${dateline(freshness)}${moreToCome(nextCursor)}`);
173
- }
174
- /**
175
- * ENG-4283 — the backend's code for "no Microsoft tenant on this account".
176
- *
177
- * Duplicated from `DriveInventoryFreshnessDto` rather than imported, for the
178
- * same reason `graph-link-failure.ts` duplicates its four: this package ships
179
- * over npm and takes no build dependency on the API tree. EXACT match, and no
180
- * case folding — a value this package has not learned about is not "probably
181
- * this one", and guessing at a spelling is precisely what left ENG-4311's
182
- * comparison dead for a release.
183
- */
184
- const NO_MICROSOFT_TENANT = 'NO_MICROSOFT_TENANT';
185
- /**
186
- * What to tell the assistant when the lane does not serve this account.
187
- *
188
- * Three rules, each answering a specific way the old message did harm:
189
- * - **No retry language.** Nothing about this changes on a later call, and a
190
- * model complies with "try again shortly" indefinitely.
191
- * - **No `connect_microsoft`.** There is no tenant behind it for this user, so
192
- * the link would send them to Microsoft to be refused (ENG-2614).
193
- * - **Names the scope and the alternative**, so the answer is usable rather
194
- * than only correct — a Google customer's files are enrolled a different way
195
- * and the assistant should say so instead of stopping at a refusal.
196
- */
197
- const NO_MICROSOFT_TENANT_TEXT = 'Rockhopper\'s drive inventory covers OneDrive and SharePoint, and this ' +
198
- 'account has no Microsoft link — so there is nothing for it to list and ' +
199
- 'nothing to retry. This is NOT evidence that every workbook is already in ' +
200
- 'Rockhopper, and it does not mean the user has no files: it means this ' +
201
- 'particular list cannot see them. Do not call `list_unenrolled_files` again ' +
202
- 'for this user, and do not report their drive as covered. If they use ' +
203
- 'Google Workspace, their spreadsheets are added through Rockhopper directly ' +
204
- 'rather than from this list.';
205
- function renderEmpty(freshness, nextCursor = null) {
206
- // ORDER IS LOAD-BEARING. A broken link and a first refresh that has not
207
- // finished both mean there is nothing to page THROUGH, so sending the model
208
- // after another page instead of `connect_microsoft` starts a loop that cannot
209
- // end. Those two branches come first and keep their own instruction.
210
- //
211
- // ENG-4311 — this used to compare `lastFailureReason === 'no_delegated_token'`
212
- // against a producer that writes the SCREAMING_SNAKE `GraphLinkFailure` enum,
213
- // so it never matched and this branch was dead: a user with no working link
214
- // fell through to the never-refreshed message below and was told a scan had
215
- // been started and to try again shortly. Neither was true. The four codes are
216
- // kept APART rather than collapsed, because three of them name a different
217
- // person who has to act — see `graph-link-failure.ts`.
218
- // ENG-4283 — FIRST, ahead of every other branch including the link failures.
219
- //
220
- // The four states below all assume this lane COVERS the caller and something
221
- // went wrong on the way to filling it. An account with no Microsoft tenant is
222
- // a fifth thing: the refresh returns before it writes a sync-state row, so
223
- // `asOf` stays null and `refreshing` stays false PERMANENTLY, and the
224
- // never-refreshed branch below fires on every call — asserting a scan "has
225
- // been started" when none was, and instructing a retry that cannot terminate.
226
- //
227
- // It outranks the link failures rather than following them because a link
228
- // failure names something a user or an administrator can fix, and this names
229
- // something neither can: sending a Google-only customer to
230
- // `connect_microsoft` is ENG-2614's loop with an extra hop.
231
- if (freshness.inapplicableReason === NO_MICROSOFT_TENANT) {
232
- return NO_MICROSOFT_TENANT_TEXT;
233
- }
234
- const linkFailure = graphLinkFailureFrom(freshness.lastFailureReason);
235
- if (linkFailure)
236
- return GRAPH_LINK_FAILURE_TEXT[linkFailure];
237
- if (!freshness.asOf) {
238
- return ('No answer yet — the first scan of this user\'s drive has not finished. ' +
239
- `${freshness.refreshing ? 'One is running now.' : 'One has been started.'} ` +
240
- 'Try again shortly. This is NOT evidence that every workbook is already ' +
241
- `in Rockhopper.${failureNote(freshness)}`);
242
- }
243
- /**
244
- * ENG-2814 — AN EMPTY PAGE WITH A CURSOR IS THE MIDDLE OF A WALK.
245
- *
246
- * The backend filters enrolled rows out AFTER cutting a chunk and stops at a
247
- * per-request scan budget, so "this page had none" is routine and says
248
- * nothing about the drive. Reporting it as "everything is already covered" is
249
- * the exact failure this whole tool was written against — an empty list that
250
- * reads as reassurance — and pagination gives it a brand new way to happen.
251
- */
252
- if (nextCursor) {
253
- return ('No un-enrolled workbooks on this page — but the search is NOT ' +
254
- 'finished, and this says nothing yet about what the user has.' +
255
- `${moreToCome(nextCursor)}`);
256
- }
257
- if (freshness.consecutiveFailures > 0) {
258
- return ('No un-enrolled workbooks in the stored list, but the list is not ' +
259
- `trustworthy right now.${failureNote(freshness)}\n\n${dateline(freshness)}`);
260
- }
261
- return ('Every workbook Rockhopper has seen for this user is already in ' +
262
- `Rockhopper.\n\n${dateline(freshness)}`);
263
- }
264
- /**
265
- * How old the answer is, on every branch that has an answer.
266
- *
267
- * Never omitted: the rows are stored, so a surface that does not date them
268
- * invites a user to conclude a file is absent from their drive when it is only
269
- * absent from our last refresh.
270
- */
271
- function dateline(freshness) {
272
- const asOf = freshness.asOf
273
- ? `As of ${freshness.asOf}`
274
- : 'Never successfully refreshed';
275
- const stale = freshness.stale ? ' (stale)' : '';
276
- const refreshing = freshness.refreshing
277
- ? ' A refresh is running; call again for a newer answer.'
278
- : '';
279
- return (`${asOf}${stale} — from Rockhopper's stored records, not a live ` +
280
- `Microsoft read.${refreshing}${failureNote(freshness)}`);
281
- }
282
- function failureNote(freshness) {
283
- if (!freshness.consecutiveFailures || !freshness.lastFailureAt)
284
- return '';
285
- return (` Refresh has failed ${freshness.consecutiveFailures} time(s) in a row, ` +
286
- `last at ${freshness.lastFailureAt}` +
287
- `${freshness.lastFailureReason ? ` (${freshness.lastFailureReason})` : ''}.`);
288
- }
289
129
  //# sourceMappingURL=list-unenrolled-files.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-unenrolled-files.js","sourceRoot":"","sources":["../../src/tools/list-unenrolled-files.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"list-unenrolled-files.js","sourceRoot":"","sources":["../../src/tools/list-unenrolled-files.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,+BAA+B,CAC7C,MAAiB,EACjB,GAAc;IAEd,MAAM,CAAC,YAAY,CACjB,uBAAuB,EACvB;QACE,KAAK,EAAE,kCAAkC;QACzC,WAAW,EACT,gEAAgE;YAChE,uEAAuE;YACvE,sEAAsE;YACtE,qEAAqE;YACrE,kEAAkE;YAClE,iCAAiC;YACjC,qEAAqE;YACrE,oEAAoE;YACpE,sEAAsE;YACtE,sDAAsD;YACtD,sEAAsE;YACtE,qEAAqE;YACrE,uEAAuE;YACvE,wEAAwE;YACxE,uEAAuE;YACvE,+CAA+C;YAC/C,sEAAsE;YACtE,oEAAoE;YACpE,gEAAgE;YAChE,kEAAkE;YAClE,sEAAsE;YACtE,gEAAgE;YAChE,uEAAuE;YACvE,0DAA0D;YAC1D,qEAAqE;YACrE,oEAAoE;YACpE,kEAAkE;YAClE,oEAAoE;YACpE,qEAAqE;YACrE,gEAAgE;YAChE,uEAAuE;QACzE,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;YACpB,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,GAAG,CAAC,CAAC,CAAC;iBACN,GAAG,CAAC,GAAG,CAAC;iBACR,QAAQ,EAAE;iBACV,QAAQ,CACP,4DAA4D;gBAC1D,gEAAgE;gBAChE,kCAAkC,CACrC;YACH,MAAM,EAAE,CAAC;iBACN,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CACP,6DAA6D;gBAC3D,4DAA4D;gBAC5D,iEAAiE;gBACjE,4DAA4D,CAC/D;SACJ,CAAC;QACF,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,aAAa,EAAE,KAAK;SACrB;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAC1B,IAAI,CAAC;YACH,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,MAAM,GAAG,CAAC,kBAAkB,CAAC;gBACpE,UAAU,EAAE,cAAc;gBAC1B,KAAK;gBACL,MAAM;aACP,CAAC,CAAC;YAEH,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,KAAK,CAAC,MAAM;4BAChB,CAAC,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,UAAU,CAAC;4BAC3C,CAAC,CAAC,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC;qBACvC;iBACF;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,uEAAuE;YACvE,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EACF,oDAAoD;4BACpD,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI;4BAC7D,6DAA6D;4BAC7D,oCAAoC;qBACvC;iBACF;gBACD,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { DriveInventoryFreshness, DriveInventoryItem } from '../types.js';
2
+ export declare function renderFound(items: readonly DriveInventoryItem[], freshness: DriveInventoryFreshness, nextCursor?: string | null): string;
3
+ export declare function renderEmpty(freshness: DriveInventoryFreshness, nextCursor?: string | null): string;
4
+ //# sourceMappingURL=list-unenrolled-render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-unenrolled-render.d.ts","sourceRoot":"","sources":["../../src/tools/list-unenrolled-render.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,uBAAuB,EACvB,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AA4GrB,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,SAAS,EAAE,uBAAuB,EAClC,UAAU,GAAE,MAAM,GAAG,IAAW,GAC/B,MAAM,CA0BR;AAmDD,wBAAgB,WAAW,CACzB,SAAS,EAAE,uBAAuB,EAClC,UAAU,GAAE,MAAM,GAAG,IAAW,GAC/B,MAAM,CA2CR"}
@@ -0,0 +1,219 @@
1
+ import { GRAPH_LINK_FAILURE_TEXT, graphLinkFailureFrom, } from '../graph-link-failure.js';
2
+ /**
3
+ * ENG-6410 — what `list_unenrolled_files` prints, derived from the backend's
4
+ * per-provider freshness rather than from a hardcoded provider.
5
+ *
6
+ * Moved out of `list-unenrolled-files.ts` so that file holds the tool's
7
+ * contract and this one holds its words. The Microsoft-only output is
8
+ * byte-identical to what shipped before the Google lane existed; that is
9
+ * pinned by `tools.list-unenrolled.google.test.ts`.
10
+ */
11
+ /**
12
+ * ENG-4283 / ENG-6409 — the backend's codes for "this lane does not serve this
13
+ * account". Duplicated rather than imported: this package ships over npm and
14
+ * takes no build dependency on the API tree. EXACT match — an unknown value
15
+ * falls through rather than being guessed at (ENG-4311).
16
+ */
17
+ const INAPPLICABLE = {
18
+ NO_MICROSOFT_TENANT: { covers: 'OneDrive and SharePoint', lacks: 'no Microsoft link' },
19
+ NO_GOOGLE_GRANT: { covers: 'Google Drive', lacks: 'no Google connection' },
20
+ };
21
+ /**
22
+ * Every lane the backend reported. A backend older than ENG-6409 sends no
23
+ * `providers` and served Microsoft alone, so its top level IS that one lane.
24
+ */
25
+ function lanesOf(freshness) {
26
+ if (freshness.providers?.length)
27
+ return freshness.providers;
28
+ const { providers: _absent, ...top } = freshness;
29
+ return [{ ...top, provider: 'microsoft' }];
30
+ }
31
+ /**
32
+ * The lanes that serve this caller — the ones its rows can come from. A code
33
+ * this package does not know is NOT read as "unserved" (ENG-4311: match the
34
+ * known codes exactly and fall through on anything else).
35
+ */
36
+ function servedLanes(freshness) {
37
+ return lanesOf(freshness).filter((lane) => !lane.inapplicableReason || !INAPPLICABLE[lane.inapplicableReason]);
38
+ }
39
+ /** Total over the provider union: a new provider fails to compile here. */
40
+ function readerName(provider) {
41
+ switch (provider) {
42
+ case 'microsoft':
43
+ return 'Microsoft';
44
+ case 'google':
45
+ return 'Google';
46
+ default: {
47
+ const unhandled = provider;
48
+ return String(unhandled);
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * "workbook" while only Microsoft serves the caller, exactly as before; "file"
54
+ * once a Google lane does, because that lane lists Docs and Slides too.
55
+ */
56
+ function noun(freshness) {
57
+ return servedLanes(freshness).some((lane) => lane.provider === 'google')
58
+ ? 'file'
59
+ : 'workbook';
60
+ }
61
+ /**
62
+ * ENG-2814 — the "there is more" line, and it is never optional when a cursor
63
+ * came back. Written to the MODEL: it names the exact next call, because a
64
+ * hint that only says "more available" gets summarised away.
65
+ */
66
+ function moreToCome(nextCursor) {
67
+ if (!nextCursor)
68
+ return '';
69
+ return (`\n\nMORE FILES REMAIN — this is not the whole list. Call ` +
70
+ `\`list_unenrolled_files\` again with \`cursor="${nextCursor}"\`, and ` +
71
+ 'keep going until a response comes back with no cursor. Do not tell the ' +
72
+ 'user what they do or do not have until then. The cursor stops working 30 ' +
73
+ 'minutes after the first page; if it is refused as expired, start again ' +
74
+ 'from the first page rather than retrying it.');
75
+ }
76
+ /**
77
+ * The identity line `enroll_file` can act on, in the row's own provider's
78
+ * terms. A Microsoft row keeps its id pair. A Google row has no drive id and
79
+ * `enroll_file` takes a Google file only by its link, so the link is the id
80
+ * printed; with no link recorded the row says so rather than composing one.
81
+ */
82
+ function identityLine(item) {
83
+ const provider = item.provider ?? 'microsoft';
84
+ switch (provider) {
85
+ case 'microsoft':
86
+ return ` msId: ${item.msId}, driveMsId: ${item.driveMsId}`;
87
+ case 'google':
88
+ return item.webUrl
89
+ ? ` provider: google, fileId: ${item.msId} — enroll with url: ${item.webUrl}`
90
+ : ` provider: google, fileId: ${item.msId} — no link recorded; ask ` +
91
+ 'the user for its link and pass that to `enroll_file` as `url`';
92
+ default: {
93
+ const unhandled = provider;
94
+ return ` provider: ${String(unhandled)}, fileId: ${item.msId}`;
95
+ }
96
+ }
97
+ }
98
+ export function renderFound(items, freshness, nextCursor = null) {
99
+ const lines = items.map((item) => {
100
+ const where = item.parentPath ? ` in ${item.parentPath}` : '';
101
+ const modified = item.lastModifiedAt
102
+ ? `, modified ${item.lastModifiedAt}`
103
+ : '';
104
+ // `hidden` means the user REMOVED this file before; enrolling restores
105
+ // rather than duplicates, the only thing distinguishing it from new.
106
+ const previously = item.enrollmentState === 'hidden' ? ' [previously removed]' : '';
107
+ return (`- **${item.name}**${where}${modified}${previously}\n` +
108
+ identityLine(item));
109
+ });
110
+ // "on this page", never a bare count: with a cursor outstanding the number
111
+ // is a page size, not a total.
112
+ const counted = nextCursor
113
+ ? `${items.length} ${noun(freshness)}(s) not yet in Rockhopper on this page:`
114
+ : `${items.length} ${noun(freshness)}(s) not yet in Rockhopper:`;
115
+ return (`${counted}\n\n` +
116
+ `${lines.join('\n')}\n\n${dateline(freshness)}${moreToCome(nextCursor)}`);
117
+ }
118
+ /**
119
+ * No lane serves the account. No retry language (nothing changes on a later
120
+ * call), no `connect_microsoft` (ENG-2614's loop), and the scope named from
121
+ * the lanes the backend reported rather than assumed.
122
+ */
123
+ function inapplicableText(freshness) {
124
+ const known = lanesOf(freshness)
125
+ .map((lane) => INAPPLICABLE[lane.inapplicableReason ?? ''])
126
+ .filter((entry) => entry !== undefined);
127
+ return (`Rockhopper's drive inventory covers ${known.map((k) => k.covers).join(' and ')}, ` +
128
+ `and this account has ${known.map((k) => k.lacks).join(' and ')} — so ` +
129
+ 'there is nothing for it to list and nothing to retry. This is NOT ' +
130
+ 'evidence that every file is already in Rockhopper, and it does not mean ' +
131
+ 'the user has no files: it means this particular list cannot see them. ' +
132
+ 'Do not call `list_unenrolled_files` again for this user, and do not ' +
133
+ 'report their drive as covered. A file the user can link to is still ' +
134
+ 'added with `enroll_file` and that link.');
135
+ }
136
+ /** A Google lane whose refresh could not use the user's Google credential. */
137
+ const GOOGLE_LINK_FAILURE_TEXT = 'Rockhopper cannot see this user\'s Google Drive files: their Google ' +
138
+ 'account is not connected, or its connection has expired. Ask the user to ' +
139
+ 'connect Google Drive in Rockhopper Settings; do not compose a sign-in link ' +
140
+ 'yourself. This list will stay empty until that is done, which is NOT the ' +
141
+ 'same as having nothing to add.';
142
+ /** The first serving lane whose last refresh failed on its link, as text. */
143
+ function linkFailureText(freshness) {
144
+ for (const lane of servedLanes(freshness)) {
145
+ const failure = graphLinkFailureFrom(lane.lastFailureReason);
146
+ if (!failure)
147
+ continue;
148
+ switch (lane.provider) {
149
+ case 'microsoft':
150
+ return GRAPH_LINK_FAILURE_TEXT[failure];
151
+ case 'google':
152
+ return GOOGLE_LINK_FAILURE_TEXT;
153
+ default: {
154
+ const unhandled = lane.provider;
155
+ return String(unhandled);
156
+ }
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+ export function renderEmpty(freshness, nextCursor = null) {
162
+ // ORDER IS LOAD-BEARING (ENG-4283, ENG-4311, ENG-2814). An account no lane
163
+ // serves comes first: every later branch assumes a lane covers the caller,
164
+ // and its never-refreshed branch would otherwise claim a scan "has been
165
+ // started" forever. A broken link and an unfinished first refresh come next,
166
+ // because neither has anything to page THROUGH.
167
+ if (freshness.inapplicableReason && INAPPLICABLE[freshness.inapplicableReason]) {
168
+ return inapplicableText(freshness);
169
+ }
170
+ const linkFailure = linkFailureText(freshness);
171
+ if (linkFailure)
172
+ return linkFailure;
173
+ if (!freshness.asOf) {
174
+ return ('No answer yet — the first scan of this user\'s drive has not finished. ' +
175
+ `${freshness.refreshing ? 'One is running now.' : 'One has been started.'} ` +
176
+ 'Try again shortly. This is NOT evidence that every workbook is already ' +
177
+ `in Rockhopper.${failureNote(freshness)}`);
178
+ }
179
+ // ENG-2814 — an empty page WITH a cursor is the middle of a walk: the
180
+ // backend filters after cutting a chunk and stops at a scan budget.
181
+ if (nextCursor) {
182
+ return ('No un-enrolled workbooks on this page — but the search is NOT ' +
183
+ 'finished, and this says nothing yet about what the user has.' +
184
+ `${moreToCome(nextCursor)}`);
185
+ }
186
+ if (freshness.consecutiveFailures > 0) {
187
+ return ('No un-enrolled workbooks in the stored list, but the list is not ' +
188
+ `trustworthy right now.${failureNote(freshness)}\n\n${dateline(freshness)}`);
189
+ }
190
+ return (`Every ${noun(freshness)} Rockhopper has seen for this user is already in ` +
191
+ `Rockhopper.\n\n${dateline(freshness)}`);
192
+ }
193
+ /**
194
+ * How old the answer is, on every branch that has an answer, and which
195
+ * providers it was read from — never omitted, because the rows are stored.
196
+ */
197
+ function dateline(freshness) {
198
+ const asOf = freshness.asOf
199
+ ? `As of ${freshness.asOf}`
200
+ : 'Never successfully refreshed';
201
+ const stale = freshness.stale ? ' (stale)' : '';
202
+ const refreshing = freshness.refreshing
203
+ ? ' A refresh is running; call again for a newer answer.'
204
+ : '';
205
+ const served = servedLanes(freshness);
206
+ const readers = (served.length ? served : lanesOf(freshness))
207
+ .map((lane) => readerName(lane.provider))
208
+ .join(' or ');
209
+ return (`${asOf}${stale} — from Rockhopper's stored records, not a live ` +
210
+ `${readers} read.${refreshing}${failureNote(freshness)}`);
211
+ }
212
+ function failureNote(freshness) {
213
+ if (!freshness.consecutiveFailures || !freshness.lastFailureAt)
214
+ return '';
215
+ return (` Refresh has failed ${freshness.consecutiveFailures} time(s) in a row, ` +
216
+ `last at ${freshness.lastFailureAt}` +
217
+ `${freshness.lastFailureReason ? ` (${freshness.lastFailureReason})` : ''}.`);
218
+ }
219
+ //# sourceMappingURL=list-unenrolled-render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-unenrolled-render.js","sourceRoot":"","sources":["../../src/tools/list-unenrolled-render.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAkBlD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,SAAS,GACb,IAAI,CA8UN"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAMlD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,SAAS,GACb,IAAI,CAmSN"}
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { assertChangeHistoryComplete, assertEnrollmentComplete, isNotReady, notReadyToolResult, } from '../not-ready.js';
3
- import { changeModelForFileType, documentChangesUnavailableToolResult, } from '../document-changes.js';
4
- import { assertSheetExists, isUnknownSheet, unknownSheetToolResult, } from '../sheet-catalogue.js';
5
- import { readDocumentChanges } from './document-changes-read.js';
2
+ import { isNotReady, notReadyToolResult } from '../not-ready.js';
3
+ import { formatDocumentChanges } from '../document-changes.js';
4
+ import { fileChangesUnavailableText, readFileChanges } from '../file-changes.js';
5
+ import { isUnknownSheet, unknownSheetToolResult } from '../sheet-catalogue.js';
6
6
  export function registerSearchTool(server, api) {
7
7
  server.registerTool('search_files', {
8
8
  title: 'Search Files',
@@ -151,55 +151,31 @@ export function registerSearchTool(server, api) {
151
151
  },
152
152
  }, async ({ fileMsId, sheetName, cursor }) => {
153
153
  try {
154
- // ENG-5397 — WHICH CHANGE MODEL, before which reader. The two lanes
155
- // below are spreadsheet-only: `unattributed_change` requires a
156
- // `sheetName` and the sheet route also filters `changeType = 'cell'`,
157
- // so a Word document or a deck returned `[]` from them by
158
- // construction and the tool printed "No unattributed changes found".
159
- // Routing on the file's own type is what makes an absence mean
160
- // something.
161
- const file = await api.getEnrolledFile(fileMsId);
162
- const model = changeModelForFileType(file.fileType);
163
- if (model === null) {
164
- return documentChangesUnavailableToolResult({
165
- reason: 'unknown_file_type',
166
- fileMsId,
167
- fileName: file.name,
168
- });
154
+ // ENG-5397 — WHICH CHANGE MODEL, before which reader: the spreadsheet
155
+ // lanes cannot hold a document row, so a `.docx` read `[]` from them by
156
+ // construction. ENG-6431 — that dispatch (plus the fold, enrolment and
157
+ // sheet-existence gates) lives in `readFileChanges`, shared with the
158
+ // `…/changes` resource and the prompts; this tool only formats.
159
+ const read = await readFileChanges(api, fileMsId, { sheetName, cursor });
160
+ if (read.kind === 'unavailable') {
161
+ return {
162
+ content: [{ type: 'text', text: fileChangesUnavailableText(read) }],
163
+ isError: true,
164
+ };
169
165
  }
170
- if (model !== 'spreadsheet') {
171
- return await readDocumentChanges(api, {
172
- fileMsId,
173
- sheetName,
174
- fileType: file.fileType,
175
- fileName: file.name,
176
- });
166
+ if (read.kind === 'document') {
167
+ return {
168
+ content: [
169
+ {
170
+ type: 'text',
171
+ text: formatDocumentChanges(read.response, read.file.name),
172
+ },
173
+ ],
174
+ };
177
175
  }
178
- // Plan 02 ruling 5 (STRICT): gate BOTH modes. The commit-diff fold
179
- // retracts and rewrites the uncommitted window, so a pending fold
180
- // means this list is mid-rewrite in either shape.
181
- await assertChangeHistoryComplete(api, fileMsId);
182
- if (sheetName) {
176
+ if (read.kind === 'spreadsheet_sheet') {
183
177
  // Sheet-filtered mode: bounded by sheet size, no pagination needed.
184
- // Audit (2026-05-23) measured: `Summary` sheet on a 12 MB file
185
- // returned 9 rows clean. This is the practical workaround when the
186
- // caller already knows which sheet to drill into.
187
- const changes = await api.getUnattributedChangesBySheet(fileMsId, sheetName);
188
- // ENG-2824: an empty answer is the only ambiguous one, so it is the
189
- // only one that pays for the version read. A file still being read
190
- // for the first time has no versions and no parsed cells, and the
191
- // fold probe above cannot see that state — enrolment enqueues no
192
- // fold. See `assertEnrollmentComplete`.
193
- if (changes.length === 0) {
194
- assertEnrollmentComplete(fileMsId, await api.getFileVersions(fileMsId));
195
- // ENG-4347 — and the same trade one ambiguity over: only an empty
196
- // answer asserts something this tool has not checked. A row that
197
- // came back names its own sheet, so a non-empty answer has already
198
- // proved what this goes and asks. Enrolment is settled FIRST above
199
- // because a file whose initial read has not landed has no
200
- // catalogue either, and "still reading" is the truer answer.
201
- await assertSheetExists(api, file, sheetName);
202
- }
178
+ const { changes } = read;
203
179
  const body = formatChangeRows(changes);
204
180
  return {
205
181
  content: [
@@ -217,14 +193,8 @@ export function registerSearchTool(server, api) {
217
193
  // 25k-token client limit (audit measured one file at 12.5 MB / 28k
218
194
  // rows on the unpaginated route — KI-097).
219
195
  const MAX_DISPLAYED = 200;
220
- const page = await api.getUnattributedChangesPaginated(fileMsId, cursor);
196
+ const { page } = read;
221
197
  if (page.changes.length === 0) {
222
- // ENG-2824 — same rule as the sheet mode above. `totalCount === 0`
223
- // over a file whose initial read has not landed is a premature
224
- // absence, not a fact about the workbook.
225
- if (page.totalCount === 0) {
226
- assertEnrollmentComplete(fileMsId, await api.getFileVersions(fileMsId));
227
- }
228
198
  return {
229
199
  content: [
230
200
  {