@notis_ai/cli 0.2.0-beta.16.1 → 0.2.0-beta.160.1

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 (154) hide show
  1. package/README.md +433 -133
  2. package/config/notis_app_boundary_rules.json +50 -0
  3. package/config/notis_app_design_rules.json +135 -0
  4. package/dist/agent-hooks/notis-agent-hook.mjs +18672 -0
  5. package/dist/base-skills/notis-apps/SKILL.md +70 -0
  6. package/dist/base-skills/notis-apps/references/architecture.md +164 -0
  7. package/dist/base-skills/notis-apps/references/context.md +81 -0
  8. package/dist/base-skills/notis-apps/references/design.md +165 -0
  9. package/dist/base-skills/notis-apps/references/reading.md +89 -0
  10. package/dist/base-skills/notis-apps/references/release.md +99 -0
  11. package/dist/base-skills/notis-apps/references/sdk.md +62 -0
  12. package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +140 -0
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
  15. package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  17. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  18. package/dist/base-skills/notis-query/SKILL.md +67 -0
  19. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  20. package/dist/base-skills/notis-query/references/documents.md +50 -0
  21. package/dist/base-skills/notis-query/references/query.md +543 -0
  22. package/dist/skill-sync/index.js +1626 -0
  23. package/dist/skill-sync/index.js.map +7 -0
  24. package/dist/skill-sync-worker.mjs +2990 -0
  25. package/package.json +16 -6
  26. package/skills/notis-apps/cli.md +313 -0
  27. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  28. package/skills/notis-onboarding/BRIEF.md +129 -0
  29. package/skills/notis-query/cli.md +39 -0
  30. package/src/agent-hook-entry.js +5 -0
  31. package/src/cli.js +294 -25
  32. package/src/command-specs/agents.js +392 -0
  33. package/src/command-specs/apps.js +1470 -202
  34. package/src/command-specs/auth.js +114 -137
  35. package/src/command-specs/diagnostics.js +716 -0
  36. package/src/command-specs/handover.js +374 -0
  37. package/src/command-specs/helpers.js +84 -82
  38. package/src/command-specs/index.js +25 -6
  39. package/src/command-specs/meta.js +150 -18
  40. package/src/command-specs/onboarding.js +290 -0
  41. package/src/command-specs/profile.js +358 -0
  42. package/src/command-specs/reports.js +86 -0
  43. package/src/command-specs/skills.js +75 -0
  44. package/src/command-specs/smoke.js +386 -0
  45. package/src/command-specs/tools.js +455 -139
  46. package/src/runtime/agent-browser.js +632 -0
  47. package/src/runtime/agent-memory-state.js +126 -0
  48. package/src/runtime/agent-setup.js +383 -0
  49. package/src/runtime/app-boundary-validator.js +404 -0
  50. package/src/runtime/app-changelog.js +79 -0
  51. package/src/runtime/app-platform.js +2633 -210
  52. package/src/runtime/app-registry-scaffolds.js +367 -0
  53. package/src/runtime/app-test-server.js +292 -0
  54. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  55. package/src/runtime/auth-recovery.js +110 -0
  56. package/src/runtime/base-skills.d.ts +20 -0
  57. package/src/runtime/base-skills.js +167 -0
  58. package/src/runtime/channel.js +133 -0
  59. package/src/runtime/delegated-context.js +68 -0
  60. package/src/runtime/errors.js +1 -0
  61. package/src/runtime/git.js +233 -0
  62. package/src/runtime/login-listener.js +15 -0
  63. package/src/runtime/oauth.js +2622 -0
  64. package/src/runtime/output.js +37 -5
  65. package/src/runtime/ports.js +31 -0
  66. package/src/runtime/profiles.js +906 -55
  67. package/src/runtime/skill-sync/cloud-client.ts +99 -0
  68. package/src/runtime/skill-sync/index.ts +697 -0
  69. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  70. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  71. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  72. package/src/runtime/skill-sync/types.ts +110 -0
  73. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  74. package/src/runtime/skill-sync-service.js +109 -0
  75. package/src/runtime/store-screenshot.js +143 -0
  76. package/src/runtime/sync-skills.d.ts +37 -0
  77. package/src/runtime/sync-skills.js +231 -0
  78. package/src/runtime/telemetry.js +92 -0
  79. package/src/runtime/transport.js +324 -45
  80. package/src/skill-sync-worker-entry.js +2 -0
  81. package/src/skill-sync-worker.js +50 -0
  82. package/template/.harness/index.html.tmpl +430 -0
  83. package/template/CHANGELOG.md +5 -0
  84. package/template/app/layout.tsx +5 -2
  85. package/template/app/page.tsx +49 -42
  86. package/template/components/page-heading.tsx +23 -0
  87. package/template/components/ui/badge.tsx +7 -4
  88. package/template/components/ui/card.tsx +24 -11
  89. package/template/components/ui/native-select.tsx +24 -0
  90. package/template/notis.config.ts +24 -6
  91. package/template/package-lock.json +4137 -0
  92. package/template/package.json +5 -5
  93. package/template/packages/{notis-sdk → sdk}/package.json +13 -3
  94. package/template/packages/sdk/src/agentContext.ts +36 -0
  95. package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
  96. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  97. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  98. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  99. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  100. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  101. package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  102. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  103. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  104. package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
  105. package/template/packages/sdk/src/config.ts +257 -0
  106. package/template/packages/sdk/src/documents.ts +256 -0
  107. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  108. package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
  109. package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
  110. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
  111. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  112. package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  113. package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
  114. package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
  115. package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
  116. package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
  117. package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
  118. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
  119. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
  120. package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
  121. package/template/packages/sdk/src/hooks/useTool.ts +65 -0
  122. package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
  123. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
  124. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  125. package/template/packages/sdk/src/index.ts +161 -0
  126. package/template/packages/sdk/src/interactions/actions.ts +59 -0
  127. package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
  128. package/template/packages/sdk/src/interactions/visibility.ts +13 -0
  129. package/template/packages/sdk/src/interactions.ts +45 -0
  130. package/template/packages/sdk/src/provider.tsx +44 -0
  131. package/template/packages/sdk/src/queryCache.ts +170 -0
  132. package/template/packages/sdk/src/runtime.ts +451 -0
  133. package/template/packages/sdk/src/styles.css +213 -0
  134. package/template/packages/sdk/src/tailwind.ts +56 -0
  135. package/template/packages/{notis-sdk → sdk}/src/vite.ts +5 -1
  136. package/template/tailwind.config.ts +1 -0
  137. package/src/command-specs/db.js +0 -163
  138. package/src/runtime/app-preview-server.js +0 -312
  139. package/template/packages/notis-sdk/src/config.ts +0 -48
  140. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  141. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  142. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  143. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  144. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  145. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  146. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  147. package/template/packages/notis-sdk/src/index.ts +0 -47
  148. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  149. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  150. package/template/packages/notis-sdk/src/styles.css +0 -123
  151. /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
  152. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
  153. /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
  154. /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
@@ -1,17 +1,24 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { accessSync, constants as fsConstants, createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { basename } from 'node:path';
2
4
  import { usageError } from '../runtime/errors.js';
5
+ import { assertToolNotDelegated } from '../runtime/delegated-context.js';
3
6
  import {
7
+ COMPOSIO_GET_TOOL_SCHEMAS,
8
+ COMPOSIO_MULTI_EXECUTE_TOOL,
9
+ COMPOSIO_SEARCH_TOOLS,
10
+ fetchToolDiscovery,
4
11
  fetchToolkits,
5
12
  fetchToolSchema,
13
+ localNotisToolSlug,
6
14
  nextIdempotencyKey,
7
15
  parseJson,
8
16
  parseMaybeJson,
9
- resolveSearchToolkits,
10
17
  runToolCommand,
11
18
  validateArguments,
12
19
  } from './helpers.js';
13
20
 
14
- async function resolveJsonInput(value, label) {
21
+ export async function resolveJsonInput(value, label) {
15
22
  if (!value) return undefined;
16
23
 
17
24
  if (value === '-') {
@@ -33,6 +40,263 @@ async function resolveJsonInput(value, label) {
33
40
  return parseJson(value, label);
34
41
  }
35
42
 
43
+ const READ_ACTIONS = new Set([
44
+ 'CHECK',
45
+ 'DESCRIBE',
46
+ 'DOWNLOAD',
47
+ 'FETCH',
48
+ 'FIND',
49
+ 'GET',
50
+ 'INSPECT',
51
+ 'LIST',
52
+ 'LOOKUP',
53
+ 'PREVIEW',
54
+ 'QUERY',
55
+ 'READ',
56
+ 'SEARCH',
57
+ 'STATUS',
58
+ 'VERIFY',
59
+ ]);
60
+ const WRITE_ACTIONS = new Set([
61
+ 'AUTHENTIFY',
62
+ 'CANCEL',
63
+ 'CONNECT',
64
+ 'COPY',
65
+ 'CREATE',
66
+ 'DELETE',
67
+ 'DEPLOY',
68
+ 'DISCARD',
69
+ 'EDIT',
70
+ 'GRANT',
71
+ 'INCREMENT',
72
+ 'INSERT',
73
+ 'LINK',
74
+ 'MOVE',
75
+ 'POST',
76
+ 'PUBLISH',
77
+ 'REFUND',
78
+ 'REMOVE',
79
+ 'RENAME',
80
+ 'SAVE',
81
+ 'SEND',
82
+ 'SUBMIT',
83
+ 'UPDATE',
84
+ 'UPLOAD',
85
+ 'UPSERT',
86
+ ]);
87
+
88
+ function splitSqlStatements(query) {
89
+ const statements = [];
90
+ let current = '';
91
+ let quote = null;
92
+ let dollarTag = null;
93
+ for (let index = 0; index < query.length; index += 1) {
94
+ const char = query[index];
95
+ if (dollarTag) {
96
+ if (query.startsWith(dollarTag, index)) {
97
+ current += dollarTag;
98
+ index += dollarTag.length - 1;
99
+ dollarTag = null;
100
+ } else {
101
+ current += char;
102
+ }
103
+ continue;
104
+ }
105
+ if (quote) {
106
+ current += char;
107
+ if (char === quote) {
108
+ if (query[index + 1] === quote) {
109
+ current += query[index + 1];
110
+ index += 1;
111
+ } else {
112
+ quote = null;
113
+ }
114
+ }
115
+ continue;
116
+ }
117
+ if (char === "'" || char === '"') {
118
+ quote = char;
119
+ current += char;
120
+ continue;
121
+ }
122
+ if (char === '$') {
123
+ const match = query.slice(index).match(/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/);
124
+ if (match) {
125
+ dollarTag = match[0];
126
+ current += dollarTag;
127
+ index += dollarTag.length - 1;
128
+ continue;
129
+ }
130
+ }
131
+ if (char === ';') {
132
+ if (current.trim()) statements.push(current.trim());
133
+ current = '';
134
+ continue;
135
+ }
136
+ current += char;
137
+ }
138
+ if (current.trim()) statements.push(current.trim());
139
+ return statements;
140
+ }
141
+
142
+ function classifySqlMutation(query) {
143
+ const statements = splitSqlStatements(query);
144
+ if (statements.length > 1) {
145
+ const classifications = statements.map(classifySqlMutation);
146
+ if (classifications.includes(true)) return true;
147
+ if (classifications.every((classification) => classification === false)) return false;
148
+ return null;
149
+ }
150
+ const statement = statements[0] || '';
151
+ if (statement) {
152
+ const normalizedStatement = statement.toUpperCase();
153
+ if (/^EXPLAIN\b/.test(normalizedStatement)) {
154
+ const executes = /^EXPLAIN\s+(?:\([^)]*\bANALYZE\b[^)]*\)\s*|ANALYZE\s+)/.test(normalizedStatement);
155
+ if (!executes) return false;
156
+ return /\b(ALTER|CREATE|DELETE|DROP|GRANT|INSERT|MERGE|REVOKE|TRUNCATE|UPDATE)\b/.test(normalizedStatement);
157
+ }
158
+ if (/^SHOW\b/.test(normalizedStatement)) return false;
159
+ // PostgreSQL SELECT can call side-effecting functions. Without catalog
160
+ // knowledge it is not provably read-only, so preserve idempotency and report
161
+ // an unknown classification instead of claiming mutating=false.
162
+ if (/^SELECT\b/.test(normalizedStatement)) return null;
163
+ if (/^WITH\b/.test(normalizedStatement)) {
164
+ return /\b(ALTER|CREATE|DELETE|DROP|GRANT|INSERT|MERGE|REVOKE|TRUNCATE|UPDATE)\b/.test(normalizedStatement)
165
+ ? true
166
+ : null;
167
+ }
168
+ if (/^(ALTER|CREATE|DELETE|DROP|GRANT|INSERT|MERGE|REVOKE|TRUNCATE|UPDATE)\b/.test(normalizedStatement)) {
169
+ return true;
170
+ }
171
+ }
172
+ return null;
173
+ }
174
+
175
+ export function classifyToolMutation(toolName, args = {}) {
176
+ const normalized = localNotisToolSlug(toolName).toUpperCase();
177
+ if (normalized === 'LOCAL_NOTIS_SANDBOX_PREVIEW') {
178
+ if (args.action === 'status') return false;
179
+ if (['register', 'open', 'heartbeat', 'stop'].includes(args.action)) return true;
180
+ return null;
181
+ }
182
+ if (normalized.includes('EXECUTE_SQL') && typeof args.query === 'string') {
183
+ const query = args.query
184
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
185
+ .replace(/--[^\n]*/g, ' ')
186
+ .trim();
187
+ const sqlClassification = classifySqlMutation(query);
188
+ if (sqlClassification !== null) return sqlClassification;
189
+ }
190
+ if (/(?:^|_)(?:FIND|GET|LOOKUP|SEARCH)_OR_(?:CREATE|INSERT|SAVE|UPDATE|UPSERT)(?:_|$)/.test(normalized)) {
191
+ return true;
192
+ }
193
+ const tokens = normalized.split(/[^A-Z0-9]+/).filter(Boolean);
194
+ for (const token of tokens) {
195
+ if (WRITE_ACTIONS.has(token)) return true;
196
+ if (READ_ACTIONS.has(token)) return false;
197
+ }
198
+ return null;
199
+ }
200
+
201
+ const LONG_RUNNING_TOOL_TIMEOUT_MS = 600000;
202
+ const LONG_RUNNING_TOOL_NAMES = new Set([
203
+ 'LOCAL_NOTIS_GENERATE_IMAGE_OPENAI',
204
+ 'LOCAL_NOTIS_EDIT_IMAGE_OPENAI',
205
+ 'LOCAL_NOTIS_GENERATE_IMAGE_GEMINI',
206
+ 'LOCAL_NOTIS_EDIT_IMAGE_GEMINI',
207
+ ]);
208
+
209
+ const EXTENSION_CONTENT_TYPES = new Map([
210
+ ['.pdf', 'application/pdf'],
211
+ ['.png', 'image/png'],
212
+ ['.jpg', 'image/jpeg'],
213
+ ['.jpeg', 'image/jpeg'],
214
+ ['.gif', 'image/gif'],
215
+ ['.webp', 'image/webp'],
216
+ ['.txt', 'text/plain'],
217
+ ['.csv', 'text/csv'],
218
+ ['.json', 'application/json'],
219
+ ['.md', 'text/markdown'],
220
+ ]);
221
+
222
+ function guessContentType(filePath) {
223
+ const lower = filePath.toLowerCase();
224
+ const dotIndex = lower.lastIndexOf('.');
225
+ if (dotIndex === -1) return 'application/octet-stream';
226
+ return EXTENSION_CONTENT_TYPES.get(lower.slice(dotIndex)) || 'application/octet-stream';
227
+ }
228
+
229
+ export async function hashFileSha256(localPath) {
230
+ const hash = createHash('sha256');
231
+ for await (const chunk of createReadStream(localPath)) {
232
+ hash.update(chunk);
233
+ }
234
+ return hash.digest('hex');
235
+ }
236
+
237
+ async function parseFileBindingSpec(spec, index) {
238
+ if (typeof spec !== 'string' || !spec.trim()) {
239
+ throw usageError('--file must be formatted as <argument-path>=<local-path>');
240
+ }
241
+
242
+ const equalsIndex = spec.indexOf('=');
243
+ if (equalsIndex <= 0 || equalsIndex === spec.length - 1) {
244
+ throw usageError('--file must be formatted as <argument-path>=<local-path>');
245
+ }
246
+
247
+ const argumentPath = spec.slice(0, equalsIndex).trim();
248
+ const localPath = spec.slice(equalsIndex + 1).trim();
249
+ if (!argumentPath) {
250
+ throw usageError('--file argument path is required');
251
+ }
252
+ if (!localPath) {
253
+ throw usageError('--file local path is required');
254
+ }
255
+ if (!existsSync(localPath)) {
256
+ throw usageError(`File not found: ${localPath}`);
257
+ }
258
+
259
+ let stats;
260
+ try {
261
+ stats = statSync(localPath);
262
+ accessSync(localPath, fsConstants.R_OK);
263
+ } catch {
264
+ throw usageError(`File is not readable: ${localPath}`);
265
+ }
266
+ if (!stats.isFile()) {
267
+ throw usageError(`File is not a regular file: ${localPath}`);
268
+ }
269
+
270
+ const sha256 = await hashFileSha256(localPath);
271
+ const fileBasename = basename(localPath);
272
+ const fieldName = `file_${index}`;
273
+
274
+ return {
275
+ argument_path: argumentPath,
276
+ field_name: fieldName,
277
+ basename: fileBasename,
278
+ size: stats.size,
279
+ sha256,
280
+ contentType: guessContentType(fileBasename),
281
+ localPath,
282
+ };
283
+ }
284
+
285
+ export async function parseFileBindings(rawFileOptions) {
286
+ const values = Array.isArray(rawFileOptions)
287
+ ? rawFileOptions
288
+ : (rawFileOptions ? [rawFileOptions] : []);
289
+ return Promise.all(values.map((value, index) => parseFileBindingSpec(value, index)));
290
+ }
291
+
292
+ function ensureLongRunningToolTimeout(ctx, toolNames) {
293
+ const names = Array.isArray(toolNames) ? toolNames : [toolNames];
294
+ if (!names.some((name) => LONG_RUNNING_TOOL_NAMES.has(localNotisToolSlug(name)))) {
295
+ return;
296
+ }
297
+ ctx.runtime.timeoutMs = Math.max(ctx.runtime.timeoutMs || 0, LONG_RUNNING_TOOL_TIMEOUT_MS);
298
+ }
299
+
36
300
  async function toolsToolkitsHandler(ctx) {
37
301
  const toolkits = await fetchToolkits(ctx.runtime);
38
302
  return ctx.output.emitSuccess({
@@ -40,27 +304,55 @@ async function toolsToolkitsHandler(ctx) {
40
304
  data: { toolkits },
41
305
  humanSummary: `Found ${toolkits.length} toolkits`,
42
306
  hints: [
43
- { command: 'notis tools search <query>', reason: 'Search for tools in these toolkits' },
307
+ { command: 'notis tools search <query>', reason: 'Search canonical tools and schemas' },
44
308
  ],
45
- renderHuman: () => toolkits.map((entry) => `${entry.id} ${entry.description}`).join('\n'),
309
+ renderHuman: () =>
310
+ toolkits
311
+ .map((entry) => {
312
+ const status = entry.has_active_connection ? 'connected' : 'needs auth';
313
+ return `${entry.id} ${status} ${entry.description}`;
314
+ })
315
+ .join('\n'),
46
316
  });
47
317
  }
48
318
 
319
+ function discoveryToolRows(payload) {
320
+ const schemas = payload.tool_schemas || {};
321
+ const statuses = new Map(
322
+ (payload.toolkit_connection_statuses || [])
323
+ .filter((entry) => entry && typeof entry.toolkit === 'string')
324
+ .map((entry) => [entry.toolkit, entry])
325
+ );
326
+ const seen = new Set();
327
+ const rows = [];
328
+ for (const result of payload.results || []) {
329
+ for (const name of [...(result.primary_tool_slugs || []), ...(result.related_tool_slugs || [])]) {
330
+ if (!name || seen.has(name)) continue;
331
+ seen.add(name);
332
+ const schema = schemas[name] || {};
333
+ const toolkit = schema.toolkit || name.split('-').slice(0, -1).join('-');
334
+ const status = statuses.get(toolkit);
335
+ rows.push({
336
+ name,
337
+ toolkit,
338
+ description: schema.description || '',
339
+ schema_available: Boolean(schemas[name]),
340
+ connection_status: status
341
+ ? (status.has_active_connection ? 'connected' : 'needs auth')
342
+ : 'unknown',
343
+ });
344
+ }
345
+ }
346
+ return rows;
347
+ }
348
+
49
349
  async function toolsSearchHandler(ctx) {
50
- const toolkits = await resolveSearchToolkits(ctx.runtime, ctx.options.toolkits);
51
- const result = await runToolCommand({
52
- runtime: ctx.runtime,
53
- toolName: 'notis_find_tools',
54
- arguments_: {
55
- query: ctx.args.query || undefined,
56
- toolkits,
57
- },
58
- });
59
- const tools = result.payload.tools || [];
350
+ const payload = await fetchToolDiscovery(ctx.runtime, ctx.args.query || 'Find tools', ctx.options.knownFields || '');
351
+ const tools = discoveryToolRows(payload);
60
352
  const firstTool = tools[0];
61
353
  return ctx.output.emitSuccess({
62
354
  command: ctx.spec.command_path.join(' '),
63
- data: { toolkits, tools },
355
+ data: payload,
64
356
  humanSummary: `Found ${tools.length} tools`,
65
357
  hints: firstTool
66
358
  ? [
@@ -70,70 +362,79 @@ async function toolsSearchHandler(ctx) {
70
362
  : [],
71
363
  renderHuman: () =>
72
364
  tools
73
- .map((tool) => `${tool.name}\n ${tool.toolkit_id || 'unknown toolkit'}\n ${tool.description || 'No description'}\n`)
365
+ .map((tool) => `${tool.name}\n toolkit: ${tool.toolkit || 'unknown'} (${tool.connection_status})\n schema: ${tool.schema_available ? 'available' : 'missing'}\n ${tool.description || 'No description'}\n`)
74
366
  .join('\n')
75
367
  .trim(),
76
368
  });
77
369
  }
78
370
 
79
371
  async function toolsDescribeHandler(ctx) {
80
- const toolkits = await resolveSearchToolkits(ctx.runtime, ctx.options.toolkits);
81
- const result = await runToolCommand({
82
- runtime: ctx.runtime,
83
- toolName: 'notis_find_tools',
84
- arguments_: {
85
- query: ctx.args.toolName,
86
- toolkits,
87
- },
88
- });
89
- const tools = result.payload.tools || [];
90
- const match = tools.find((tool) => tool.name === ctx.args.toolName);
91
- if (!match) {
92
- throw usageError(`Tool ${ctx.args.toolName} was not found in the selected toolkits.`);
93
- }
372
+ const requestedToolName = localNotisToolSlug(ctx.args.toolName);
373
+ const match = await fetchToolSchema(ctx.runtime, requestedToolName);
94
374
 
95
375
  return ctx.output.emitSuccess({
96
376
  command: ctx.spec.command_path.join(' '),
97
- data: { tool: match, toolkits },
98
- humanSummary: `Described tool ${ctx.args.toolName}`,
377
+ data: { tool: match },
378
+ humanSummary: `Described tool ${requestedToolName}`,
99
379
  hints: [
100
- { command: `notis tools exec ${ctx.args.toolName} --dry-run --arguments '{}'`, reason: 'Validate arguments first' },
101
- { command: `notis tools exec ${ctx.args.toolName} --arguments '{}'`, reason: 'Execute this tool' },
380
+ { command: `notis tools exec ${requestedToolName} --dry-run --arguments '{}'`, reason: 'Validate arguments first' },
381
+ { command: `notis tools exec ${requestedToolName} --arguments '{}'`, reason: 'Execute this tool' },
102
382
  ],
103
383
  renderHuman: () => JSON.stringify(match, null, 2),
104
384
  });
105
385
  }
106
386
 
107
387
  async function toolsExecHandler(ctx) {
388
+ const requestedToolName = localNotisToolSlug(ctx.args.toolName);
389
+ // `tools exec` is the escape hatch around every first-class command, so the
390
+ // hand-over guard has to live here too or it guards nothing.
391
+ assertToolNotDelegated(requestedToolName);
392
+ let targetMutating = classifyToolMutation(requestedToolName);
393
+ ensureLongRunningToolTimeout(ctx, requestedToolName);
394
+ ctx.output.emitProgress({
395
+ phase: 'prepare',
396
+ message: `Resolving ${requestedToolName}`,
397
+ });
398
+ const fileBindings = await parseFileBindings(ctx.options.file);
399
+
108
400
  if (ctx.options.getSchema) {
109
- const tool = await fetchToolSchema(ctx.runtime, ctx.args.toolName);
401
+ const tool = await fetchToolSchema(ctx.runtime, requestedToolName);
110
402
  return ctx.output.emitSuccess({
111
403
  command: ctx.spec.command_path.join(' '),
112
404
  data: { tool },
113
- humanSummary: `Schema for ${ctx.args.toolName}`,
405
+ humanSummary: `Schema for ${requestedToolName}`,
114
406
  hints: [
115
- { command: `notis tools exec ${ctx.args.toolName} --dry-run --arguments '{}'`, reason: 'Validate arguments before executing' },
407
+ { command: `notis tools exec ${requestedToolName} --dry-run --arguments '{}'`, reason: 'Validate arguments before executing' },
116
408
  ],
117
409
  renderHuman: () => JSON.stringify(tool, null, 2),
118
410
  });
119
411
  }
120
412
 
121
- const rawArguments = ctx.options.arguments || '{}';
413
+ if (ctx.options.argumentsFile && ctx.options.arguments) {
414
+ throw usageError('Use either --arguments or --arguments-file, not both.');
415
+ }
416
+ const rawArguments = ctx.options.argumentsFile
417
+ ? `@${ctx.options.argumentsFile}`
418
+ : (ctx.options.arguments || '{}');
122
419
  const args = rawArguments === '-' || rawArguments.startsWith('@')
123
420
  ? await resolveJsonInput(rawArguments, 'arguments') || {}
124
421
  : parseMaybeJson(rawArguments, 'arguments') || {};
422
+ targetMutating = classifyToolMutation(requestedToolName, args);
125
423
 
126
424
  if (ctx.options.dryRun) {
127
- const tool = await fetchToolSchema(ctx.runtime, ctx.args.toolName);
425
+ if (fileBindings.length) {
426
+ throw usageError('--file is only supported when executing a tool, not with --dry-run');
427
+ }
428
+ const tool = await fetchToolSchema(ctx.runtime, requestedToolName);
128
429
  const errors = validateArguments(tool.parameters, args);
129
430
 
130
431
  if (errors.length) {
131
432
  return ctx.output.emitSuccess({
132
433
  command: ctx.spec.command_path.join(' '),
133
- data: { valid: false, errors, tool: ctx.args.toolName },
434
+ data: { valid: false, errors, tool: requestedToolName },
134
435
  humanSummary: `Dry run failed: ${errors.length} validation error(s)`,
135
436
  hints: [
136
- { command: `notis tools exec ${ctx.args.toolName} --get-schema`, reason: 'Review the full parameter schema' },
437
+ { command: `notis tools exec ${requestedToolName} --get-schema`, reason: 'Review the full parameter schema' },
137
438
  ],
138
439
  renderHuman: () => errors.map((e) => ` - ${e}`).join('\n'),
139
440
  });
@@ -141,64 +442,59 @@ async function toolsExecHandler(ctx) {
141
442
 
142
443
  return ctx.output.emitSuccess({
143
444
  command: ctx.spec.command_path.join(' '),
144
- data: { valid: true, tool: ctx.args.toolName, arguments: args },
145
- humanSummary: `Dry run passed for ${ctx.args.toolName}`,
445
+ data: { valid: true, tool: requestedToolName, arguments: args },
446
+ humanSummary: `Dry run passed for ${requestedToolName}`,
146
447
  hints: [
147
- { command: `notis tools exec ${ctx.args.toolName} --arguments '${JSON.stringify(args)}'`, reason: 'Execute with these arguments' },
448
+ { command: `notis tools exec ${requestedToolName} --arguments '${JSON.stringify(args)}'`, reason: 'Execute with these arguments' },
148
449
  ],
149
450
  });
150
451
  }
151
452
 
152
453
  const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
153
-
154
- if (ctx.options.watch) {
155
- const intervalSec = Math.max(1, parseInt(ctx.options.watch, 10));
156
- const intervalMs = intervalSec * 1000;
157
- process.on('SIGINT', () => process.exit(0));
158
-
159
- while (true) {
160
- try {
161
- const result = await runToolCommand({
162
- runtime: ctx.runtime,
163
- toolName: 'notis_execute_tool',
164
- arguments_: { tool_name: ctx.args.toolName, arguments: args },
165
- mutating: true,
166
- idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
167
- });
168
- ctx.output.emitSuccess({
169
- command: ctx.spec.command_path.join(' '),
170
- data: result.payload,
171
- humanSummary: `[${new Date().toISOString()}] ${ctx.args.toolName}`,
172
- renderHuman: () => JSON.stringify(result.payload, null, 2),
173
- });
174
- } catch (error) {
175
- process.stderr.write(`[${new Date().toISOString()}] Error: ${error.message}\n`);
176
- }
177
- await new Promise((r) => setTimeout(r, intervalMs));
178
- }
179
- }
454
+ ctx.output.emitProgress({
455
+ phase: 'execute',
456
+ message: `Calling ${requestedToolName}`,
457
+ });
180
458
 
181
459
  const result = await runToolCommand({
182
460
  runtime: ctx.runtime,
183
- toolName: 'notis_execute_tool',
461
+ toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
184
462
  arguments_: {
185
- tool_name: ctx.args.toolName,
186
- arguments: args,
463
+ tools: [{ tool_slug: requestedToolName, arguments: args }],
187
464
  },
465
+ // The server is authoritative about provider/MCP effects. A tool name that
466
+ // looks like a read is not proof of read-only behavior, so generic
467
+ // executions always carry a key and use mutation-safe transport retries.
468
+ // Proven reads are still executed in the read lane by hosted MCP.
188
469
  mutating: true,
189
470
  idempotencyKey,
471
+ fileBindings,
472
+ });
473
+ ctx.output.emitProgress({
474
+ phase: 'complete',
475
+ message: `Finished ${requestedToolName}`,
476
+ requestId: result.requestId,
190
477
  });
191
478
 
192
479
  return ctx.output.emitSuccess({
193
480
  command: ctx.spec.command_path.join(' '),
194
481
  data: result.payload,
195
- humanSummary: `Executed tool ${ctx.args.toolName}`,
196
- meta: { mutating: true, idempotency_key: idempotencyKey },
482
+ humanSummary: `Executed tool ${requestedToolName}`,
483
+ requestId: result.requestId,
484
+ meta: {
485
+ mutating: targetMutating,
486
+ mutation_classification: targetMutating === null ? 'unknown' : 'name_hint_only',
487
+ idempotency_key: idempotencyKey,
488
+ },
197
489
  renderHuman: () => JSON.stringify(result.payload, null, 2),
198
490
  });
199
491
  }
200
492
 
201
493
  async function toolsExecParallelHandler(ctx) {
494
+ const parallelFileBindings = await parseFileBindings(ctx.options.file);
495
+ if (parallelFileBindings.length) {
496
+ throw usageError('--file is supported by `notis tools exec` only; exec-parallel accepts JSON calls without file uploads.');
497
+ }
202
498
  const calls = parseMaybeJson(ctx.args.calls, 'calls');
203
499
  if (!Array.isArray(calls) || !calls.length) {
204
500
  throw usageError('calls must be a non-empty JSON array of {tool_name, arguments} objects');
@@ -208,16 +504,18 @@ async function toolsExecParallelHandler(ctx) {
208
504
  if (!call.tool_name || typeof call.tool_name !== 'string') {
209
505
  throw usageError(`calls[${i}] missing required "tool_name" string`);
210
506
  }
507
+ // A batch is just as good an escape hatch as a single exec.
508
+ assertToolNotDelegated(localNotisToolSlug(call.tool_name));
211
509
  }
510
+ ensureLongRunningToolTimeout(ctx, calls.map((call) => localNotisToolSlug(call.tool_name)));
212
511
 
213
512
  const data = await Promise.all(
214
513
  calls.map((call) =>
215
514
  runToolCommand({
216
515
  runtime: ctx.runtime,
217
- toolName: 'notis_execute_tool',
516
+ toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
218
517
  arguments_: {
219
- tool_name: call.tool_name,
220
- arguments: call.arguments || {},
518
+ tools: [{ tool_slug: localNotisToolSlug(call.tool_name), arguments: call.arguments || {} }],
221
519
  },
222
520
  mutating: true,
223
521
  idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
@@ -237,58 +535,63 @@ async function toolsExecParallelHandler(ctx) {
237
535
  }
238
536
 
239
537
  async function toolsLinkHandler(ctx) {
240
- const apiBase = ctx.runtime.apiBase.replace(/\/+$/, '');
241
- let portalBase;
242
- try {
243
- const parsed = new URL(apiBase);
244
- if (parsed.hostname.startsWith('api.')) {
245
- parsed.hostname = parsed.hostname.replace(/^api\./, 'app.');
246
- portalBase = parsed.origin;
247
- } else if (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') {
248
- const apiPort = parseInt(parsed.port, 10) || 80;
249
- parsed.port = String(apiPort === 3001 ? 3000 : apiPort);
250
- portalBase = parsed.origin;
251
- } else {
252
- portalBase = apiBase;
253
- }
254
- } catch {
255
- portalBase = apiBase.replace('api.', 'app.');
538
+ const credentials = ctx.options.credentials
539
+ ? await resolveJsonInput(ctx.options.credentials, 'credentials')
540
+ : undefined;
541
+ if (credentials !== undefined && (typeof credentials !== 'object' || credentials === null || Array.isArray(credentials))) {
542
+ throw usageError('credentials must be a JSON object');
543
+ }
544
+
545
+ const arguments_ = {
546
+ toolkit: ctx.args.toolkit,
547
+ ...(ctx.options.reconnect ? { reconnect: true } : {}),
548
+ ...(ctx.options.connectionId ? { reconnect_connection_id: ctx.options.connectionId } : {}),
549
+ ...(ctx.options.label ? { label: ctx.options.label } : {}),
550
+ ...(credentials ? { credentials } : {}),
551
+ };
552
+ const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
553
+ const result = await runToolCommand({
554
+ runtime: ctx.runtime,
555
+ toolName: 'LOCAL_NOTIS_AUTHENTIFY',
556
+ arguments_,
557
+ mutating: true,
558
+ idempotencyKey,
559
+ });
560
+ const payload = result.payload || {};
561
+ if (payload.status === 'error') {
562
+ throw usageError(payload.message || `Failed to connect ${ctx.args.toolkit}`, payload);
256
563
  }
257
- const portalUrl = `${portalBase}/integrations?connect=${encodeURIComponent(ctx.args.toolkit)}`;
564
+ const authUrl = payload.redirect_url || payload.integrations_url || payload.url || null;
258
565
 
259
566
  return ctx.output.emitSuccess({
260
567
  command: ctx.spec.command_path.join(' '),
261
568
  data: {
262
- toolkit: ctx.args.toolkit,
263
- auth_url: portalUrl,
569
+ ...payload,
570
+ auth_url: authUrl,
264
571
  },
265
- humanSummary: `Open this URL to connect ${ctx.args.toolkit}`,
572
+ humanSummary: ctx.options.reconnect
573
+ ? `Reconnected ${ctx.args.toolkit}`
574
+ : `Started connection for ${ctx.args.toolkit}`,
266
575
  hints: [
267
576
  { command: 'notis tools toolkits', reason: 'Verify the toolkit is connected after setup' },
268
577
  ],
269
- renderHuman: () =>
270
- [
271
- `Connect ${ctx.args.toolkit}:`,
272
- ` ${portalUrl}`,
273
- '',
274
- 'After connecting, verify with:',
275
- ' notis tools toolkits',
276
- ].join('\n'),
578
+ meta: { mutating: true, idempotency_key: idempotencyKey },
579
+ renderHuman: () => JSON.stringify({ ...payload, auth_url: authUrl }, null, 2),
277
580
  });
278
581
  }
279
582
 
280
583
  export const toolsCommandSpecs = [
281
584
  {
282
585
  command_path: ['tools', 'toolkits'],
283
- summary: 'List toolkit namespaces available to the active user.',
284
- when_to_use: 'Use this before searching or executing generic tools.',
586
+ summary: 'List toolkit namespaces and connection statuses available to the active user.',
587
+ when_to_use: 'Use this to inspect connection state before searching or executing generic tools.',
285
588
  args_schema: { arguments: [], options: [] },
286
589
  examples: ['notis tools toolkits', 'notis tools toolkits --json'],
287
- output_schema: 'Returns available toolkit namespaces.',
590
+ output_schema: 'Returns toolkit_connection_statuses from COMPOSIO_SEARCH_TOOLS, rendered as toolkit rows.',
288
591
  mutates: false,
289
592
  idempotent: true,
290
593
  related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>'],
291
- backend_call: { type: 'tool', name: 'notis_find_toolkits' },
594
+ backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
292
595
  handler: toolsToolkitsHandler,
293
596
  },
294
597
  {
@@ -297,14 +600,14 @@ export const toolsCommandSpecs = [
297
600
  when_to_use: 'Use this when you need a generic capability that does not have a first-class CLI command.',
298
601
  args_schema: {
299
602
  arguments: [{ token: '<query>', description: 'Natural language description of the tool you need.' }],
300
- options: [{ flags: '--toolkits <csv-or-json>', description: 'Optional subset of toolkit ids to search.' }],
603
+ options: [{ flags: '--known-fields <text>', key: 'knownFields', description: 'Optional known field hints, such as channel_name:general or user_email:a@example.com.' }],
301
604
  },
302
- examples: ['notis tools search "send an email"', 'notis tools search "update framer page" --toolkits mcp-framer'],
303
- output_schema: 'Returns tool matches with descriptions and parameter schemas.',
605
+ examples: ['notis tools search "send an email"', 'notis tools search "post on LinkedIn" --known-fields "platform:linkedin"'],
606
+ output_schema: 'Returns the full COMPOSIO_SEARCH_TOOLS response. Human output renders canonical names, connection status, and schema availability.',
304
607
  mutates: false,
305
608
  idempotent: true,
306
609
  related_commands: ['notis tools toolkits', 'notis tools exec <tool-name>'],
307
- backend_call: { type: 'tool', name: 'notis_find_tools' },
610
+ backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
308
611
  handler: toolsSearchHandler,
309
612
  },
310
613
  {
@@ -313,14 +616,14 @@ export const toolsCommandSpecs = [
313
616
  when_to_use: 'Use this when you know the tool name and want its parameter schema before execution.',
314
617
  args_schema: {
315
618
  arguments: [{ token: '<tool-name>', description: 'Exact tool name to locate and describe.' }],
316
- options: [{ flags: '--toolkits <csv-or-json>', description: 'Optional subset of toolkit ids to search.' }],
619
+ options: [],
317
620
  },
318
- examples: ['notis tools describe composio-gmail-default-send_email', 'notis tools describe notis-default-query --toolkits notis-default'],
621
+ examples: ['notis tools describe composio-gmail-send_email', 'notis tools describe LOCAL_NOTIS_DATABASE_QUERY'],
319
622
  output_schema: 'Returns one tool descriptor with name, description, and parameters.',
320
623
  mutates: false,
321
624
  idempotent: true,
322
625
  related_commands: ['notis tools search <query>', 'notis tools exec <tool-name>'],
323
- backend_call: { type: 'tool', name: 'notis_find_tools' },
626
+ backend_call: { type: 'tool', name: COMPOSIO_GET_TOOL_SCHEMAS },
324
627
  handler: toolsDescribeHandler,
325
628
  },
326
629
  {
@@ -331,24 +634,27 @@ export const toolsCommandSpecs = [
331
634
  arguments: [{ token: '<tool-name>', description: 'Tool name returned by `notis tools search`.' }],
332
635
  options: [
333
636
  { flags: '--arguments <json>', description: 'JSON object, @file path, or - for stdin.' },
637
+ { flags: '--arguments-file <path>', description: 'Read the JSON arguments object from a file.' },
638
+ { flags: '--file <argument-path=local-path>', description: 'Upload a local file into a file-uploadable tool argument. Repeatable.', collect: true },
334
639
  { flags: '--get-schema', description: 'Display the tool parameter schema without executing.' },
335
640
  { flags: '--dry-run', description: 'Validate arguments against the tool schema without executing.' },
336
- { flags: '--watch <seconds>', description: 'Re-execute on an interval and stream results.' },
337
641
  ],
338
642
  },
339
643
  examples: [
340
- 'notis tools exec notis-default-query --arguments \'{"database_slug":"tasks","query":{}}\'',
341
- 'notis tools exec notis-default-query --get-schema',
342
- 'notis tools exec notis-default-query --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
343
- 'notis tools exec notis-default-query --arguments @query.json',
344
- 'notis tools exec notis-default-query --arguments - < query.json',
345
- 'notis tools exec notis-default-query --watch 10 --arguments \'{"database_slug":"tasks","query":{}}\'',
644
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments \'{"database_slug":"tasks","query":{}}\'',
645
+ 'notis tools exec LOCAL_NOTIS_DATABASE_GET_DATABASE --arguments \'{"database_slug":"tasks"}\'',
646
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --get-schema',
647
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
648
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments @query.json',
649
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments-file query.json',
650
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments - < query.json',
651
+ 'notis tools exec composio-dropbox-upload_file --arguments \'{"path":"/target/in/dropbox.pdf"}\' --file content=./Invoice.pdf',
346
652
  ],
347
653
  output_schema: 'Returns the raw tool execution payload.',
348
654
  mutates: true,
349
655
  idempotent: true,
350
656
  related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>', 'notis tools exec-parallel <calls>'],
351
- backend_call: { type: 'tool', name: 'notis_execute_tool' },
657
+ backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
352
658
  handler: toolsExecHandler,
353
659
  },
354
660
  {
@@ -359,33 +665,43 @@ export const toolsCommandSpecs = [
359
665
  arguments: [
360
666
  { token: '<calls>', description: 'JSON array of {tool_name, arguments} objects.' },
361
667
  ],
362
- options: [],
668
+ options: [
669
+ { flags: '--file <argument-path=local-path>', description: 'Unsupported for exec-parallel; use tools exec for file uploads.', collect: true },
670
+ ],
363
671
  },
364
672
  examples: [
365
- 'notis tools exec-parallel \'[{"tool_name":"notis-default-query","arguments":{"database_slug":"tasks","query":{}}},{"tool_name":"notis-default-list_databases","arguments":{}}]\'',
673
+ 'notis tools exec-parallel \'[{"tool_name":"LOCAL_NOTIS_DATABASE_QUERY","arguments":{"database_slug":"tasks","query":{}}},{"tool_name":"LOCAL_NOTIS_DATABASE_LIST_DATABASES","arguments":{}}]\'',
366
674
  ],
367
675
  output_schema: 'Returns an array of results, one per tool call.',
368
676
  mutates: true,
369
677
  idempotent: true,
370
678
  related_commands: ['notis tools exec <tool-name>', 'notis tools search <query>'],
371
- backend_call: { type: 'tool', name: 'notis_execute_tool' },
679
+ backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
372
680
  handler: toolsExecParallelHandler,
373
681
  },
374
682
  {
375
683
  command_path: ['tools', 'link'],
376
- summary: 'Get the URL to connect an integration toolkit.',
377
- when_to_use: 'Use this when a tool requires authentication with an external service.',
684
+ summary: 'Connect or reconnect an integration toolkit.',
685
+ when_to_use: 'Use this when a tool requires authentication or an active connection must be replaced.',
378
686
  args_schema: {
379
687
  arguments: [{ token: '<toolkit>', description: 'Toolkit name to connect (e.g. github, gmail, slack).' }],
380
- options: [],
688
+ options: [
689
+ { flags: '--reconnect', description: 'Replace the existing account instead of adding another connection.' },
690
+ { flags: '--connection-id <id>', key: 'connectionId', description: 'Exact connection id to replace when multiple accounts exist.' },
691
+ { flags: '--label <label>', description: 'Account label for a new or replacement connection.' },
692
+ { flags: '--credentials <json>', description: 'Credential JSON object, @file path, or - for stdin. Prefer stdin so secrets do not enter shell history.' },
693
+ ],
381
694
  },
382
- examples: ['notis tools link github', 'notis tools link gmail --json'],
383
- output_schema: 'Returns the authentication URL for the specified toolkit.',
384
- mutates: false,
385
- idempotent: true,
386
- require_auth: false,
695
+ examples: [
696
+ 'notis tools link github',
697
+ 'notis tools link dataforseo --reconnect --credentials - < credentials.json',
698
+ ],
699
+ output_schema: 'Returns the connection result and an authentication URL when provider authorization is still required.',
700
+ mutates: true,
701
+ idempotent: false,
702
+ require_auth: true,
387
703
  related_commands: ['notis tools toolkits', 'notis tools search <query>'],
388
- backend_call: { type: 'local' },
704
+ backend_call: { type: 'tool', name: 'LOCAL_NOTIS_AUTHENTIFY' },
389
705
  handler: toolsLinkHandler,
390
706
  },
391
707
  ];