@nestr/mcp 0.1.89 → 0.1.91
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/build/api/client.d.ts +224 -1
- package/build/api/client.d.ts.map +1 -1
- package/build/api/client.js +135 -2
- package/build/api/client.js.map +1 -1
- package/build/help/topics.js +3 -3
- package/build/tools/index.d.ts +1690 -159
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +478 -8
- package/build/tools/index.js.map +1 -1
- package/package.json +1 -1
package/build/tools/index.js
CHANGED
|
@@ -123,11 +123,31 @@ const HINT_URL_PATTERNS = [
|
|
|
123
123
|
},
|
|
124
124
|
// /nests/{id}/posts → nestr_get_comments
|
|
125
125
|
{ pattern: /^\/nests\/([^/]+)\/posts$/, tool: "nestr_get_comments", params: (m) => ({ nestId: m[1] }) },
|
|
126
|
+
// /nests/{id}/files → nestr_get_nest_files
|
|
127
|
+
{ pattern: /^\/nests\/([^/]+)\/files$/, tool: "nestr_get_nest_files", params: (m) => ({ nestId: m[1] }) },
|
|
126
128
|
// /nests/{id}/tensions → nestr_list_tensions
|
|
127
129
|
{ pattern: /^\/nests\/([^/]+)\/tensions$/, tool: "nestr_list_tensions", params: (m) => ({ nestId: m[1] }) },
|
|
128
130
|
// /nests/{id} → nestr_get_nest (must be last — catches all /nests/{id} patterns)
|
|
129
131
|
{ pattern: /^\/nests\/([^/]+)$/, tool: "nestr_get_nest", params: (m) => ({ nestId: m[1] }) },
|
|
130
132
|
];
|
|
133
|
+
const HINT_TYPE_TOOL_CALLS = {
|
|
134
|
+
no_strategy(nest) {
|
|
135
|
+
const nestId = nest._id;
|
|
136
|
+
if (!nestId)
|
|
137
|
+
return null;
|
|
138
|
+
const labels = nest.labels || [];
|
|
139
|
+
const isAnchorCircle = labels.includes("circleplus-anchor-circle")
|
|
140
|
+
|| labels.includes("anchor-circle");
|
|
141
|
+
const fieldKey = isAnchorCircle ? "anchor-circle.strategy" : "circle.strategy";
|
|
142
|
+
return {
|
|
143
|
+
tool: "nestr_update_nest",
|
|
144
|
+
params: {
|
|
145
|
+
nestId,
|
|
146
|
+
fields: { [fieldKey]: "<strategy statement — what this circle prioritises now vs. defers>" },
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
131
151
|
const HINT_ENDPOINT_TOOL_MAPPINGS = [
|
|
132
152
|
{
|
|
133
153
|
method: "POST",
|
|
@@ -262,9 +282,17 @@ export function enrichHints(data) {
|
|
|
262
282
|
const workspaceId = ancestors?.length ? ancestors[ancestors.length - 1] : undefined;
|
|
263
283
|
record.hints = record.hints.map((hint) => {
|
|
264
284
|
const enriched = { ...hint };
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
285
|
+
// Type-based overrides win over URL matching for hints whose URL is
|
|
286
|
+
// just the nest itself (`/nests/{id}` → nestr_get_nest) and doesn't
|
|
287
|
+
// point at the actionable follow-up. Registered in HINT_TYPE_TOOL_CALLS.
|
|
288
|
+
const typeOverride = HINT_TYPE_TOOL_CALLS[hint.type];
|
|
289
|
+
const overrideCall = typeOverride ? typeOverride(record) : null;
|
|
290
|
+
if (overrideCall) {
|
|
291
|
+
enriched.toolCall = overrideCall;
|
|
292
|
+
}
|
|
293
|
+
else if (hint.url) {
|
|
294
|
+
// Legacy: single URL → toolCall. Kept for backwards compatibility with
|
|
295
|
+
// hints that pre-date the endpoints[] payload.
|
|
268
296
|
let rawUrl = hint.url;
|
|
269
297
|
const apiPrefixMatch = rawUrl.match(/^https?:\/\/[^/]+\/api(\/.*)/);
|
|
270
298
|
if (apiPrefixMatch)
|
|
@@ -385,10 +413,15 @@ const coerceIntArray = (schema) => z.preprocess((val) => {
|
|
|
385
413
|
}
|
|
386
414
|
return val;
|
|
387
415
|
}, schema);
|
|
416
|
+
// Shared description for the sort parameter on list/fetch tools. All of these
|
|
417
|
+
// endpoints honor a `sort` query param server-side (field name, '-' prefix for
|
|
418
|
+
// descending) — the same fields the search `sort:` operator uses.
|
|
419
|
+
const SORT_DESCRIPTION = "Field to sort by, e.g. 'title', 'createdAt', 'updatedAt', 'due', 'order' (manual order). Prefix with '-' for descending, e.g. '-updatedAt'.";
|
|
388
420
|
// Tool input schemas using Zod
|
|
389
421
|
export const schemas = {
|
|
390
422
|
listWorkspaces: z.object({
|
|
391
423
|
search: z.string().optional().describe("Search query to filter workspaces"),
|
|
424
|
+
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
392
425
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
393
426
|
page: z.number().optional().describe("Page number (1-indexed) for pagination"),
|
|
394
427
|
}),
|
|
@@ -407,6 +440,7 @@ export const schemas = {
|
|
|
407
440
|
search: z.object({
|
|
408
441
|
workspaceId: z.string().describe("Workspace ID to search in"),
|
|
409
442
|
query: z.string().describe("Search query"),
|
|
443
|
+
sort: z.string().optional().describe(`${SORT_DESCRIPTION} Takes precedence over sort:/sort-order: operators in the query.`),
|
|
410
444
|
limit: z.number().optional().describe("Max results per page. Omit on first call to see meta.total count."),
|
|
411
445
|
page: z.number().optional().describe("Page number (1-indexed) for pagination"),
|
|
412
446
|
_listTitle: z.string().optional().describe("Short descriptive title for the list UI (e.g., \"Marketing projects\", \"Overdue tasks\"). Omit for default."),
|
|
@@ -418,6 +452,7 @@ export const schemas = {
|
|
|
418
452
|
}),
|
|
419
453
|
getNestChildren: z.object({
|
|
420
454
|
nestId: z.string().describe("Parent nest ID"),
|
|
455
|
+
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
421
456
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
422
457
|
page: z.number().optional().describe("Page number for pagination"),
|
|
423
458
|
hints: z.boolean().optional().describe("Include contextual hints on each child nest (default: true). Set to false for large result sets or bulk operations where contextual signals aren't needed."),
|
|
@@ -469,17 +504,20 @@ export const schemas = {
|
|
|
469
504
|
}),
|
|
470
505
|
listCircles: z.object({
|
|
471
506
|
workspaceId: z.string().describe("Workspace ID"),
|
|
507
|
+
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
472
508
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
473
509
|
page: z.number().optional().describe("Page number for pagination"),
|
|
474
510
|
}),
|
|
475
511
|
getCircleRoles: z.object({
|
|
476
512
|
workspaceId: z.string().describe("Workspace ID"),
|
|
477
513
|
circleId: z.string().describe("Circle ID"),
|
|
514
|
+
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
478
515
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
479
516
|
page: z.number().optional().describe("Page number for pagination"),
|
|
480
517
|
}),
|
|
481
518
|
listRoles: z.object({
|
|
482
519
|
workspaceId: z.string().describe("Workspace ID"),
|
|
520
|
+
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
483
521
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
484
522
|
page: z.number().optional().describe("Page number for pagination"),
|
|
485
523
|
}),
|
|
@@ -508,6 +546,7 @@ export const schemas = {
|
|
|
508
546
|
}),
|
|
509
547
|
getProjects: z.object({
|
|
510
548
|
workspaceId: z.string().describe("Workspace ID"),
|
|
549
|
+
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
511
550
|
limit: z.number().optional().describe("Max results per page. Omit to see full count in meta.total."),
|
|
512
551
|
page: z.number().optional().describe("Page number for pagination"),
|
|
513
552
|
_listTitle: z.string().optional().describe("Short descriptive title for the list UI (e.g., \"Engineering projects\"). Omit for default."),
|
|
@@ -609,6 +648,16 @@ export const schemas = {
|
|
|
609
648
|
getMe: z.object({
|
|
610
649
|
fullWorkspaces: z.boolean().optional().describe("Set true to include full workspace details (purpose, labels, governance type, user access roles). Recommended on first call to establish workspace context."),
|
|
611
650
|
}),
|
|
651
|
+
// Current user's cross-workspace activity (requires OAuth token)
|
|
652
|
+
myActivity: z.object({
|
|
653
|
+
limit: z.number().optional().describe("Max activity items to return (default 50, max 200)."),
|
|
654
|
+
withUser: z.string().optional().describe("Scope DM considerations to the conversation with this user id. Agent internal considerations from direct-message runs are redacted to an anonymous marker unless you name that conversation's counterpart here."),
|
|
655
|
+
}),
|
|
656
|
+
// Another user's cross-workspace activity (requires OAuth token)
|
|
657
|
+
userActivity: z.object({
|
|
658
|
+
userId: z.string().describe("The user whose activity to fetch."),
|
|
659
|
+
limit: z.number().optional().describe("Max activity items to return (default 50, max 200)."),
|
|
660
|
+
}),
|
|
612
661
|
// User tension tools (requires OAuth token)
|
|
613
662
|
listMyTensions: z.object({
|
|
614
663
|
context: z.string().optional().describe("Optional context filter (e.g., workspace ID or circle ID)"),
|
|
@@ -640,8 +689,10 @@ export const schemas = {
|
|
|
640
689
|
listTensions: z.object({
|
|
641
690
|
nestId: z.string().describe("ID of the circle or role to list tensions for"),
|
|
642
691
|
search: z.string().optional().describe("Search query to filter tensions"),
|
|
692
|
+
sort: z.string().optional().describe(SORT_DESCRIPTION),
|
|
643
693
|
limit: z.number().optional().describe("Max results to return"),
|
|
644
|
-
|
|
694
|
+
page: z.number().optional().describe("Page number for pagination"),
|
|
695
|
+
order: z.string().optional().describe("Deprecated alias of sort"),
|
|
645
696
|
}),
|
|
646
697
|
updateTension: z.object({
|
|
647
698
|
nestId: z.string().describe("ID of the circle or role the tension belongs to"),
|
|
@@ -759,6 +810,46 @@ export const schemas = {
|
|
|
759
810
|
maxImages: coerceFromJson(z.number().int().positive().optional()).describe("Help-article mode only. Cap on how many screenshots the default selection attaches (the first N content images in document order). Default 3, max 6. Ignored when imageIndexes is provided."),
|
|
760
811
|
}).refine((v) => Boolean(v.topic) || Boolean(v.search), { message: "Provide either `topic` or `search`." }),
|
|
761
812
|
diagnose: z.object({}).describe("No arguments — diagnose reads session state from the server."),
|
|
813
|
+
// Connector tools (registration is workspace-admin only)
|
|
814
|
+
listConnectors: z.object({
|
|
815
|
+
workspaceId: z.string().describe("Workspace ID whose connector catalog to list"),
|
|
816
|
+
}),
|
|
817
|
+
registerConnector: z.object({
|
|
818
|
+
workspaceId: z.string().describe("Workspace ID to register the connector in"),
|
|
819
|
+
type: z.enum(["mcp", "cli", "api"]).describe("Transport: 'mcp' (MCP server over a url), 'api' (REST endpoint over a url), or 'cli' (a command)"),
|
|
820
|
+
name: z.string().describe("Unique connector name within the workspace catalog"),
|
|
821
|
+
config: coerceFromJson(z.record(z.unknown())).optional().describe("Per-type transport config, no secret. mcp/api need a url (e.g., { url: 'https://...' }); cli needs a command (e.g., { command: 'some-cli' }). Optional non-secret headers go under headers."),
|
|
822
|
+
capabilities: coerceFromJson(z.record(z.unknown())).optional().describe("Capability descriptor: { discover: boolean, tools: [{ name, description, inputSchema }] }. discover:true lets the connector self-describe its tools at runtime."),
|
|
823
|
+
exposure: coerceFromJson(z.record(z.unknown())).optional().describe("Exposure policy deciding which owners may bind: { userAgent: boolean, domainGated: boolean }. Set domainGated:true to allow binding to a role's domain."),
|
|
824
|
+
authStrategy: z.enum(["secret", "oauth2"]).optional().describe("How a principal connects: 'secret' (a one-time secret captured via the Connect button) or 'oauth2'. The agent never sees the secret."),
|
|
825
|
+
}),
|
|
826
|
+
bindConnector: z.object({
|
|
827
|
+
workspaceId: z.string().describe("Workspace ID the connector and owner belong to"),
|
|
828
|
+
connectorId: z.string().describe("ID of an enabled connector from nestr_list_connectors"),
|
|
829
|
+
ownerType: z.enum(["user", "agent", "workspace", "role-domain"]).describe("Owner type. 'role-domain' binds the connector to a role's domain so the role can use it and a credentials field is materialised on the domain."),
|
|
830
|
+
ownerId: z.string().describe("Owner ID. user/agent: the user ID. workspace: the workspace ID. role-domain: the domain nest ID."),
|
|
831
|
+
}),
|
|
832
|
+
// File attachments (a comment id works as the nestId — files are keyed by nestId)
|
|
833
|
+
getNestFiles: z.object({
|
|
834
|
+
nestId: z.string().describe("Nest or comment ID whose file attachments to list"),
|
|
835
|
+
}),
|
|
836
|
+
readFile: z.object({
|
|
837
|
+
nestId: z.string().describe("Nest or comment ID the file is attached to"),
|
|
838
|
+
fileId: z.string().describe("File ID from nestr_get_nest_files"),
|
|
839
|
+
}),
|
|
840
|
+
uploadFile: z.object({
|
|
841
|
+
nestId: z.string().describe("Nest or comment ID to attach the file to"),
|
|
842
|
+
name: z.string().describe('File name including extension (e.g. "notes.md", "data.csv")'),
|
|
843
|
+
contentType: z.string().describe('MIME type of the file (e.g. "text/markdown", "text/csv", "image/png")'),
|
|
844
|
+
content: z.string().optional().describe("File content as UTF-8 text. Use for text-native files you author directly (.md, .txt, .csv, .json, .html, .svg, code). Stored verbatim. Provide this OR dataBase64, not both."),
|
|
845
|
+
dataBase64: z.string().optional().describe("File bytes, base64-encoded. Use for binary content you already hold as base64. Provide this OR content, not both."),
|
|
846
|
+
}).refine((v) => (v.content !== undefined) !== (v.dataBase64 !== undefined), {
|
|
847
|
+
message: "Provide exactly one of content (UTF-8 text) or dataBase64 (base64 bytes).",
|
|
848
|
+
}),
|
|
849
|
+
deleteFile: z.object({
|
|
850
|
+
nestId: z.string().describe("Nest or comment ID the file is attached to"),
|
|
851
|
+
fileId: z.string().describe("File ID from nestr_get_nest_files"),
|
|
852
|
+
}),
|
|
762
853
|
};
|
|
763
854
|
// Tool annotations for MCP - hints for clients on tool behavior
|
|
764
855
|
const readOnly = { annotations: { readOnlyHint: true, destructiveHint: false } };
|
|
@@ -797,6 +888,7 @@ export const toolDefinitions = [
|
|
|
797
888
|
type: "object",
|
|
798
889
|
properties: {
|
|
799
890
|
search: { type: "string", description: "Search query to filter workspaces" },
|
|
891
|
+
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
800
892
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
801
893
|
page: { type: "number", description: "Page number (1-indexed) for pagination" },
|
|
802
894
|
stripDescription: { type: "boolean", description: "Set true to strip description fields from response, significantly reducing size. Ideal for bulk/index operations." },
|
|
@@ -863,6 +955,7 @@ export const toolDefinitions = [
|
|
|
863
955
|
properties: {
|
|
864
956
|
workspaceId: { type: "string", description: "Workspace ID to search in" },
|
|
865
957
|
query: { type: "string", description: "Search query with optional operators (e.g., 'label:role', 'assignee:me completed:false')" },
|
|
958
|
+
sort: { type: "string", description: `${SORT_DESCRIPTION} Takes precedence over sort:/sort-order: operators in the query.` },
|
|
866
959
|
limit: { type: "number", description: "Max results per page. Do NOT set on first call - let API return default with meta.total count showing total matches." },
|
|
867
960
|
page: { type: "number", description: "Page number (1-indexed) for fetching additional pages" },
|
|
868
961
|
stripDescription: { type: "boolean", description: "Set true to strip description fields from response, significantly reducing size. Ideal for bulk/index operations." },
|
|
@@ -896,6 +989,7 @@ export const toolDefinitions = [
|
|
|
896
989
|
type: "object",
|
|
897
990
|
properties: {
|
|
898
991
|
nestId: { type: "string", description: "Parent nest ID" },
|
|
992
|
+
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
899
993
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
900
994
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
901
995
|
hints: { type: "boolean", description: "Include contextual hints (default: true). Set to false for large result sets or bulk operations." },
|
|
@@ -1074,6 +1168,7 @@ export const toolDefinitions = [
|
|
|
1074
1168
|
type: "object",
|
|
1075
1169
|
properties: {
|
|
1076
1170
|
workspaceId: { type: "string", description: "Workspace ID" },
|
|
1171
|
+
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1077
1172
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1078
1173
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1079
1174
|
stripDescription: { type: "boolean", description: "Set true to strip description fields from response, significantly reducing size. Ideal for large workspaces." },
|
|
@@ -1090,6 +1185,7 @@ export const toolDefinitions = [
|
|
|
1090
1185
|
properties: {
|
|
1091
1186
|
workspaceId: { type: "string", description: "Workspace ID" },
|
|
1092
1187
|
circleId: { type: "string", description: "Circle ID" },
|
|
1188
|
+
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1093
1189
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1094
1190
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1095
1191
|
stripDescription: { type: "boolean", description: "Set true to strip description fields from response, significantly reducing size. Ideal for large circles." },
|
|
@@ -1105,6 +1201,7 @@ export const toolDefinitions = [
|
|
|
1105
1201
|
type: "object",
|
|
1106
1202
|
properties: {
|
|
1107
1203
|
workspaceId: { type: "string", description: "Workspace ID" },
|
|
1204
|
+
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1108
1205
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1109
1206
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1110
1207
|
stripDescription: { type: "boolean", description: "Set true to strip description fields from response, significantly reducing size. Ideal for large workspaces." },
|
|
@@ -1179,6 +1276,7 @@ export const toolDefinitions = [
|
|
|
1179
1276
|
type: "object",
|
|
1180
1277
|
properties: {
|
|
1181
1278
|
workspaceId: { type: "string", description: "Workspace ID" },
|
|
1279
|
+
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1182
1280
|
limit: { type: "number", description: "Omit on first call to see meta.total count" },
|
|
1183
1281
|
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1184
1282
|
stripDescription: { type: "boolean", description: "Set true to strip description fields from response, significantly reducing size. Ideal for large workspaces." },
|
|
@@ -1444,6 +1542,33 @@ export const toolDefinitions = [
|
|
|
1444
1542
|
_meta: completableListUi,
|
|
1445
1543
|
...readOnly,
|
|
1446
1544
|
},
|
|
1545
|
+
// Current user's cross-workspace activity (requires OAuth token)
|
|
1546
|
+
{
|
|
1547
|
+
name: "nestr_my_activity",
|
|
1548
|
+
description: "The caller's own activity across ALL their workspaces, newest first (gated by workspace membership and the token's scope). Not scoped to a project or a single nest — it's everything you've done. For an agent, this is how it sees what it has done: past considerations (with the tools used and the outcome), comments, governance changes, and other actions. Agent internal considerations from direct-message runs are redacted to an anonymous marker unless you pass withUser with that conversation's counterpart. Auth: OAuth only (user-scoped — workspace API keys lack user identity). On auth failure call nestr_diagnose.",
|
|
1549
|
+
inputSchema: {
|
|
1550
|
+
type: "object",
|
|
1551
|
+
properties: {
|
|
1552
|
+
limit: { type: "number", description: "Max activity items to return (default 50, max 200)." },
|
|
1553
|
+
withUser: { type: "string", description: "Scope DM considerations to the conversation with this user id. Agent internal considerations from direct-message runs are redacted to an anonymous marker unless you name that conversation's counterpart here." },
|
|
1554
|
+
},
|
|
1555
|
+
},
|
|
1556
|
+
...readOnly,
|
|
1557
|
+
},
|
|
1558
|
+
// Another user's cross-workspace activity (requires OAuth token)
|
|
1559
|
+
{
|
|
1560
|
+
name: "nestr_user_activity",
|
|
1561
|
+
description: "Another user's activity across the workspaces you share with them, newest first. Lets an agent see what a colleague or another agent has done: their past considerations (with the tools used and the outcome), comments, governance changes, and other actions. Agent internal considerations from direct-message runs show their substance only when you are a participant of that conversation, otherwise an anonymous marker. Auth: OAuth only (user-scoped — workspace API keys lack user identity). On auth failure call nestr_diagnose.",
|
|
1562
|
+
inputSchema: {
|
|
1563
|
+
type: "object",
|
|
1564
|
+
properties: {
|
|
1565
|
+
userId: { type: "string", description: "The user whose activity to fetch." },
|
|
1566
|
+
limit: { type: "number", description: "Max activity items to return (default 50, max 200)." },
|
|
1567
|
+
},
|
|
1568
|
+
required: ["userId"],
|
|
1569
|
+
},
|
|
1570
|
+
...readOnly,
|
|
1571
|
+
},
|
|
1447
1572
|
// Label management
|
|
1448
1573
|
{
|
|
1449
1574
|
name: "nestr_add_label",
|
|
@@ -1601,8 +1726,12 @@ export const toolDefinitions = [
|
|
|
1601
1726
|
properties: {
|
|
1602
1727
|
nestId: { type: "string", description: "ID of the circle or role to list tensions for" },
|
|
1603
1728
|
search: { type: "string", description: "Search query to filter tensions" },
|
|
1729
|
+
sort: { type: "string", description: SORT_DESCRIPTION },
|
|
1604
1730
|
limit: { type: "number", description: "Max results to return" },
|
|
1605
|
-
|
|
1731
|
+
page: { type: "number", description: "Page number (1-indexed)" },
|
|
1732
|
+
// The legacy `order` alias is deliberately not advertised — the Zod
|
|
1733
|
+
// schema still accepts it so existing callers keep working, but new
|
|
1734
|
+
// clients should only learn the canonical `sort` param.
|
|
1606
1735
|
},
|
|
1607
1736
|
required: ["nestId"],
|
|
1608
1737
|
},
|
|
@@ -1856,6 +1985,129 @@ export const toolDefinitions = [
|
|
|
1856
1985
|
},
|
|
1857
1986
|
...destructive,
|
|
1858
1987
|
},
|
|
1988
|
+
{
|
|
1989
|
+
name: "nestr_list_connectors",
|
|
1990
|
+
description: "List the workspace's connector catalog: the mcp / cli / api templates an admin has registered. Each entry holds no secret and shows its type, name, authStrategy, config, capabilities, exposure ({ userAgent, domainGated }) and whether it is enabled. Typical flow: register a connector, then bind it to a role's domain with nestr_bind_connector, then a human or agent connects the account via the credentials field's Connect button (the secret is captured out-of-band, never by the agent). Auth: any valid token can list.",
|
|
1991
|
+
inputSchema: {
|
|
1992
|
+
type: "object",
|
|
1993
|
+
properties: {
|
|
1994
|
+
workspaceId: { type: "string", description: "Workspace ID whose connector catalog to list" },
|
|
1995
|
+
},
|
|
1996
|
+
required: ["workspaceId"],
|
|
1997
|
+
},
|
|
1998
|
+
...readOnly,
|
|
1999
|
+
},
|
|
2000
|
+
{
|
|
2001
|
+
name: "nestr_register_connector",
|
|
2002
|
+
description: "Register a connector in the workspace catalog: a reusable mcp / cli / api template that holds no secret. Workspace-admin only. A non-admin caller gets AUTH_SCOPE_INSUFFICIENT (call nestr_diagnose on any auth error). Provide type ('mcp' or 'api' need a url in config; 'cli' needs a command) and a unique name; optionally capabilities, exposure ({ userAgent, domainGated }), and authStrategy ('secret' or 'oauth2'). This only creates the template. Typical flow: register here, then bind it to a role's domain with nestr_bind_connector, then a human or agent connects the account via the credentials field's Connect button. The secret is captured out-of-band through that button, never by the agent.",
|
|
2003
|
+
inputSchema: {
|
|
2004
|
+
type: "object",
|
|
2005
|
+
properties: {
|
|
2006
|
+
workspaceId: { type: "string", description: "Workspace ID to register the connector in" },
|
|
2007
|
+
type: {
|
|
2008
|
+
type: "string",
|
|
2009
|
+
enum: ["mcp", "cli", "api"],
|
|
2010
|
+
description: "Transport: 'mcp' (MCP server over a url), 'api' (REST endpoint over a url), or 'cli' (a command)",
|
|
2011
|
+
},
|
|
2012
|
+
name: { type: "string", description: "Unique connector name within the workspace catalog" },
|
|
2013
|
+
config: {
|
|
2014
|
+
type: "object",
|
|
2015
|
+
description: "Per-type transport config, no secret. mcp/api need a url (e.g., { url: 'https://...' }); cli needs a command (e.g., { command: 'some-cli' }). Optional non-secret headers go under headers.",
|
|
2016
|
+
},
|
|
2017
|
+
capabilities: {
|
|
2018
|
+
type: "object",
|
|
2019
|
+
description: "Capability descriptor: { discover: boolean, tools: [{ name, description, inputSchema }] }. discover:true lets the connector self-describe its tools at runtime.",
|
|
2020
|
+
},
|
|
2021
|
+
exposure: {
|
|
2022
|
+
type: "object",
|
|
2023
|
+
description: "Exposure policy deciding which owners may bind: { userAgent: boolean, domainGated: boolean }. Set domainGated:true to allow binding to a role's domain.",
|
|
2024
|
+
},
|
|
2025
|
+
authStrategy: {
|
|
2026
|
+
type: "string",
|
|
2027
|
+
enum: ["secret", "oauth2"],
|
|
2028
|
+
description: "How a principal connects: 'secret' (a one-time secret captured via the Connect button) or 'oauth2'. The agent never sees the secret.",
|
|
2029
|
+
},
|
|
2030
|
+
},
|
|
2031
|
+
required: ["workspaceId", "type", "name"],
|
|
2032
|
+
},
|
|
2033
|
+
...mutating,
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
name: "nestr_bind_connector",
|
|
2037
|
+
description: "Bind a registered connector to an owner so that owner can use it. Owner types: 'user' or 'agent' (ownerId is the user ID), 'workspace' (ownerId is the workspace ID), or 'role-domain' (ownerId is the domain nest ID). A 'role-domain' owner also materialises a credentials field on the domain nest, so the role can use the connector and the Connect button renders there; the response then includes credentialsField { domainId, fieldId, fieldCode }. After binding, a human or agent connects the account via that Connect button. The secret is captured out-of-band and is never seen by the agent. Workspace-admin only: a non-admin caller gets AUTH_SCOPE_INSUFFICIENT. The connector must already be registered (nestr_register_connector) and enabled.",
|
|
2038
|
+
inputSchema: {
|
|
2039
|
+
type: "object",
|
|
2040
|
+
properties: {
|
|
2041
|
+
workspaceId: { type: "string", description: "Workspace ID the connector and owner belong to" },
|
|
2042
|
+
connectorId: { type: "string", description: "ID of an enabled connector from nestr_list_connectors" },
|
|
2043
|
+
ownerType: {
|
|
2044
|
+
type: "string",
|
|
2045
|
+
enum: ["user", "agent", "workspace", "role-domain"],
|
|
2046
|
+
description: "Owner type. 'role-domain' binds the connector to a role's domain so the role can use it and a credentials field is materialised on the domain.",
|
|
2047
|
+
},
|
|
2048
|
+
ownerId: {
|
|
2049
|
+
type: "string",
|
|
2050
|
+
description: "Owner ID. user/agent: the user ID. workspace: the workspace ID. role-domain: the domain nest ID.",
|
|
2051
|
+
},
|
|
2052
|
+
},
|
|
2053
|
+
required: ["workspaceId", "connectorId", "ownerType", "ownerId"],
|
|
2054
|
+
},
|
|
2055
|
+
...mutating,
|
|
2056
|
+
},
|
|
2057
|
+
{
|
|
2058
|
+
name: "nestr_get_nest_files",
|
|
2059
|
+
description: "List a nest's file attachments. A comment ID works too — files are keyed by nestId, so pass a comment ID to see files attached to that comment. Returns each file's id, name, contentType and size. Use nestr_read_file with a returned id to read one (images come back as viewable image content). Auth: any valid token with access to the nest.",
|
|
2060
|
+
inputSchema: {
|
|
2061
|
+
type: "object",
|
|
2062
|
+
properties: {
|
|
2063
|
+
nestId: { type: "string", description: "Nest or comment ID whose file attachments to list" },
|
|
2064
|
+
},
|
|
2065
|
+
required: ["nestId"],
|
|
2066
|
+
},
|
|
2067
|
+
...readOnly,
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
name: "nestr_read_file",
|
|
2071
|
+
description: "Read a single file attachment on a nest (or comment). Branches on contentType: images (image/*) return as viewable image content so you can see them (very large images return metadata only); JSON and text (application/json, text/*) return as decoded UTF-8 text (large text is truncated); PDFs and other types return their metadata only (cannot be inlined yet). Get file ids from nestr_get_nest_files. A comment ID works as the nestId. Auth: any valid token with access to the nest.",
|
|
2072
|
+
inputSchema: {
|
|
2073
|
+
type: "object",
|
|
2074
|
+
properties: {
|
|
2075
|
+
nestId: { type: "string", description: "Nest or comment ID the file is attached to" },
|
|
2076
|
+
fileId: { type: "string", description: "File ID from nestr_get_nest_files" },
|
|
2077
|
+
},
|
|
2078
|
+
required: ["nestId", "fileId"],
|
|
2079
|
+
},
|
|
2080
|
+
...readOnly,
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
name: "nestr_upload_file",
|
|
2084
|
+
description: "Upload a file and attach it to a nest (or comment; files are keyed by nestId, so a comment ID attaches the file to that comment). Provide the file one of two ways: content (UTF-8 text, for text-native files you author directly such as .md, .csv, .json, .html, .svg, or code) or dataBase64 (base64-encoded bytes you already have). Any content type is accepted; the upload is rejected if it exceeds the server's maximum size (default 10MB). Returns the new file's descriptor (id, name, contentType, size). Auth: a token with write access to the nest.",
|
|
2085
|
+
inputSchema: {
|
|
2086
|
+
type: "object",
|
|
2087
|
+
properties: {
|
|
2088
|
+
nestId: { type: "string", description: "Nest or comment ID to attach the file to" },
|
|
2089
|
+
name: { type: "string", description: 'File name including extension (e.g. "notes.md", "data.csv")' },
|
|
2090
|
+
contentType: { type: "string", description: 'MIME type (e.g. "text/markdown", "text/csv", "image/png")' },
|
|
2091
|
+
content: { type: "string", description: "File content as UTF-8 text (for text-native files: .md, .txt, .csv, .json, .html, .svg, code). Stored verbatim. Provide this OR dataBase64." },
|
|
2092
|
+
dataBase64: { type: "string", description: "File bytes, base64-encoded. Provide this OR content." },
|
|
2093
|
+
},
|
|
2094
|
+
required: ["nestId", "name", "contentType"],
|
|
2095
|
+
},
|
|
2096
|
+
...mutating,
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
name: "nestr_delete_file",
|
|
2100
|
+
description: "Delete a file attachment from a nest (or comment). Get the file id from nestr_get_nest_files. A comment ID works as the nestId. This permanently removes the file. Auth: a token with delete access to the nest.",
|
|
2101
|
+
inputSchema: {
|
|
2102
|
+
type: "object",
|
|
2103
|
+
properties: {
|
|
2104
|
+
nestId: { type: "string", description: "Nest or comment ID the file is attached to" },
|
|
2105
|
+
fileId: { type: "string", description: "File ID from nestr_get_nest_files" },
|
|
2106
|
+
},
|
|
2107
|
+
required: ["nestId", "fileId"],
|
|
2108
|
+
},
|
|
2109
|
+
...destructive,
|
|
2110
|
+
},
|
|
1859
2111
|
];
|
|
1860
2112
|
// Strip description fields from nest objects in response data
|
|
1861
2113
|
export function stripDescriptionFields(data) {
|
|
@@ -1907,6 +2159,42 @@ export function unescapeRichTextFields(args) {
|
|
|
1907
2159
|
}
|
|
1908
2160
|
return changed ? result : args;
|
|
1909
2161
|
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Extract sort:/sort-order:/limit: directives from a search query string.
|
|
2164
|
+
*
|
|
2165
|
+
* The REST API only honors sorting and limits via the `sort`/`limit` query
|
|
2166
|
+
* params — these operators inside the search string are dropped from free-text
|
|
2167
|
+
* matching and otherwise ignored. Translating them here keeps the documented
|
|
2168
|
+
* search syntax (see nestr_help('search')) working. The query string itself is
|
|
2169
|
+
* sent unmodified: the server strips operator terms from text matching anyway.
|
|
2170
|
+
*
|
|
2171
|
+
* First occurrence wins for each directive, matching the server-side parsing
|
|
2172
|
+
* of these operators elsewhere in Nestr.
|
|
2173
|
+
*/
|
|
2174
|
+
export function extractSearchDirectives(query) {
|
|
2175
|
+
let sortField;
|
|
2176
|
+
let sortOrder;
|
|
2177
|
+
let limit;
|
|
2178
|
+
for (const term of query.split(/\s+/)) {
|
|
2179
|
+
const lower = term.toLowerCase();
|
|
2180
|
+
if (sortField === undefined && lower.startsWith("sort:")) {
|
|
2181
|
+
sortField = term.slice("sort:".length);
|
|
2182
|
+
}
|
|
2183
|
+
else if (sortOrder === undefined && lower.startsWith("sort-order:")) {
|
|
2184
|
+
sortOrder = lower.slice("sort-order:".length);
|
|
2185
|
+
}
|
|
2186
|
+
else if (limit === undefined && lower.startsWith("limit:")) {
|
|
2187
|
+
const parsed = Number.parseInt(lower.slice("limit:".length), 10);
|
|
2188
|
+
if (!Number.isNaN(parsed) && parsed > 0)
|
|
2189
|
+
limit = parsed;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
let sort;
|
|
2193
|
+
if (sortField) {
|
|
2194
|
+
sort = sortOrder === "desc" && !sortField.startsWith("-") ? `-${sortField}` : sortField;
|
|
2195
|
+
}
|
|
2196
|
+
return { sort, limit };
|
|
2197
|
+
}
|
|
1910
2198
|
export async function handleToolCall(client, name, args, context) {
|
|
1911
2199
|
const sanitizedArgs = unescapeRichTextFields(args);
|
|
1912
2200
|
const shouldStripDescription = sanitizedArgs.stripDescription === true;
|
|
@@ -2114,6 +2402,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2114
2402
|
const parsed = schemas.listWorkspaces.parse(args);
|
|
2115
2403
|
const workspaces = await client.listWorkspaces({
|
|
2116
2404
|
search: parsed.search,
|
|
2405
|
+
sort: parsed.sort,
|
|
2117
2406
|
limit: parsed.limit,
|
|
2118
2407
|
page: parsed.page,
|
|
2119
2408
|
cleanText: true,
|
|
@@ -2142,7 +2431,13 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2142
2431
|
}
|
|
2143
2432
|
case "nestr_search": {
|
|
2144
2433
|
const parsed = schemas.search.parse(args);
|
|
2145
|
-
const
|
|
2434
|
+
const directives = extractSearchDirectives(parsed.query);
|
|
2435
|
+
const results = await client.searchWorkspace(parsed.workspaceId, parsed.query, {
|
|
2436
|
+
sort: parsed.sort ?? directives.sort,
|
|
2437
|
+
limit: parsed.limit ?? directives.limit,
|
|
2438
|
+
page: parsed.page,
|
|
2439
|
+
cleanText: true,
|
|
2440
|
+
});
|
|
2146
2441
|
return formatResult(completableResponse(compactResponse(results), "search", parsed._listTitle || `Search: ${parsed.query}`));
|
|
2147
2442
|
}
|
|
2148
2443
|
case "nestr_get_nest": {
|
|
@@ -2157,6 +2452,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2157
2452
|
case "nestr_get_nest_children": {
|
|
2158
2453
|
const parsed = schemas.getNestChildren.parse(args);
|
|
2159
2454
|
const children = await client.getNestChildren(parsed.nestId, {
|
|
2455
|
+
sort: parsed.sort,
|
|
2160
2456
|
limit: parsed.limit,
|
|
2161
2457
|
page: parsed.page,
|
|
2162
2458
|
cleanText: true,
|
|
@@ -2275,6 +2571,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2275
2571
|
case "nestr_list_circles": {
|
|
2276
2572
|
const parsed = schemas.listCircles.parse(args);
|
|
2277
2573
|
const circles = await client.listCircles(parsed.workspaceId, {
|
|
2574
|
+
sort: parsed.sort,
|
|
2278
2575
|
limit: parsed.limit,
|
|
2279
2576
|
page: parsed.page,
|
|
2280
2577
|
cleanText: true,
|
|
@@ -2283,12 +2580,13 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2283
2580
|
}
|
|
2284
2581
|
case "nestr_get_circle_roles": {
|
|
2285
2582
|
const parsed = schemas.getCircleRoles.parse(args);
|
|
2286
|
-
const roles = await client.getCircleRoles(parsed.workspaceId, parsed.circleId, { limit: parsed.limit, page: parsed.page, cleanText: true });
|
|
2583
|
+
const roles = await client.getCircleRoles(parsed.workspaceId, parsed.circleId, { sort: parsed.sort, limit: parsed.limit, page: parsed.page, cleanText: true });
|
|
2287
2584
|
return formatResult(compactResponse(roles, "role"));
|
|
2288
2585
|
}
|
|
2289
2586
|
case "nestr_list_roles": {
|
|
2290
2587
|
const parsed = schemas.listRoles.parse(args);
|
|
2291
2588
|
const roles = await client.listRoles(parsed.workspaceId, {
|
|
2589
|
+
sort: parsed.sort,
|
|
2292
2590
|
limit: parsed.limit,
|
|
2293
2591
|
page: parsed.page,
|
|
2294
2592
|
cleanText: true,
|
|
@@ -2331,6 +2629,7 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2331
2629
|
case "nestr_get_projects": {
|
|
2332
2630
|
const parsed = schemas.getProjects.parse(args);
|
|
2333
2631
|
const projects = await client.getWorkspaceProjects(parsed.workspaceId, {
|
|
2632
|
+
sort: parsed.sort,
|
|
2334
2633
|
limit: parsed.limit,
|
|
2335
2634
|
page: parsed.page,
|
|
2336
2635
|
cleanText: true,
|
|
@@ -2490,6 +2789,21 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2490
2789
|
const items = await client.getDailyPlan();
|
|
2491
2790
|
return formatResult(completableResponse(compactResponse(items), "daily-plan", "Daily Plan"));
|
|
2492
2791
|
}
|
|
2792
|
+
case "nestr_my_activity": {
|
|
2793
|
+
const parsed = schemas.myActivity.parse(args);
|
|
2794
|
+
const activity = await client.getMyActivity({
|
|
2795
|
+
limit: parsed.limit,
|
|
2796
|
+
withUser: parsed.withUser,
|
|
2797
|
+
});
|
|
2798
|
+
return formatResult({ count: activity.length, activity });
|
|
2799
|
+
}
|
|
2800
|
+
case "nestr_user_activity": {
|
|
2801
|
+
const parsed = schemas.userActivity.parse(args);
|
|
2802
|
+
const activity = await client.getUserActivity(parsed.userId, {
|
|
2803
|
+
limit: parsed.limit,
|
|
2804
|
+
});
|
|
2805
|
+
return formatResult({ count: activity.length, activity });
|
|
2806
|
+
}
|
|
2493
2807
|
// Current user identity and workspace context
|
|
2494
2808
|
case "nestr_get_me": {
|
|
2495
2809
|
const parsed = schemas.getMe.parse(args);
|
|
@@ -2610,7 +2924,14 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2610
2924
|
}
|
|
2611
2925
|
case "nestr_list_tensions": {
|
|
2612
2926
|
const parsed = schemas.listTensions.parse(args);
|
|
2613
|
-
const tensions = await client.listTensions(parsed.nestId, parsed.search, {
|
|
2927
|
+
const tensions = await client.listTensions(parsed.nestId, parsed.search, {
|
|
2928
|
+
// `order` is the legacy name for this option — it was never honored
|
|
2929
|
+
// by the API (which reads `sort`), so route both through sort.
|
|
2930
|
+
sort: parsed.sort ?? parsed.order,
|
|
2931
|
+
limit: parsed.limit,
|
|
2932
|
+
page: parsed.page,
|
|
2933
|
+
cleanText: true,
|
|
2934
|
+
});
|
|
2614
2935
|
return formatResult(compactResponse(enrichHints(tensions)));
|
|
2615
2936
|
}
|
|
2616
2937
|
case "nestr_update_tension": {
|
|
@@ -2733,6 +3054,127 @@ async function _handleToolCall(client, name, args, context) {
|
|
|
2733
3054
|
const result = await client.removeGraphLink(parsed.nestId, parsed.relation, parsed.targetId);
|
|
2734
3055
|
return formatResult({ message: "Graph link removed" });
|
|
2735
3056
|
}
|
|
3057
|
+
// Connector tools
|
|
3058
|
+
case "nestr_list_connectors": {
|
|
3059
|
+
const parsed = schemas.listConnectors.parse(args);
|
|
3060
|
+
const connectors = await client.listConnectors(parsed.workspaceId);
|
|
3061
|
+
return formatResult(connectors);
|
|
3062
|
+
}
|
|
3063
|
+
case "nestr_register_connector": {
|
|
3064
|
+
const parsed = schemas.registerConnector.parse(args);
|
|
3065
|
+
const connector = await client.registerConnector(parsed.workspaceId, {
|
|
3066
|
+
type: parsed.type,
|
|
3067
|
+
name: parsed.name,
|
|
3068
|
+
config: parsed.config,
|
|
3069
|
+
capabilities: parsed.capabilities,
|
|
3070
|
+
exposure: parsed.exposure,
|
|
3071
|
+
authStrategy: parsed.authStrategy,
|
|
3072
|
+
});
|
|
3073
|
+
return formatResult({
|
|
3074
|
+
message: "Connector registered. Next, bind it to an owner with nestr_bind_connector (e.g. a role's domain), then a human or agent connects the account via the credentials field's Connect button.",
|
|
3075
|
+
connector,
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
case "nestr_bind_connector": {
|
|
3079
|
+
const parsed = schemas.bindConnector.parse(args);
|
|
3080
|
+
const connection = await client.bindConnector(parsed.workspaceId, {
|
|
3081
|
+
connectorId: parsed.connectorId,
|
|
3082
|
+
owner: { type: parsed.ownerType, id: parsed.ownerId },
|
|
3083
|
+
});
|
|
3084
|
+
// For a role-domain binding the API materialises a credentials field on
|
|
3085
|
+
// the domain; surface that explicitly so the caller knows the Connect
|
|
3086
|
+
// button now renders there and the secret is captured out-of-band.
|
|
3087
|
+
const message = parsed.ownerType === "role-domain"
|
|
3088
|
+
? "Connector bound to the role's domain. A credentials field was materialised on the domain (see credentialsField) so the role can use the connector and the Connect button renders. A human or agent now connects the account via that button; the secret is captured out-of-band and never by the agent."
|
|
3089
|
+
: "Connector bound to the owner. The owner now connects the account out-of-band; the secret is never seen by the agent.";
|
|
3090
|
+
return formatResult({ message, connection });
|
|
3091
|
+
}
|
|
3092
|
+
case "nestr_get_nest_files": {
|
|
3093
|
+
const parsed = schemas.getNestFiles.parse(args);
|
|
3094
|
+
const files = await client.getNestFiles(parsed.nestId);
|
|
3095
|
+
if (files.length === 0) {
|
|
3096
|
+
return { content: [{ type: "text", text: `No file attachments on ${parsed.nestId}.` }] };
|
|
3097
|
+
}
|
|
3098
|
+
const lines = files.map((f) => `- ${f.id} — ${f.name} (${f.contentType}, ${formatBytes(f.size)})`);
|
|
3099
|
+
const text = `${files.length} file${files.length === 1 ? "" : "s"} attached to ${parsed.nestId}. Read one with nestr_read_file({ nestId: "${parsed.nestId}", fileId: "<id>" }).\n\n${lines.join("\n")}`;
|
|
3100
|
+
return { content: [{ type: "text", text }] };
|
|
3101
|
+
}
|
|
3102
|
+
case "nestr_read_file": {
|
|
3103
|
+
const parsed = schemas.readFile.parse(args);
|
|
3104
|
+
const file = await client.getNestFile(parsed.nestId, parsed.fileId);
|
|
3105
|
+
const contentType = file.contentType || "application/octet-stream";
|
|
3106
|
+
// Images: return as a viewable image content item (mirrors nestr_help).
|
|
3107
|
+
// Guard the size first — an image over MAX_IMAGE_INLINE_BYTES exceeds the
|
|
3108
|
+
// per-image limit of the model API the agent forwards it to, so inlining
|
|
3109
|
+
// the base64 would just make that call fail. Degrade to a metadata note.
|
|
3110
|
+
if (contentType.startsWith("image/")) {
|
|
3111
|
+
// Not `??`: size can legitimately be 0 when a legacy file's metadata was
|
|
3112
|
+
// lost (the API descriptor falls back to 0), and there we want the real
|
|
3113
|
+
// byte length so the cap still applies. `> 0` recomputes on a zero/missing
|
|
3114
|
+
// size; trusting a 0 would skip the guard on a large legacy image.
|
|
3115
|
+
const imageBytes = file.size > 0 ? file.size : Buffer.byteLength(file.dataBase64, "base64");
|
|
3116
|
+
if (imageBytes > MAX_IMAGE_INLINE_BYTES) {
|
|
3117
|
+
return {
|
|
3118
|
+
content: [
|
|
3119
|
+
{
|
|
3120
|
+
type: "text",
|
|
3121
|
+
text: `File: ${file.name}\nContent type: ${contentType}\nSize: ${formatBytes(imageBytes)}\n\nThis image is too large to inline (limit ${formatBytes(MAX_IMAGE_INLINE_BYTES)}). Download it directly to view the content.`,
|
|
3122
|
+
},
|
|
3123
|
+
],
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
return {
|
|
3127
|
+
content: [
|
|
3128
|
+
{ type: "text", text: `File: ${file.name} (${contentType}, ${formatBytes(file.size)})` },
|
|
3129
|
+
{ type: "image", data: file.dataBase64, mimeType: contentType },
|
|
3130
|
+
],
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
// JSON / text / CSV: decode to UTF-8 and return as a text block.
|
|
3134
|
+
if (contentType.startsWith("text/") ||
|
|
3135
|
+
contentType.startsWith("application/json")) {
|
|
3136
|
+
const decoded = Buffer.from(file.dataBase64, "base64").toString("utf-8");
|
|
3137
|
+
const { text: body, truncated } = capText(decoded);
|
|
3138
|
+
const note = truncated
|
|
3139
|
+
? `\n\n_(truncated — file is ${formatBytes(file.size)}; showing the first ${MAX_TEXT_FILE_CHARS} characters)_`
|
|
3140
|
+
: "";
|
|
3141
|
+
return {
|
|
3142
|
+
content: [
|
|
3143
|
+
{ type: "text", text: `File: ${file.name} (${contentType}, ${formatBytes(file.size)})\n\n${body}${note}` },
|
|
3144
|
+
],
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
// PDFs and everything else: metadata only. The ToolResult content type
|
|
3148
|
+
// supports text + image blocks, so a non-image binary can't be inlined yet.
|
|
3149
|
+
return {
|
|
3150
|
+
content: [
|
|
3151
|
+
{
|
|
3152
|
+
type: "text",
|
|
3153
|
+
text: `File: ${file.name}\nContent type: ${contentType}\nSize: ${formatBytes(file.size)}\n\nThis file type can't be inlined yet — only images (returned as viewable image content) and text/JSON (returned as decoded text) are supported.`,
|
|
3154
|
+
},
|
|
3155
|
+
],
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
case "nestr_upload_file": {
|
|
3159
|
+
const parsed = schemas.uploadFile.parse(args);
|
|
3160
|
+
// The REST API takes base64 bytes; base64-encode the text convenience here
|
|
3161
|
+
// so the model can hand over content it authored without encoding it itself.
|
|
3162
|
+
const dataBase64 = parsed.dataBase64 ?? Buffer.from(parsed.content ?? "", "utf-8").toString("base64");
|
|
3163
|
+
const file = await client.createNestFile(parsed.nestId, {
|
|
3164
|
+
name: parsed.name,
|
|
3165
|
+
contentType: parsed.contentType,
|
|
3166
|
+
dataBase64,
|
|
3167
|
+
});
|
|
3168
|
+
return formatResult({
|
|
3169
|
+
message: `Uploaded ${file.name} (${formatBytes(file.size)}) to ${parsed.nestId}.`,
|
|
3170
|
+
file,
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
case "nestr_delete_file": {
|
|
3174
|
+
const parsed = schemas.deleteFile.parse(args);
|
|
3175
|
+
await client.deleteNestFile(parsed.nestId, parsed.fileId);
|
|
3176
|
+
return formatResult({ message: `Deleted file ${parsed.fileId} from ${parsed.nestId}.` });
|
|
3177
|
+
}
|
|
2736
3178
|
default:
|
|
2737
3179
|
return formatError({
|
|
2738
3180
|
error: true,
|
|
@@ -2801,6 +3243,34 @@ function buildDiagnoseHint(snapshot) {
|
|
|
2801
3243
|
}
|
|
2802
3244
|
return "Server-side state looks healthy. If a tool is failing, include sessionCorrelationId in the bug report.";
|
|
2803
3245
|
}
|
|
3246
|
+
// Cap on decoded text-file content returned by nestr_read_file, to bound token
|
|
3247
|
+
// cost on large files. ~200 KB of characters.
|
|
3248
|
+
const MAX_TEXT_FILE_CHARS = 200_000;
|
|
3249
|
+
/** Truncate decoded text to MAX_TEXT_FILE_CHARS, flagging whether it was cut. */
|
|
3250
|
+
function capText(text) {
|
|
3251
|
+
if (text.length <= MAX_TEXT_FILE_CHARS)
|
|
3252
|
+
return { text, truncated: false };
|
|
3253
|
+
return { text: text.slice(0, MAX_TEXT_FILE_CHARS), truncated: true };
|
|
3254
|
+
}
|
|
3255
|
+
// Cap on an image returned inline by nestr_read_file. Above this the base64 blob
|
|
3256
|
+
// exceeds the ~5MB-per-image limit of the model API the agent forwards it to, so
|
|
3257
|
+
// we return metadata instead of an image block that call would reject.
|
|
3258
|
+
const MAX_IMAGE_INLINE_BYTES = 5 * 1024 * 1024;
|
|
3259
|
+
/** Human-readable byte size (e.g. "12.3 KB"). */
|
|
3260
|
+
function formatBytes(bytes) {
|
|
3261
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
3262
|
+
return "unknown size";
|
|
3263
|
+
if (bytes < 1024)
|
|
3264
|
+
return `${bytes} B`;
|
|
3265
|
+
const units = ["KB", "MB", "GB"];
|
|
3266
|
+
let size = bytes / 1024;
|
|
3267
|
+
let unit = 0;
|
|
3268
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
3269
|
+
size /= 1024;
|
|
3270
|
+
unit++;
|
|
3271
|
+
}
|
|
3272
|
+
return `${size.toFixed(1)} ${units[unit]}`;
|
|
3273
|
+
}
|
|
2804
3274
|
function formatResult(data) {
|
|
2805
3275
|
return {
|
|
2806
3276
|
content: [
|