neoagent 2.1.18-beta.10 → 2.1.18-beta.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.1.18-beta.10",
3
+ "version": "2.1.18-beta.11",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"425cfb54d01a9472b3e81d9e76fd63a4a44cfb
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "4008655561" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "126115739" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -258,7 +258,7 @@ function parseMaybeJson(value, fallback = null) {
258
258
  }
259
259
  }
260
260
 
261
- function classifyToolExecution(toolName, result, errorMessage = '') {
261
+ function classifyToolExecution(toolName, toolArgs = {}, result, errorMessage = '') {
262
262
  const name = String(toolName || '');
263
263
  const evidenceRelevantPrefixes = ['browser_', 'android_'];
264
264
  const evidenceRelevantExact = new Set([
@@ -339,7 +339,7 @@ function classifyToolExecution(toolName, result, errorMessage = '') {
339
339
  evidenceRelevant,
340
340
  stateChanged,
341
341
  dependsOnOutput: true,
342
- summary: compactToolResult(name, {}, result || { error: errorMessage || 'Tool failed' }, {
342
+ summary: compactToolResult(name, toolArgs, result || { error: errorMessage || 'Tool failed' }, {
343
343
  softLimit: 500,
344
344
  hardLimit: 900,
345
345
  }),
@@ -1396,7 +1396,7 @@ class AgentEngine {
1396
1396
  );
1397
1397
  }
1398
1398
 
1399
- toolExecutions.push(classifyToolExecution(toolName, toolResult, toolErrorMessage));
1399
+ toolExecutions.push(classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage));
1400
1400
  this.persistRunMetadata(runId, {
1401
1401
  evidenceSources: [...new Set(toolExecutions.map((item) => item.evidenceSource).filter(Boolean))],
1402
1402
  subagentState: this.listSubagents(runId),
@@ -1427,6 +1427,20 @@ class AgentEngine {
1427
1427
  });
1428
1428
  }
1429
1429
 
1430
+ if (
1431
+ toolName === 'execute_command'
1432
+ && !toolErrorMessage
1433
+ && !toolResult?.timedOut
1434
+ && !toolResult?.killed
1435
+ && toolResult?.exitCode !== undefined
1436
+ && toolResult.exitCode !== 0
1437
+ ) {
1438
+ messages.push({
1439
+ role: 'system',
1440
+ content: 'The previous shell command exited non-zero. Treat its output as partial evidence only. If it chained multiple shell segments, later segments may not have run. Do not summarize missing sections as observed facts; rerun or verify them separately first.'
1441
+ });
1442
+ }
1443
+
1430
1444
  if (conversationId) {
1431
1445
  db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tool_call_id, name) VALUES (?, ?, ?, ?, ?)')
1432
1446
  .run(conversationId, 'tool', toolMessage.content, toolCall.id, toolName);
@@ -89,8 +89,12 @@ When the system context gives app-level official integration status, trust it ov
89
89
 
90
90
  SHELL COMMANDS
91
91
  When you use execute_command, treat timed out or killed commands as unfinished work, not success. For installs, updates, restarts, config changes, or other state-changing shell actions, verify the outcome with a follow-up command before telling the user it is done.
92
+ When execute_command exits non-zero, treat the output as partial evidence only. If the command chained multiple shell segments, later segments may not have run at all, so do not summarize them as observed facts unless you verified them separately.
92
93
  If you restart or stop the NeoAgent service, this run ends immediately. Warn the user before doing it and say you cannot continue the current run after the restart.
93
94
 
95
+ MESSAGING CLAIMS
96
+ Do not claim a messaging platform is blocked, disconnected, receive-only, or unable to send unless a messaging tool or capability check in this run actually showed that failure. If send_message succeeded, do not describe outbound delivery as blocked.
97
+
94
98
  SKILLS
95
99
  Create or improve a skill only when it is clearly reusable, polished, and likely to matter again. Most completed tasks should not become skills.
96
100
 
@@ -286,6 +286,9 @@ function buildVerifierPrompt({ analysis, toolExecutionSummary, evidenceSources,
286
286
  'Return JSON only. No markdown, no prose, no code fences.',
287
287
  'Verify whether the draft final reply is adequately supported by the gathered evidence.',
288
288
  'If the evidence is insufficient, revise the reply so it states the uncertainty clearly instead of guessing.',
289
+ 'Cross-check every concrete claim against tool status and output. Remove or rewrite claims that are contradicted by the evidence.',
290
+ 'A non-zero execute_command exit code means partial or failed shell evidence. Do not treat later sections of a chained shell command as observed unless they were verified separately.',
291
+ 'A successful send_message or make_call means outbound delivery succeeded in this run unless a later messaging tool failed.',
289
292
  `Freshness risk: ${analysis.freshness_risk}`,
290
293
  `Verification need: ${analysis.verification_need}`,
291
294
  evidenceSources?.length ? `Evidence sources used: ${evidenceSources.join(', ')}` : 'Evidence sources used: none',
@@ -47,14 +47,22 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
47
47
  envelope = trimObject({
48
48
  tool: toolName,
49
49
  status: toolResult?.timedOut ? 'timed_out' : (toolResult?.exitCode === 0 ? 'ok' : 'error'),
50
+ command: clampText(toolArgs.command || '', Math.floor(softLimit * 0.28)),
50
51
  exitCode: toolResult?.exitCode,
51
52
  cwd: toolResult?.cwd || toolArgs.cwd,
52
53
  killed: toolResult?.killed || false,
53
54
  timedOut: toolResult?.timedOut || false,
54
55
  signal: toolResult?.signal,
55
56
  durationMs: toolResult?.durationMs,
57
+ note: toolResult?.timedOut
58
+ ? 'Command timed out. Treat the output as partial.'
59
+ : toolResult?.killed
60
+ ? 'Command was killed. Treat the output as partial.'
61
+ : (toolResult?.exitCode !== undefined && toolResult?.exitCode !== 0)
62
+ ? 'Command exited non-zero. Output may be partial; later segments of a chained shell command may not have run.'
63
+ : '',
56
64
  stdout: lineExcerpt(toolResult?.stdout, 12, Math.floor(softLimit * 0.45)),
57
- stderr: lineExcerpt(toolResult?.stderr, 8, Math.floor(softLimit * 0.25))
65
+ stderr: lineExcerpt(toolResult?.stderr, 10, Math.floor(softLimit * 0.35))
58
66
  });
59
67
  break;
60
68
 
@@ -156,6 +164,27 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
156
164
 
157
165
  case 'send_message':
158
166
  case 'make_call':
167
+ envelope = trimObject({
168
+ tool: toolName,
169
+ status: toolResult?.skipped
170
+ ? 'skipped'
171
+ : (toolResult?.success === false || toolResult?.error ? 'error' : 'ok'),
172
+ platform: toolArgs.platform,
173
+ to: toolArgs.to,
174
+ success: typeof toolResult?.success === 'boolean' ? toolResult.success : undefined,
175
+ skipped: toolResult?.skipped === true ? true : undefined,
176
+ sent: typeof toolResult?.sent === 'boolean' ? toolResult.sent : undefined,
177
+ suppressed: toolResult?.suppressed === true ? true : undefined,
178
+ message: clampText(toolResult?.message || toolResult?.reason || toolResult?.error || '', Math.floor(softLimit * 0.45)),
179
+ result: clampText(JSON.stringify(trimObject({
180
+ id: toolResult?.id,
181
+ key: toolResult?.key,
182
+ deleted: toolResult?.deleted,
183
+ count: Array.isArray(toolResult?.results) ? toolResult.results.length : undefined
184
+ })), Math.floor(softLimit * 0.3))
185
+ });
186
+ break;
187
+
159
188
  case 'memory_save':
160
189
  case 'memory_recall':
161
190
  case 'memory_update_core':
@@ -13,7 +13,109 @@ const FIGMA_APPS = [
13
13
  id: 'figma',
14
14
  label: 'Figma',
15
15
  description: 'Connect a Figma account for future official design and file tools.',
16
- scopes: ['current_user:read'],
16
+ scopes: [
17
+ 'current_user:read',
18
+ 'file_content:read',
19
+ 'file_metadata:read',
20
+ 'file_comments:read',
21
+ 'file_comments:write',
22
+ 'library_content:read',
23
+ 'library_assets:read',
24
+ 'file_dev_resources:read',
25
+ 'file_dev_resources:write',
26
+ ],
27
+ },
28
+ ];
29
+
30
+ const figmaToolDefinitions = [
31
+ {
32
+ appId: 'figma',
33
+ name: 'figma_get_me',
34
+ description: 'Get the current Figma user.',
35
+ parameters: { type: 'object', properties: {} },
36
+ },
37
+ {
38
+ appId: 'figma',
39
+ name: 'figma_get_file',
40
+ description: 'Read a Figma file JSON document.',
41
+ parameters: {
42
+ type: 'object',
43
+ properties: {
44
+ file_key: { type: 'string', description: 'Figma file key.' },
45
+ ids: { type: 'string', description: 'Optional comma-separated node IDs.' },
46
+ depth: { type: 'number', description: 'Optional traversal depth.' },
47
+ },
48
+ required: ['file_key'],
49
+ },
50
+ },
51
+ {
52
+ appId: 'figma',
53
+ name: 'figma_get_file_nodes',
54
+ description: 'Read specific nodes from a Figma file.',
55
+ parameters: {
56
+ type: 'object',
57
+ properties: {
58
+ file_key: { type: 'string', description: 'Figma file key.' },
59
+ ids: { type: 'string', description: 'Comma-separated node IDs.' },
60
+ },
61
+ required: ['file_key', 'ids'],
62
+ },
63
+ },
64
+ {
65
+ appId: 'figma',
66
+ name: 'figma_get_file_images',
67
+ description: 'Render Figma nodes to image URLs.',
68
+ parameters: {
69
+ type: 'object',
70
+ properties: {
71
+ file_key: { type: 'string', description: 'Figma file key.' },
72
+ ids: { type: 'string', description: 'Comma-separated node IDs.' },
73
+ format: { type: 'string', description: 'jpg, png, svg, or pdf. Default png.' },
74
+ scale: { type: 'number', description: 'Optional image scale.' },
75
+ },
76
+ required: ['file_key', 'ids'],
77
+ },
78
+ },
79
+ {
80
+ appId: 'figma',
81
+ name: 'figma_get_comments',
82
+ description: 'List comments on a Figma file.',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: {
86
+ file_key: { type: 'string', description: 'Figma file key.' },
87
+ },
88
+ required: ['file_key'],
89
+ },
90
+ },
91
+ {
92
+ appId: 'figma',
93
+ name: 'figma_post_comment',
94
+ description: 'Post a Figma file comment.',
95
+ parameters: {
96
+ type: 'object',
97
+ properties: {
98
+ file_key: { type: 'string', description: 'Figma file key.' },
99
+ message: { type: 'string', description: 'Comment message.' },
100
+ client_meta: { type: 'object', description: 'Optional Figma comment position metadata.' },
101
+ },
102
+ required: ['file_key', 'message'],
103
+ },
104
+ },
105
+ {
106
+ appId: 'figma',
107
+ name: 'figma_api_request',
108
+ description: 'Make an authenticated Figma REST API request for advanced file, comment, library, variable, webhook, and dev resource operations.',
109
+ parameters: {
110
+ type: 'object',
111
+ properties: {
112
+ method: { type: 'string', description: 'HTTP method: GET, POST, PUT, PATCH, or DELETE.' },
113
+ path: { type: 'string', description: 'Figma API path, e.g. /v1/files/{key}.' },
114
+ query: { type: 'object', description: 'Optional query parameters.' },
115
+ body: { type: 'object', description: 'Optional JSON body.' },
116
+ },
117
+ required: ['method', 'path'],
118
+ },
17
119
  },
18
120
  ];
19
121
 
@@ -28,6 +130,102 @@ function normalizeFigmaUser(profile) {
28
130
  };
29
131
  }
30
132
 
133
+ function requireText(value, label) {
134
+ const text = String(value || '').trim();
135
+ if (!text) throw new Error(`${label} is required.`);
136
+ return text;
137
+ }
138
+
139
+ function figmaUrl(path, query) {
140
+ const url = new URL(
141
+ String(path || '').startsWith('http')
142
+ ? String(path)
143
+ : `https://api.figma.com${String(path || '').startsWith('/') ? '' : '/'}${path}`,
144
+ );
145
+ if (url.hostname !== 'api.figma.com') {
146
+ throw new Error('Figma API request URL must target api.figma.com.');
147
+ }
148
+ for (const [key, value] of Object.entries(query || {})) {
149
+ if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
150
+ }
151
+ return url.toString();
152
+ }
153
+
154
+ async function figmaRequest(credentials, { method = 'GET', path, query, body }) {
155
+ return fetchJson(
156
+ figmaUrl(path, query),
157
+ {
158
+ method: String(method || 'GET').toUpperCase(),
159
+ headers: { Authorization: `Bearer ${credentials.access_token}` },
160
+ ...(body === undefined ? {} : { json: body }),
161
+ },
162
+ { serviceName: 'Figma' },
163
+ );
164
+ }
165
+
166
+ async function executeFigmaTool(toolName, args, { credentials }) {
167
+ switch (toolName) {
168
+ case 'figma_get_me':
169
+ return { result: await figmaRequest(credentials, { path: '/v1/me' }) };
170
+ case 'figma_get_file':
171
+ return {
172
+ result: await figmaRequest(credentials, {
173
+ path: `/v1/files/${encodeURIComponent(requireText(args.file_key, 'file_key'))}`,
174
+ query: {
175
+ ids: args.ids || undefined,
176
+ depth: args.depth || undefined,
177
+ },
178
+ }),
179
+ };
180
+ case 'figma_get_file_nodes':
181
+ return {
182
+ result: await figmaRequest(credentials, {
183
+ path: `/v1/files/${encodeURIComponent(requireText(args.file_key, 'file_key'))}/nodes`,
184
+ query: { ids: requireText(args.ids, 'ids') },
185
+ }),
186
+ };
187
+ case 'figma_get_file_images':
188
+ return {
189
+ result: await figmaRequest(credentials, {
190
+ path: `/v1/images/${encodeURIComponent(requireText(args.file_key, 'file_key'))}`,
191
+ query: {
192
+ ids: requireText(args.ids, 'ids'),
193
+ format: String(args.format || 'png'),
194
+ scale: args.scale || undefined,
195
+ },
196
+ }),
197
+ };
198
+ case 'figma_get_comments':
199
+ return {
200
+ result: await figmaRequest(credentials, {
201
+ path: `/v1/files/${encodeURIComponent(requireText(args.file_key, 'file_key'))}/comments`,
202
+ }),
203
+ };
204
+ case 'figma_post_comment':
205
+ return {
206
+ result: await figmaRequest(credentials, {
207
+ method: 'POST',
208
+ path: `/v1/files/${encodeURIComponent(requireText(args.file_key, 'file_key'))}/comments`,
209
+ body: {
210
+ message: requireText(args.message, 'message'),
211
+ client_meta: args.client_meta || undefined,
212
+ },
213
+ }),
214
+ };
215
+ case 'figma_api_request':
216
+ return {
217
+ result: await figmaRequest(credentials, {
218
+ method: args.method,
219
+ path: requireText(args.path, 'path'),
220
+ query: args.query,
221
+ body: args.body,
222
+ }),
223
+ };
224
+ default:
225
+ return null;
226
+ }
227
+ }
228
+
31
229
  function createFigmaProvider() {
32
230
  return createOAuthProvider({
33
231
  key: 'figma',
@@ -36,6 +234,7 @@ function createFigmaProvider() {
36
234
  'Official Figma OAuth account connections for future design file and collaboration workflows.',
37
235
  icon: 'figma',
38
236
  apps: FIGMA_APPS,
237
+ toolDefinitions: figmaToolDefinitions,
39
238
  connectPrompt:
40
239
  'This enables the official Figma account layer now. Native Figma tools are not shipped yet in this run.',
41
240
  getEnvStatus() {
@@ -58,13 +257,17 @@ function createFigmaProvider() {
58
257
  },
59
258
  async finishOAuth({ code, app }) {
60
259
  const config = resolveFigmaOAuthConfig();
260
+ const basic = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(
261
+ 'base64',
262
+ );
61
263
  const token = await fetchJson(
62
264
  'https://api.figma.com/v1/oauth/token',
63
265
  {
64
266
  method: 'POST',
267
+ headers: {
268
+ Authorization: `Basic ${basic}`,
269
+ },
65
270
  form: {
66
- client_id: config.clientId,
67
- client_secret: config.clientSecret,
68
271
  redirect_uri: config.redirectUri,
69
272
  code,
70
273
  grant_type: 'authorization_code',
@@ -109,6 +312,7 @@ function createFigmaProvider() {
109
312
  },
110
313
  };
111
314
  },
315
+ executeTool: executeFigmaTool,
112
316
  });
113
317
  }
114
318
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { google } = require('googleapis');
4
- const { coerceStringList } = require('./common');
4
+ const { coerceStringList, executeGoogleApiRequest } = require('./common');
5
5
 
6
6
  const calendarToolDefinitions = [
7
7
  {
@@ -117,6 +117,25 @@ const calendarToolDefinitions = [
117
117
  required: ['time_min', 'time_max'],
118
118
  },
119
119
  },
120
+ {
121
+ name: 'google_workspace_calendar_api_request',
122
+ description:
123
+ 'Make an authenticated Google Calendar API request for advanced calendar operations not covered by the dedicated tools.',
124
+ parameters: {
125
+ type: 'object',
126
+ properties: {
127
+ method: { type: 'string', description: 'HTTP method: GET, POST, PUT, PATCH, or DELETE.' },
128
+ path: {
129
+ type: 'string',
130
+ description:
131
+ 'Calendar API path or URL, e.g. /calendar/v3/users/me/calendarList.',
132
+ },
133
+ query: { type: 'object', description: 'Optional query parameters.' },
134
+ body: { type: 'object', description: 'Optional JSON request body.' },
135
+ },
136
+ required: ['method', 'path'],
137
+ },
138
+ },
120
139
  ];
121
140
 
122
141
  function summarizeEvent(event) {
@@ -241,6 +260,12 @@ async function executeCalendarTool(toolName, args, auth) {
241
260
  return { calendars: response.data.calendars || {} };
242
261
  }
243
262
 
263
+ case 'google_workspace_calendar_api_request': {
264
+ return executeGoogleApiRequest(auth, args, {
265
+ baseUrl: 'https://www.googleapis.com',
266
+ });
267
+ }
268
+
244
269
  default:
245
270
  return null;
246
271
  }
@@ -96,9 +96,48 @@ function summarizeFile(file) {
96
96
  };
97
97
  }
98
98
 
99
+ function requireGoogleApiUrl(pathOrUrl, defaultBaseUrl) {
100
+ const raw = String(pathOrUrl || '').trim();
101
+ if (!raw) throw new Error('path is required.');
102
+ const base = String(defaultBaseUrl || 'https://www.googleapis.com').replace(/\/$/, '');
103
+ const url = raw.startsWith('http://') || raw.startsWith('https://')
104
+ ? new URL(raw)
105
+ : new URL(raw.startsWith('/') ? raw : `/${raw}`, base);
106
+ if (!url.hostname.endsWith('googleapis.com')) {
107
+ throw new Error('Google API request URL must target a googleapis.com host.');
108
+ }
109
+ return url.toString();
110
+ }
111
+
112
+ async function executeGoogleApiRequest(auth, args, options = {}) {
113
+ const method = String(args.method || 'GET').trim().toUpperCase();
114
+ const allowedMethods = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
115
+ if (!allowedMethods.has(method)) {
116
+ throw new Error('method must be one of GET, POST, PUT, PATCH, DELETE.');
117
+ }
118
+
119
+ const response = await auth.request({
120
+ url: requireGoogleApiUrl(args.path || args.url, options.baseUrl),
121
+ method,
122
+ params: args.query && typeof args.query === 'object' ? args.query : undefined,
123
+ data: args.body === undefined ? undefined : args.body,
124
+ headers: args.headers && typeof args.headers === 'object' ? args.headers : undefined,
125
+ responseType: args.response_type === 'arraybuffer' ? 'arraybuffer' : undefined,
126
+ });
127
+
128
+ return {
129
+ status: response.status,
130
+ statusText: response.statusText,
131
+ data: Buffer.isBuffer(response.data)
132
+ ? response.data.toString('base64')
133
+ : response.data,
134
+ };
135
+ }
136
+
99
137
  module.exports = {
100
138
  coerceStringList,
101
139
  ensureParentDir,
140
+ executeGoogleApiRequest,
102
141
  extractMessageBody,
103
142
  getHeader,
104
143
  stringToBase64Url,
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { google } = require('googleapis');
4
+ const { executeGoogleApiRequest } = require('./common');
4
5
 
5
6
  const docsToolDefinitions = [
6
7
  {
@@ -51,6 +52,25 @@ const docsToolDefinitions = [
51
52
  required: ['document_id', 'search_text', 'replace_text'],
52
53
  },
53
54
  },
55
+ {
56
+ name: 'google_workspace_docs_api_request',
57
+ description:
58
+ 'Make an authenticated Google Docs API request for advanced document batchUpdate operations not covered by the dedicated tools.',
59
+ parameters: {
60
+ type: 'object',
61
+ properties: {
62
+ method: { type: 'string', description: 'HTTP method: GET, POST, PUT, PATCH, or DELETE.' },
63
+ path: {
64
+ type: 'string',
65
+ description:
66
+ 'Docs API path or URL, e.g. /v1/documents/{documentId}:batchUpdate.',
67
+ },
68
+ query: { type: 'object', description: 'Optional query parameters.' },
69
+ body: { type: 'object', description: 'Optional JSON request body.' },
70
+ },
71
+ required: ['method', 'path'],
72
+ },
73
+ },
54
74
  ];
55
75
 
56
76
  function documentToText(document) {
@@ -176,6 +196,12 @@ async function executeDocsTool(toolName, args, auth) {
176
196
  };
177
197
  }
178
198
 
199
+ case 'google_workspace_docs_api_request': {
200
+ return executeGoogleApiRequest(auth, args, {
201
+ baseUrl: 'https://docs.googleapis.com',
202
+ });
203
+ }
204
+
179
205
  default:
180
206
  return null;
181
207
  }
@@ -3,7 +3,12 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { google } = require('googleapis');
6
- const { coerceStringList, ensureParentDir, summarizeFile } = require('./common');
6
+ const {
7
+ coerceStringList,
8
+ ensureParentDir,
9
+ executeGoogleApiRequest,
10
+ summarizeFile,
11
+ } = require('./common');
7
12
 
8
13
  const driveToolDefinitions = [
9
14
  {
@@ -101,6 +106,25 @@ const driveToolDefinitions = [
101
106
  required: ['file_id'],
102
107
  },
103
108
  },
109
+ {
110
+ name: 'google_workspace_drive_api_request',
111
+ description:
112
+ 'Make an authenticated Google Drive API request for advanced file, folder, permission, comment, revision, and shared drive operations.',
113
+ parameters: {
114
+ type: 'object',
115
+ properties: {
116
+ method: { type: 'string', description: 'HTTP method: GET, POST, PUT, PATCH, or DELETE.' },
117
+ path: {
118
+ type: 'string',
119
+ description:
120
+ 'Drive API path or URL, e.g. /drive/v3/files or /drive/v3/files/{fileId}/comments.',
121
+ },
122
+ query: { type: 'object', description: 'Optional query parameters.' },
123
+ body: { type: 'object', description: 'Optional JSON request body.' },
124
+ },
125
+ required: ['method', 'path'],
126
+ },
127
+ },
104
128
  ];
105
129
 
106
130
  async function validateExistingReadableFilePath(filePath) {
@@ -235,6 +259,12 @@ async function executeDriveTool(toolName, args, auth) {
235
259
  return summarizeFile(fileResponse.data);
236
260
  }
237
261
 
262
+ case 'google_workspace_drive_api_request': {
263
+ return executeGoogleApiRequest(auth, args, {
264
+ baseUrl: 'https://www.googleapis.com',
265
+ });
266
+ }
267
+
238
268
  default:
239
269
  return null;
240
270
  }
@@ -3,6 +3,7 @@
3
3
  const { google } = require('googleapis');
4
4
  const {
5
5
  coerceStringList,
6
+ executeGoogleApiRequest,
6
7
  extractMessageBody,
7
8
  getHeader,
8
9
  stringToBase64Url,
@@ -113,6 +114,25 @@ const gmailToolDefinitions = [
113
114
  required: ['thread_id'],
114
115
  },
115
116
  },
117
+ {
118
+ name: 'google_workspace_gmail_api_request',
119
+ description:
120
+ 'Make an authenticated Gmail API request for advanced Gmail operations not covered by the dedicated Gmail tools.',
121
+ parameters: {
122
+ type: 'object',
123
+ properties: {
124
+ method: { type: 'string', description: 'HTTP method: GET, POST, PUT, PATCH, or DELETE.' },
125
+ path: {
126
+ type: 'string',
127
+ description:
128
+ 'Gmail API path or URL, e.g. /gmail/v1/users/me/settings/sendAs.',
129
+ },
130
+ query: { type: 'object', description: 'Optional query parameters.' },
131
+ body: { type: 'object', description: 'Optional JSON request body.' },
132
+ },
133
+ required: ['method', 'path'],
134
+ },
135
+ },
116
136
  ];
117
137
 
118
138
  function summarizeMessage(message) {
@@ -294,6 +314,12 @@ async function executeGmailTool(toolName, args, auth) {
294
314
  };
295
315
  }
296
316
 
317
+ case 'google_workspace_gmail_api_request': {
318
+ return executeGoogleApiRequest(auth, args, {
319
+ baseUrl: 'https://gmail.googleapis.com',
320
+ });
321
+ }
322
+
297
323
  default:
298
324
  return null;
299
325
  }
@@ -21,7 +21,7 @@ const GOOGLE_WORKSPACE_APPS = [
21
21
  id: 'gmail',
22
22
  label: 'Gmail',
23
23
  description: 'Search threads, read messages, send mail, and manage labels.',
24
- scopes: ['https://www.googleapis.com/auth/gmail.modify'],
24
+ scopes: ['https://mail.google.com/'],
25
25
  toolDefinitions: gmailToolDefinitions,
26
26
  executor: executeGmailTool,
27
27
  },