@notis_ai/cli 0.2.2 → 0.2.3
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/README.md +19 -18
- package/config/notis_app_boundary_rules.json +50 -0
- package/dist/scaffolds/notis-database/app/globals.css +44 -0
- package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
- package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
- package/dist/scaffolds/notis-database/index.html +12 -0
- package/dist/scaffolds/notis-database/lib/types.ts +134 -0
- package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/notis.config.ts +27 -0
- package/dist/scaffolds/notis-database/package.json +31 -0
- package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
- package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
- package/dist/scaffolds/{notes → notis-database}/tsconfig.json +2 -1
- package/dist/scaffolds/notis-database/vite.config.ts +22 -0
- package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
- package/dist/scaffolds/{notes → notis-notes}/app/page.tsx +783 -80
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-notes/components.json +20 -0
- package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/{notes → notis-notes}/notis.config.ts +12 -7
- package/dist/scaffolds/{notes → notis-notes}/package.json +2 -2
- package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
- package/dist/scaffolds/notis-random/README.md +33 -0
- package/dist/scaffolds/notis-random/app/globals.css +11 -0
- package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
- package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-random/app/page.tsx +222 -0
- package/dist/scaffolds/notis-random/index.html +12 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
- package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
- package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
- package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/notis.config.ts +43 -0
- package/dist/scaffolds/notis-random/package.json +32 -0
- package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
- package/dist/scaffolds/notis-random/tsconfig.json +23 -0
- package/dist/scaffolds/notis-random/vite.config.ts +11 -0
- package/dist/scaffolds.json +27 -4
- package/package.json +2 -1
- package/skills/notis-apps/SKILL.md +2 -2
- package/skills/notis-cli/SKILL.md +18 -16
- package/skills/notis-query/cli.md +9 -10
- package/src/cli.js +30 -2
- package/src/command-specs/apps.js +42 -15
- package/src/command-specs/helpers.js +73 -45
- package/src/command-specs/meta.js +7 -7
- package/src/command-specs/tools.js +196 -83
- package/src/runtime/app-boundary-validator.js +65 -14
- package/src/runtime/profiles.js +10 -4
- package/src/runtime/transport.js +122 -37
- package/template/app/page.tsx +1 -1
- package/template/notis.config.ts +1 -1
- package/template/packages/sdk/src/hooks/useTool.ts +1 -1
- /package/dist/scaffolds/{notes → notis-database}/app/layout.tsx +0 -0
- /package/dist/scaffolds/{notes → notis-database}/components/ui/badge.tsx +0 -0
- /package/dist/scaffolds/{notes → notis-database}/components/ui/button.tsx +0 -0
- /package/dist/scaffolds/{notes → notis-database}/components/ui/card.tsx +0 -0
- /package/dist/scaffolds/{notes → notis-database}/components.json +0 -0
- /package/dist/scaffolds/{notes → notis-database}/lib/utils.ts +0 -0
- /package/dist/scaffolds/{notes → notis-database}/postcss.config.mjs +0 -0
- /package/dist/scaffolds/{notes → notis-notes}/app/globals.css +0 -0
- /package/dist/scaffolds/{notes → notis-notes}/tailwind.config.ts +0 -0
- /package/dist/scaffolds/{notes → notis-notes}/vite.config.ts +0 -0
|
@@ -1,13 +1,18 @@
|
|
|
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';
|
|
3
5
|
import {
|
|
6
|
+
COMPOSIO_GET_TOOL_SCHEMAS,
|
|
7
|
+
COMPOSIO_MULTI_EXECUTE_TOOL,
|
|
8
|
+
COMPOSIO_SEARCH_TOOLS,
|
|
9
|
+
fetchToolDiscovery,
|
|
4
10
|
fetchToolkits,
|
|
5
11
|
fetchToolSchema,
|
|
12
|
+
localNotisToolSlug,
|
|
6
13
|
nextIdempotencyKey,
|
|
7
14
|
parseJson,
|
|
8
15
|
parseMaybeJson,
|
|
9
|
-
normalizeToolkits,
|
|
10
|
-
resolveSearchToolkits,
|
|
11
16
|
runToolCommand,
|
|
12
17
|
validateArguments,
|
|
13
18
|
} from './helpers.js';
|
|
@@ -36,15 +41,98 @@ async function resolveJsonInput(value, label) {
|
|
|
36
41
|
|
|
37
42
|
const LONG_RUNNING_TOOL_TIMEOUT_MS = 600000;
|
|
38
43
|
const LONG_RUNNING_TOOL_NAMES = new Set([
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
44
|
+
'LOCAL_NOTIS_GENERATE_IMAGE_OPENAI',
|
|
45
|
+
'LOCAL_NOTIS_EDIT_IMAGE_OPENAI',
|
|
46
|
+
'LOCAL_NOTIS_GENERATE_IMAGE_GEMINI',
|
|
47
|
+
'LOCAL_NOTIS_EDIT_IMAGE_GEMINI',
|
|
43
48
|
]);
|
|
44
49
|
|
|
50
|
+
const EXTENSION_CONTENT_TYPES = new Map([
|
|
51
|
+
['.pdf', 'application/pdf'],
|
|
52
|
+
['.png', 'image/png'],
|
|
53
|
+
['.jpg', 'image/jpeg'],
|
|
54
|
+
['.jpeg', 'image/jpeg'],
|
|
55
|
+
['.gif', 'image/gif'],
|
|
56
|
+
['.webp', 'image/webp'],
|
|
57
|
+
['.txt', 'text/plain'],
|
|
58
|
+
['.csv', 'text/csv'],
|
|
59
|
+
['.json', 'application/json'],
|
|
60
|
+
['.md', 'text/markdown'],
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
function guessContentType(filePath) {
|
|
64
|
+
const lower = filePath.toLowerCase();
|
|
65
|
+
const dotIndex = lower.lastIndexOf('.');
|
|
66
|
+
if (dotIndex === -1) return 'application/octet-stream';
|
|
67
|
+
return EXTENSION_CONTENT_TYPES.get(lower.slice(dotIndex)) || 'application/octet-stream';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function hashFileSha256(localPath) {
|
|
71
|
+
const hash = createHash('sha256');
|
|
72
|
+
for await (const chunk of createReadStream(localPath)) {
|
|
73
|
+
hash.update(chunk);
|
|
74
|
+
}
|
|
75
|
+
return hash.digest('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function parseFileBindingSpec(spec, index) {
|
|
79
|
+
if (typeof spec !== 'string' || !spec.trim()) {
|
|
80
|
+
throw usageError('--file must be formatted as <argument-path>=<local-path>');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const equalsIndex = spec.indexOf('=');
|
|
84
|
+
if (equalsIndex <= 0 || equalsIndex === spec.length - 1) {
|
|
85
|
+
throw usageError('--file must be formatted as <argument-path>=<local-path>');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const argumentPath = spec.slice(0, equalsIndex).trim();
|
|
89
|
+
const localPath = spec.slice(equalsIndex + 1).trim();
|
|
90
|
+
if (!argumentPath) {
|
|
91
|
+
throw usageError('--file argument path is required');
|
|
92
|
+
}
|
|
93
|
+
if (!localPath) {
|
|
94
|
+
throw usageError('--file local path is required');
|
|
95
|
+
}
|
|
96
|
+
if (!existsSync(localPath)) {
|
|
97
|
+
throw usageError(`File not found: ${localPath}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let stats;
|
|
101
|
+
try {
|
|
102
|
+
stats = statSync(localPath);
|
|
103
|
+
accessSync(localPath, fsConstants.R_OK);
|
|
104
|
+
} catch {
|
|
105
|
+
throw usageError(`File is not readable: ${localPath}`);
|
|
106
|
+
}
|
|
107
|
+
if (!stats.isFile()) {
|
|
108
|
+
throw usageError(`File is not a regular file: ${localPath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sha256 = await hashFileSha256(localPath);
|
|
112
|
+
const fileBasename = basename(localPath);
|
|
113
|
+
const fieldName = `file_${index}`;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
argument_path: argumentPath,
|
|
117
|
+
field_name: fieldName,
|
|
118
|
+
basename: fileBasename,
|
|
119
|
+
size: stats.size,
|
|
120
|
+
sha256,
|
|
121
|
+
contentType: guessContentType(fileBasename),
|
|
122
|
+
localPath,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function parseFileBindings(rawFileOptions) {
|
|
127
|
+
const values = Array.isArray(rawFileOptions)
|
|
128
|
+
? rawFileOptions
|
|
129
|
+
: (rawFileOptions ? [rawFileOptions] : []);
|
|
130
|
+
return Promise.all(values.map((value, index) => parseFileBindingSpec(value, index)));
|
|
131
|
+
}
|
|
132
|
+
|
|
45
133
|
function ensureLongRunningToolTimeout(ctx, toolNames) {
|
|
46
134
|
const names = Array.isArray(toolNames) ? toolNames : [toolNames];
|
|
47
|
-
if (!names.some((name) => LONG_RUNNING_TOOL_NAMES.has(name))) {
|
|
135
|
+
if (!names.some((name) => LONG_RUNNING_TOOL_NAMES.has(localNotisToolSlug(name)))) {
|
|
48
136
|
return;
|
|
49
137
|
}
|
|
50
138
|
ctx.runtime.timeoutMs = Math.max(ctx.runtime.timeoutMs || 0, LONG_RUNNING_TOOL_TIMEOUT_MS);
|
|
@@ -57,27 +145,55 @@ async function toolsToolkitsHandler(ctx) {
|
|
|
57
145
|
data: { toolkits },
|
|
58
146
|
humanSummary: `Found ${toolkits.length} toolkits`,
|
|
59
147
|
hints: [
|
|
60
|
-
{ command: 'notis tools search <query>', reason: 'Search
|
|
148
|
+
{ command: 'notis tools search <query>', reason: 'Search canonical tools and schemas' },
|
|
61
149
|
],
|
|
62
|
-
renderHuman: () =>
|
|
150
|
+
renderHuman: () =>
|
|
151
|
+
toolkits
|
|
152
|
+
.map((entry) => {
|
|
153
|
+
const status = entry.has_active_connection ? 'connected' : 'needs auth';
|
|
154
|
+
return `${entry.id} ${status} ${entry.description}`;
|
|
155
|
+
})
|
|
156
|
+
.join('\n'),
|
|
63
157
|
});
|
|
64
158
|
}
|
|
65
159
|
|
|
160
|
+
function discoveryToolRows(payload) {
|
|
161
|
+
const schemas = payload.tool_schemas || {};
|
|
162
|
+
const statuses = new Map(
|
|
163
|
+
(payload.toolkit_connection_statuses || [])
|
|
164
|
+
.filter((entry) => entry && typeof entry.toolkit === 'string')
|
|
165
|
+
.map((entry) => [entry.toolkit, entry])
|
|
166
|
+
);
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
const rows = [];
|
|
169
|
+
for (const result of payload.results || []) {
|
|
170
|
+
for (const name of [...(result.primary_tool_slugs || []), ...(result.related_tool_slugs || [])]) {
|
|
171
|
+
if (!name || seen.has(name)) continue;
|
|
172
|
+
seen.add(name);
|
|
173
|
+
const schema = schemas[name] || {};
|
|
174
|
+
const toolkit = schema.toolkit || name.split('-').slice(0, -1).join('-');
|
|
175
|
+
const status = statuses.get(toolkit);
|
|
176
|
+
rows.push({
|
|
177
|
+
name,
|
|
178
|
+
toolkit,
|
|
179
|
+
description: schema.description || '',
|
|
180
|
+
schema_available: Boolean(schemas[name]),
|
|
181
|
+
connection_status: status
|
|
182
|
+
? (status.has_active_connection ? 'connected' : 'needs auth')
|
|
183
|
+
: 'unknown',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return rows;
|
|
188
|
+
}
|
|
189
|
+
|
|
66
190
|
async function toolsSearchHandler(ctx) {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
runtime: ctx.runtime,
|
|
70
|
-
toolName: 'notis_find_tools',
|
|
71
|
-
arguments_: {
|
|
72
|
-
query: ctx.args.query || undefined,
|
|
73
|
-
toolkits,
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
const tools = result.payload.tools || [];
|
|
191
|
+
const payload = await fetchToolDiscovery(ctx.runtime, ctx.args.query || 'Find tools', ctx.options.knownFields || '');
|
|
192
|
+
const tools = discoveryToolRows(payload);
|
|
77
193
|
const firstTool = tools[0];
|
|
78
194
|
return ctx.output.emitSuccess({
|
|
79
195
|
command: ctx.spec.command_path.join(' '),
|
|
80
|
-
data:
|
|
196
|
+
data: payload,
|
|
81
197
|
humanSummary: `Found ${tools.length} tools`,
|
|
82
198
|
hints: firstTool
|
|
83
199
|
? [
|
|
@@ -87,52 +203,41 @@ async function toolsSearchHandler(ctx) {
|
|
|
87
203
|
: [],
|
|
88
204
|
renderHuman: () =>
|
|
89
205
|
tools
|
|
90
|
-
.map((tool) => `${tool.name}\n ${tool.
|
|
206
|
+
.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`)
|
|
91
207
|
.join('\n')
|
|
92
208
|
.trim(),
|
|
93
209
|
});
|
|
94
210
|
}
|
|
95
211
|
|
|
96
212
|
async function toolsDescribeHandler(ctx) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
runtime: ctx.runtime,
|
|
100
|
-
toolName: 'notis_find_tools',
|
|
101
|
-
arguments_: {
|
|
102
|
-
query: ctx.args.toolName,
|
|
103
|
-
toolkits,
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
const tools = result.payload.tools || [];
|
|
107
|
-
const match = tools.find((tool) => tool.name === ctx.args.toolName);
|
|
108
|
-
if (!match) {
|
|
109
|
-
throw usageError(`Tool ${ctx.args.toolName} was not found in the selected toolkits.`);
|
|
110
|
-
}
|
|
213
|
+
const requestedToolName = localNotisToolSlug(ctx.args.toolName);
|
|
214
|
+
const match = await fetchToolSchema(ctx.runtime, requestedToolName);
|
|
111
215
|
|
|
112
216
|
return ctx.output.emitSuccess({
|
|
113
217
|
command: ctx.spec.command_path.join(' '),
|
|
114
|
-
data: { tool: match
|
|
115
|
-
humanSummary: `Described tool ${
|
|
218
|
+
data: { tool: match },
|
|
219
|
+
humanSummary: `Described tool ${requestedToolName}`,
|
|
116
220
|
hints: [
|
|
117
|
-
{ command: `notis tools exec ${
|
|
118
|
-
{ command: `notis tools exec ${
|
|
221
|
+
{ command: `notis tools exec ${requestedToolName} --dry-run --arguments '{}'`, reason: 'Validate arguments first' },
|
|
222
|
+
{ command: `notis tools exec ${requestedToolName} --arguments '{}'`, reason: 'Execute this tool' },
|
|
119
223
|
],
|
|
120
224
|
renderHuman: () => JSON.stringify(match, null, 2),
|
|
121
225
|
});
|
|
122
226
|
}
|
|
123
227
|
|
|
124
228
|
async function toolsExecHandler(ctx) {
|
|
125
|
-
|
|
126
|
-
|
|
229
|
+
const requestedToolName = localNotisToolSlug(ctx.args.toolName);
|
|
230
|
+
ensureLongRunningToolTimeout(ctx, requestedToolName);
|
|
231
|
+
const fileBindings = await parseFileBindings(ctx.options.file);
|
|
127
232
|
|
|
128
233
|
if (ctx.options.getSchema) {
|
|
129
|
-
const tool = await fetchToolSchema(ctx.runtime,
|
|
234
|
+
const tool = await fetchToolSchema(ctx.runtime, requestedToolName);
|
|
130
235
|
return ctx.output.emitSuccess({
|
|
131
236
|
command: ctx.spec.command_path.join(' '),
|
|
132
|
-
data: { tool
|
|
133
|
-
humanSummary: `Schema for ${
|
|
237
|
+
data: { tool },
|
|
238
|
+
humanSummary: `Schema for ${requestedToolName}`,
|
|
134
239
|
hints: [
|
|
135
|
-
{ command: `notis tools exec ${
|
|
240
|
+
{ command: `notis tools exec ${requestedToolName} --dry-run --arguments '{}'`, reason: 'Validate arguments before executing' },
|
|
136
241
|
],
|
|
137
242
|
renderHuman: () => JSON.stringify(tool, null, 2),
|
|
138
243
|
});
|
|
@@ -144,16 +249,19 @@ async function toolsExecHandler(ctx) {
|
|
|
144
249
|
: parseMaybeJson(rawArguments, 'arguments') || {};
|
|
145
250
|
|
|
146
251
|
if (ctx.options.dryRun) {
|
|
147
|
-
|
|
252
|
+
if (fileBindings.length) {
|
|
253
|
+
throw usageError('--file is only supported when executing a tool, not with --dry-run');
|
|
254
|
+
}
|
|
255
|
+
const tool = await fetchToolSchema(ctx.runtime, requestedToolName);
|
|
148
256
|
const errors = validateArguments(tool.parameters, args);
|
|
149
257
|
|
|
150
258
|
if (errors.length) {
|
|
151
259
|
return ctx.output.emitSuccess({
|
|
152
260
|
command: ctx.spec.command_path.join(' '),
|
|
153
|
-
data: { valid: false, errors, tool:
|
|
261
|
+
data: { valid: false, errors, tool: requestedToolName },
|
|
154
262
|
humanSummary: `Dry run failed: ${errors.length} validation error(s)`,
|
|
155
263
|
hints: [
|
|
156
|
-
{ command: `notis tools exec ${
|
|
264
|
+
{ command: `notis tools exec ${requestedToolName} --get-schema`, reason: 'Review the full parameter schema' },
|
|
157
265
|
],
|
|
158
266
|
renderHuman: () => errors.map((e) => ` - ${e}`).join('\n'),
|
|
159
267
|
});
|
|
@@ -161,10 +269,10 @@ async function toolsExecHandler(ctx) {
|
|
|
161
269
|
|
|
162
270
|
return ctx.output.emitSuccess({
|
|
163
271
|
command: ctx.spec.command_path.join(' '),
|
|
164
|
-
data: { valid: true, tool:
|
|
165
|
-
humanSummary: `Dry run passed for ${
|
|
272
|
+
data: { valid: true, tool: requestedToolName, arguments: args },
|
|
273
|
+
humanSummary: `Dry run passed for ${requestedToolName}`,
|
|
166
274
|
hints: [
|
|
167
|
-
{ command: `notis tools exec ${
|
|
275
|
+
{ command: `notis tools exec ${requestedToolName} --arguments '${JSON.stringify(args)}'`, reason: 'Execute with these arguments' },
|
|
168
276
|
],
|
|
169
277
|
});
|
|
170
278
|
}
|
|
@@ -173,26 +281,29 @@ async function toolsExecHandler(ctx) {
|
|
|
173
281
|
|
|
174
282
|
const result = await runToolCommand({
|
|
175
283
|
runtime: ctx.runtime,
|
|
176
|
-
toolName:
|
|
284
|
+
toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
|
|
177
285
|
arguments_: {
|
|
178
|
-
|
|
179
|
-
arguments: args,
|
|
180
|
-
...(toolkits.length ? { toolkits } : {}),
|
|
286
|
+
tools: [{ tool_slug: requestedToolName, arguments: args }],
|
|
181
287
|
},
|
|
182
288
|
mutating: true,
|
|
183
289
|
idempotencyKey,
|
|
290
|
+
fileBindings,
|
|
184
291
|
});
|
|
185
292
|
|
|
186
293
|
return ctx.output.emitSuccess({
|
|
187
294
|
command: ctx.spec.command_path.join(' '),
|
|
188
295
|
data: result.payload,
|
|
189
|
-
humanSummary: `Executed tool ${
|
|
296
|
+
humanSummary: `Executed tool ${requestedToolName}`,
|
|
190
297
|
meta: { mutating: true, idempotency_key: idempotencyKey },
|
|
191
298
|
renderHuman: () => JSON.stringify(result.payload, null, 2),
|
|
192
299
|
});
|
|
193
300
|
}
|
|
194
301
|
|
|
195
302
|
async function toolsExecParallelHandler(ctx) {
|
|
303
|
+
const parallelFileBindings = await parseFileBindings(ctx.options.file);
|
|
304
|
+
if (parallelFileBindings.length) {
|
|
305
|
+
throw usageError('--file is supported by `notis tools exec` only; exec-parallel accepts JSON calls without file uploads.');
|
|
306
|
+
}
|
|
196
307
|
const calls = parseMaybeJson(ctx.args.calls, 'calls');
|
|
197
308
|
if (!Array.isArray(calls) || !calls.length) {
|
|
198
309
|
throw usageError('calls must be a non-empty JSON array of {tool_name, arguments} objects');
|
|
@@ -203,16 +314,15 @@ async function toolsExecParallelHandler(ctx) {
|
|
|
203
314
|
throw usageError(`calls[${i}] missing required "tool_name" string`);
|
|
204
315
|
}
|
|
205
316
|
}
|
|
206
|
-
ensureLongRunningToolTimeout(ctx, calls.map((call) => call.tool_name));
|
|
317
|
+
ensureLongRunningToolTimeout(ctx, calls.map((call) => localNotisToolSlug(call.tool_name)));
|
|
207
318
|
|
|
208
319
|
const data = await Promise.all(
|
|
209
320
|
calls.map((call) =>
|
|
210
321
|
runToolCommand({
|
|
211
322
|
runtime: ctx.runtime,
|
|
212
|
-
toolName:
|
|
323
|
+
toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
|
|
213
324
|
arguments_: {
|
|
214
|
-
|
|
215
|
-
arguments: call.arguments || {},
|
|
325
|
+
tools: [{ tool_slug: localNotisToolSlug(call.tool_name), arguments: call.arguments || {} }],
|
|
216
326
|
},
|
|
217
327
|
mutating: true,
|
|
218
328
|
idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
|
|
@@ -275,15 +385,15 @@ async function toolsLinkHandler(ctx) {
|
|
|
275
385
|
export const toolsCommandSpecs = [
|
|
276
386
|
{
|
|
277
387
|
command_path: ['tools', 'toolkits'],
|
|
278
|
-
summary: 'List toolkit namespaces available to the active user.',
|
|
279
|
-
when_to_use: 'Use this before searching or executing generic tools.',
|
|
388
|
+
summary: 'List toolkit namespaces and connection statuses available to the active user.',
|
|
389
|
+
when_to_use: 'Use this to inspect connection state before searching or executing generic tools.',
|
|
280
390
|
args_schema: { arguments: [], options: [] },
|
|
281
391
|
examples: ['notis tools toolkits', 'notis tools toolkits --json'],
|
|
282
|
-
output_schema: 'Returns
|
|
392
|
+
output_schema: 'Returns toolkit_connection_statuses from COMPOSIO_SEARCH_TOOLS, rendered as toolkit rows.',
|
|
283
393
|
mutates: false,
|
|
284
394
|
idempotent: true,
|
|
285
395
|
related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>'],
|
|
286
|
-
backend_call: { type: 'tool', name:
|
|
396
|
+
backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
|
|
287
397
|
handler: toolsToolkitsHandler,
|
|
288
398
|
},
|
|
289
399
|
{
|
|
@@ -292,14 +402,14 @@ export const toolsCommandSpecs = [
|
|
|
292
402
|
when_to_use: 'Use this when you need a generic capability that does not have a first-class CLI command.',
|
|
293
403
|
args_schema: {
|
|
294
404
|
arguments: [{ token: '<query>', description: 'Natural language description of the tool you need.' }],
|
|
295
|
-
options: [{ flags: '--
|
|
405
|
+
options: [{ flags: '--known-fields <text>', key: 'knownFields', description: 'Optional known field hints, such as channel_name:general or user_email:a@example.com.' }],
|
|
296
406
|
},
|
|
297
|
-
examples: ['notis tools search "send an email"', 'notis tools search "
|
|
298
|
-
output_schema: 'Returns
|
|
407
|
+
examples: ['notis tools search "send an email"', 'notis tools search "post on LinkedIn" --known-fields "platform:linkedin"'],
|
|
408
|
+
output_schema: 'Returns the full COMPOSIO_SEARCH_TOOLS response. Human output renders canonical names, connection status, and schema availability.',
|
|
299
409
|
mutates: false,
|
|
300
410
|
idempotent: true,
|
|
301
411
|
related_commands: ['notis tools toolkits', 'notis tools exec <tool-name>'],
|
|
302
|
-
backend_call: { type: 'tool', name:
|
|
412
|
+
backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
|
|
303
413
|
handler: toolsSearchHandler,
|
|
304
414
|
},
|
|
305
415
|
{
|
|
@@ -308,14 +418,14 @@ export const toolsCommandSpecs = [
|
|
|
308
418
|
when_to_use: 'Use this when you know the tool name and want its parameter schema before execution.',
|
|
309
419
|
args_schema: {
|
|
310
420
|
arguments: [{ token: '<tool-name>', description: 'Exact tool name to locate and describe.' }],
|
|
311
|
-
options: [
|
|
421
|
+
options: [],
|
|
312
422
|
},
|
|
313
|
-
examples: ['notis tools describe composio-gmail-
|
|
423
|
+
examples: ['notis tools describe composio-gmail-send_email', 'notis tools describe LOCAL_NOTIS_DATABASE_QUERY'],
|
|
314
424
|
output_schema: 'Returns one tool descriptor with name, description, and parameters.',
|
|
315
425
|
mutates: false,
|
|
316
426
|
idempotent: true,
|
|
317
427
|
related_commands: ['notis tools search <query>', 'notis tools exec <tool-name>'],
|
|
318
|
-
backend_call: { type: 'tool', name:
|
|
428
|
+
backend_call: { type: 'tool', name: COMPOSIO_GET_TOOL_SCHEMAS },
|
|
319
429
|
handler: toolsDescribeHandler,
|
|
320
430
|
},
|
|
321
431
|
{
|
|
@@ -326,24 +436,25 @@ export const toolsCommandSpecs = [
|
|
|
326
436
|
arguments: [{ token: '<tool-name>', description: 'Tool name returned by `notis tools search`.' }],
|
|
327
437
|
options: [
|
|
328
438
|
{ flags: '--arguments <json>', description: 'JSON object, @file path, or - for stdin.' },
|
|
439
|
+
{ flags: '--file <argument-path=local-path>', description: 'Upload a local file into a file-uploadable tool argument. Repeatable.', collect: true },
|
|
329
440
|
{ flags: '--get-schema', description: 'Display the tool parameter schema without executing.' },
|
|
330
441
|
{ flags: '--dry-run', description: 'Validate arguments against the tool schema without executing.' },
|
|
331
|
-
{ flags: '--toolkits <csv-or-json>', description: 'Optional toolkit namespace(s) to use when resolving the tool.' },
|
|
332
442
|
],
|
|
333
443
|
},
|
|
334
444
|
examples: [
|
|
335
|
-
'notis tools exec
|
|
336
|
-
'notis tools exec
|
|
337
|
-
'notis tools exec
|
|
338
|
-
'notis tools exec
|
|
339
|
-
'notis tools exec
|
|
340
|
-
'notis tools exec
|
|
445
|
+
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments \'{"database_slug":"tasks","query":{}}\'',
|
|
446
|
+
'notis tools exec LOCAL_NOTIS_DATABASE_GET_DATABASE --arguments \'{"database_slug":"tasks"}\'',
|
|
447
|
+
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --get-schema',
|
|
448
|
+
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
|
|
449
|
+
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments @query.json',
|
|
450
|
+
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments - < query.json',
|
|
451
|
+
'notis tools exec composio-dropbox-upload_file --arguments \'{"path":"/target/in/dropbox.pdf"}\' --file content=./Invoice.pdf',
|
|
341
452
|
],
|
|
342
453
|
output_schema: 'Returns the raw tool execution payload.',
|
|
343
454
|
mutates: true,
|
|
344
455
|
idempotent: true,
|
|
345
456
|
related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>', 'notis tools exec-parallel <calls>'],
|
|
346
|
-
backend_call: { type: 'tool', name:
|
|
457
|
+
backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
|
|
347
458
|
handler: toolsExecHandler,
|
|
348
459
|
},
|
|
349
460
|
{
|
|
@@ -354,16 +465,18 @@ export const toolsCommandSpecs = [
|
|
|
354
465
|
arguments: [
|
|
355
466
|
{ token: '<calls>', description: 'JSON array of {tool_name, arguments} objects.' },
|
|
356
467
|
],
|
|
357
|
-
options: [
|
|
468
|
+
options: [
|
|
469
|
+
{ flags: '--file <argument-path=local-path>', description: 'Unsupported for exec-parallel; use tools exec for file uploads.', collect: true },
|
|
470
|
+
],
|
|
358
471
|
},
|
|
359
472
|
examples: [
|
|
360
|
-
'notis tools exec-parallel \'[{"tool_name":"
|
|
473
|
+
'notis tools exec-parallel \'[{"tool_name":"LOCAL_NOTIS_DATABASE_QUERY","arguments":{"database_slug":"tasks","query":{}}},{"tool_name":"LOCAL_NOTIS_DATABASE_LIST_DATABASES","arguments":{}}]\'',
|
|
361
474
|
],
|
|
362
475
|
output_schema: 'Returns an array of results, one per tool call.',
|
|
363
476
|
mutates: true,
|
|
364
477
|
idempotent: true,
|
|
365
478
|
related_commands: ['notis tools exec <tool-name>', 'notis tools search <query>'],
|
|
366
|
-
backend_call: { type: 'tool', name:
|
|
479
|
+
backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
|
|
367
480
|
handler: toolsExecParallelHandler,
|
|
368
481
|
},
|
|
369
482
|
{
|
|
@@ -4,10 +4,30 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
|
|
5
5
|
import { usageError } from './errors.js';
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
)
|
|
7
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
// Candidate locations for the app boundary rules, in priority order:
|
|
10
|
+
// 1. The monorepo server source (always fresh during local development).
|
|
11
|
+
// 2. The copy bundled into the published package at build time (see
|
|
12
|
+
// scripts/copy-boundary-rules.js + package.json "files": config/). When the
|
|
13
|
+
// CLI is installed via npm, candidate 1 escapes the package and is absent,
|
|
14
|
+
// so the bundled copy is used. This is what was missing before: the package
|
|
15
|
+
// shipped only the escaping path and crashed with ENOENT on any command.
|
|
16
|
+
export const RULES_PATH_CANDIDATES = [
|
|
17
|
+
resolve(moduleDir, '../../../../server/config/notis_app_boundary_rules.json'),
|
|
18
|
+
resolve(moduleDir, '../../config/notis_app_boundary_rules.json'),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function resolveRulesPath() {
|
|
22
|
+
for (const candidate of RULES_PATH_CANDIDATES) {
|
|
23
|
+
if (existsSync(candidate)) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Fall back to the bundled-copy path so any error message points inside the
|
|
28
|
+
// package rather than at an escaping monorepo path.
|
|
29
|
+
return RULES_PATH_CANDIDATES[RULES_PATH_CANDIDATES.length - 1];
|
|
30
|
+
}
|
|
11
31
|
|
|
12
32
|
const SOURCE_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.css', '.scss', '.sass', '.pcss']);
|
|
13
33
|
const IGNORED_SOURCE_DIRS = new Set([
|
|
@@ -20,20 +40,50 @@ const IGNORED_SOURCE_DIRS = new Set([
|
|
|
20
40
|
'coverage',
|
|
21
41
|
]);
|
|
22
42
|
|
|
23
|
-
function loadBoundaryRules() {
|
|
24
|
-
return JSON.parse(readFileSync(RULES_PATH, 'utf-8'));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
43
|
function compileRules(entries) {
|
|
28
|
-
|
|
44
|
+
// Tolerate a rules object whose javascript/css field is a non-array (a corrupt
|
|
45
|
+
// file can parse to {"javascript":"x"}); coerce anything non-array to [] so
|
|
46
|
+
// getCompiledRules degrades to an empty rule set instead of throwing on .map.
|
|
47
|
+
return (Array.isArray(entries) ? entries : []).map((entry) => ({
|
|
29
48
|
regex: new RegExp(entry.pattern, 'm'),
|
|
30
49
|
message: entry.message,
|
|
31
50
|
}));
|
|
32
51
|
}
|
|
33
52
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
// Loaded lazily on first validation rather than at module import time: a
|
|
54
|
+
// missing rules file must not prevent the whole CLI from booting (every
|
|
55
|
+
// command imports this module transitively). The server re-validates on
|
|
56
|
+
// save/deploy, so an empty client-side rule set degrades gracefully.
|
|
57
|
+
let compiledRules = null;
|
|
58
|
+
|
|
59
|
+
function getCompiledRules() {
|
|
60
|
+
if (compiledRules) {
|
|
61
|
+
return compiledRules;
|
|
62
|
+
}
|
|
63
|
+
let boundaryRules = { javascript: [], css: [] };
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(readFileSync(resolveRulesPath(), 'utf-8'));
|
|
66
|
+
// Keep the safe default unless the file is a real rules object: a valid but
|
|
67
|
+
// non-object payload (null, a number, an array) would otherwise throw on the
|
|
68
|
+
// `.javascript`/`.css` deref below, reintroducing the boot crash this guards.
|
|
69
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
70
|
+
boundaryRules = parsed;
|
|
71
|
+
} else {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
'Warning: Notis app boundary rules are not a rules object; skipping local boundary checks.\n',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
process.stderr.write(
|
|
78
|
+
`Warning: could not load Notis app boundary rules (${error.message}); skipping local boundary checks.\n`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
compiledRules = {
|
|
82
|
+
javascript: compileRules(boundaryRules.javascript),
|
|
83
|
+
css: compileRules(boundaryRules.css),
|
|
84
|
+
};
|
|
85
|
+
return compiledRules;
|
|
86
|
+
}
|
|
37
87
|
|
|
38
88
|
function collectProjectFiles(projectDir, dir, results) {
|
|
39
89
|
if (!existsSync(dir)) {
|
|
@@ -81,11 +131,12 @@ function applyRules(content, rules, relPath) {
|
|
|
81
131
|
|
|
82
132
|
function validateTextFile(relPath, content) {
|
|
83
133
|
const extension = extname(relPath);
|
|
134
|
+
const rules = getCompiledRules();
|
|
84
135
|
if (extension === '.css' || extension === '.scss' || extension === '.sass' || extension === '.pcss') {
|
|
85
|
-
return applyRules(content,
|
|
136
|
+
return applyRules(content, rules.css, relPath);
|
|
86
137
|
}
|
|
87
138
|
if (extension === '.js' || extension === '.jsx' || extension === '.ts' || extension === '.tsx') {
|
|
88
|
-
return applyRules(content,
|
|
139
|
+
return applyRules(content, rules.javascript, relPath);
|
|
89
140
|
}
|
|
90
141
|
return [];
|
|
91
142
|
}
|
package/src/runtime/profiles.js
CHANGED
|
@@ -212,15 +212,21 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
|
|
|
212
212
|
});
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// An explicit NOTIS_JWT is a complete credential override: use it verbatim and do
|
|
216
|
+
// NOT attempt a stored-profile dev_portal token refresh (whose refresh_token may be
|
|
217
|
+
// stale or belong to a different session). Without this, a long-lived shell with an
|
|
218
|
+
// expired profile refresh_token fails every command with invalid_grant even though
|
|
219
|
+
// the supplied NOTIS_JWT is valid.
|
|
220
|
+
const usingEnvJwt = Boolean(process.env.NOTIS_JWT);
|
|
215
221
|
return {
|
|
216
222
|
config,
|
|
217
223
|
profileName,
|
|
218
224
|
apiBase,
|
|
219
225
|
jwt,
|
|
220
|
-
authMode: profile.auth_mode,
|
|
221
|
-
refreshToken: profile.refresh_token,
|
|
222
|
-
accessExpiresAt: profile.access_expires_at,
|
|
223
|
-
refreshExpiresAt: profile.refresh_expires_at,
|
|
226
|
+
authMode: usingEnvJwt ? 'jwt' : profile.auth_mode,
|
|
227
|
+
refreshToken: usingEnvJwt ? undefined : profile.refresh_token,
|
|
228
|
+
accessExpiresAt: usingEnvJwt ? undefined : profile.access_expires_at,
|
|
229
|
+
refreshExpiresAt: usingEnvJwt ? undefined : profile.refresh_expires_at,
|
|
224
230
|
agentMode,
|
|
225
231
|
nonInteractive,
|
|
226
232
|
outputMode,
|