@notis_ai/cli 0.2.1 → 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 +106 -170
- 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/layout.tsx +6 -0
- package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-database/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-database/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
- package/dist/scaffolds/notis-database/components.json +20 -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/lib/utils.ts +6 -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/postcss.config.mjs +8 -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/notis-database/tsconfig.json +23 -0
- package/dist/scaffolds/notis-database/vite.config.ts +22 -0
- package/dist/scaffolds/notis-notes/app/globals.css +3 -0
- package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +2168 -0
- 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/notis-notes/notis.config.ts +36 -0
- package/dist/scaffolds/notis-notes/package.json +31 -0
- package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-notes/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
- package/dist/scaffolds/notis-notes/vite.config.ts +10 -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 +36 -0
- package/package.json +8 -3
- package/skills/notis-apps/SKILL.md +162 -0
- package/skills/notis-apps/cli.md +208 -0
- package/skills/notis-cli/SKILL.md +260 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/cli.js +31 -3
- package/src/command-specs/apps.js +248 -56
- package/src/command-specs/helpers.js +72 -104
- package/src/command-specs/index.js +0 -6
- package/src/command-specs/meta.js +14 -13
- package/src/command-specs/tools.js +196 -115
- package/src/runtime/app-boundary-validator.js +65 -14
- package/src/runtime/app-dev-server.js +32 -4
- package/src/runtime/app-platform.js +404 -24
- package/src/runtime/profiles.js +12 -6
- package/src/runtime/transport.js +124 -39
- package/template/.harness/index.html.tmpl +1 -50
- package/template/app/page.tsx +41 -6
- package/template/metadata/cover.png +0 -0
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
- package/template/notis.config.ts +10 -6
- package/template/package.json +2 -2
- package/template/packages/{notis-sdk → sdk}/package.json +6 -0
- package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectActionBar.tsx +2 -1
- package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectCheckbox.tsx +2 -2
- package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectDragOverlay.tsx +2 -2
- package/template/packages/{notis-sdk → sdk}/src/config.ts +1 -1
- package/template/packages/{notis-sdk → sdk}/src/hooks/useDatabase.ts +1 -13
- package/template/packages/{notis-sdk → sdk}/src/hooks/useMultiSelect.ts +4 -3
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +4 -3
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +3 -3
- package/template/packages/{notis-sdk → sdk}/src/hooks/useTool.ts +22 -7
- package/template/packages/{notis-sdk → sdk}/src/hooks/useUpsertDocument.ts +0 -8
- package/template/packages/{notis-sdk → sdk}/src/index.ts +2 -15
- package/template/packages/{notis-sdk → sdk}/src/provider.tsx +1 -1
- package/template/packages/{notis-sdk → sdk}/src/runtime.ts +5 -26
- package/src/command-specs/auth.js +0 -178
- package/src/command-specs/db.js +0 -163
- package/template/packages/notis-sdk/src/helpers.ts +0 -131
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTopBarSearch.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/styles.css +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/vite.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/tsconfig.json +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,69 +269,41 @@ 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
|
}
|
|
171
279
|
|
|
172
280
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
173
281
|
|
|
174
|
-
if (ctx.options.watch) {
|
|
175
|
-
const intervalSec = Math.max(1, parseInt(ctx.options.watch, 10));
|
|
176
|
-
const intervalMs = intervalSec * 1000;
|
|
177
|
-
process.on('SIGINT', () => process.exit(0));
|
|
178
|
-
|
|
179
|
-
while (true) {
|
|
180
|
-
try {
|
|
181
|
-
const result = await runToolCommand({
|
|
182
|
-
runtime: ctx.runtime,
|
|
183
|
-
toolName: 'notis_execute_tool',
|
|
184
|
-
arguments_: {
|
|
185
|
-
tool_name: ctx.args.toolName,
|
|
186
|
-
arguments: args,
|
|
187
|
-
...(toolkits.length ? { toolkits } : {}),
|
|
188
|
-
},
|
|
189
|
-
mutating: true,
|
|
190
|
-
idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
|
|
191
|
-
});
|
|
192
|
-
ctx.output.emitSuccess({
|
|
193
|
-
command: ctx.spec.command_path.join(' '),
|
|
194
|
-
data: result.payload,
|
|
195
|
-
humanSummary: `[${new Date().toISOString()}] ${ctx.args.toolName}`,
|
|
196
|
-
renderHuman: () => JSON.stringify(result.payload, null, 2),
|
|
197
|
-
});
|
|
198
|
-
} catch (error) {
|
|
199
|
-
process.stderr.write(`[${new Date().toISOString()}] Error: ${error.message}\n`);
|
|
200
|
-
}
|
|
201
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
282
|
const result = await runToolCommand({
|
|
206
283
|
runtime: ctx.runtime,
|
|
207
|
-
toolName:
|
|
284
|
+
toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
|
|
208
285
|
arguments_: {
|
|
209
|
-
|
|
210
|
-
arguments: args,
|
|
211
|
-
...(toolkits.length ? { toolkits } : {}),
|
|
286
|
+
tools: [{ tool_slug: requestedToolName, arguments: args }],
|
|
212
287
|
},
|
|
213
288
|
mutating: true,
|
|
214
289
|
idempotencyKey,
|
|
290
|
+
fileBindings,
|
|
215
291
|
});
|
|
216
292
|
|
|
217
293
|
return ctx.output.emitSuccess({
|
|
218
294
|
command: ctx.spec.command_path.join(' '),
|
|
219
295
|
data: result.payload,
|
|
220
|
-
humanSummary: `Executed tool ${
|
|
296
|
+
humanSummary: `Executed tool ${requestedToolName}`,
|
|
221
297
|
meta: { mutating: true, idempotency_key: idempotencyKey },
|
|
222
298
|
renderHuman: () => JSON.stringify(result.payload, null, 2),
|
|
223
299
|
});
|
|
224
300
|
}
|
|
225
301
|
|
|
226
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
|
+
}
|
|
227
307
|
const calls = parseMaybeJson(ctx.args.calls, 'calls');
|
|
228
308
|
if (!Array.isArray(calls) || !calls.length) {
|
|
229
309
|
throw usageError('calls must be a non-empty JSON array of {tool_name, arguments} objects');
|
|
@@ -234,16 +314,15 @@ async function toolsExecParallelHandler(ctx) {
|
|
|
234
314
|
throw usageError(`calls[${i}] missing required "tool_name" string`);
|
|
235
315
|
}
|
|
236
316
|
}
|
|
237
|
-
ensureLongRunningToolTimeout(ctx, calls.map((call) => call.tool_name));
|
|
317
|
+
ensureLongRunningToolTimeout(ctx, calls.map((call) => localNotisToolSlug(call.tool_name)));
|
|
238
318
|
|
|
239
319
|
const data = await Promise.all(
|
|
240
320
|
calls.map((call) =>
|
|
241
321
|
runToolCommand({
|
|
242
322
|
runtime: ctx.runtime,
|
|
243
|
-
toolName:
|
|
323
|
+
toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
|
|
244
324
|
arguments_: {
|
|
245
|
-
|
|
246
|
-
arguments: call.arguments || {},
|
|
325
|
+
tools: [{ tool_slug: localNotisToolSlug(call.tool_name), arguments: call.arguments || {} }],
|
|
247
326
|
},
|
|
248
327
|
mutating: true,
|
|
249
328
|
idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
|
|
@@ -306,15 +385,15 @@ async function toolsLinkHandler(ctx) {
|
|
|
306
385
|
export const toolsCommandSpecs = [
|
|
307
386
|
{
|
|
308
387
|
command_path: ['tools', 'toolkits'],
|
|
309
|
-
summary: 'List toolkit namespaces available to the active user.',
|
|
310
|
-
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.',
|
|
311
390
|
args_schema: { arguments: [], options: [] },
|
|
312
391
|
examples: ['notis tools toolkits', 'notis tools toolkits --json'],
|
|
313
|
-
output_schema: 'Returns
|
|
392
|
+
output_schema: 'Returns toolkit_connection_statuses from COMPOSIO_SEARCH_TOOLS, rendered as toolkit rows.',
|
|
314
393
|
mutates: false,
|
|
315
394
|
idempotent: true,
|
|
316
395
|
related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>'],
|
|
317
|
-
backend_call: { type: 'tool', name:
|
|
396
|
+
backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
|
|
318
397
|
handler: toolsToolkitsHandler,
|
|
319
398
|
},
|
|
320
399
|
{
|
|
@@ -323,14 +402,14 @@ export const toolsCommandSpecs = [
|
|
|
323
402
|
when_to_use: 'Use this when you need a generic capability that does not have a first-class CLI command.',
|
|
324
403
|
args_schema: {
|
|
325
404
|
arguments: [{ token: '<query>', description: 'Natural language description of the tool you need.' }],
|
|
326
|
-
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.' }],
|
|
327
406
|
},
|
|
328
|
-
examples: ['notis tools search "send an email"', 'notis tools search "
|
|
329
|
-
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.',
|
|
330
409
|
mutates: false,
|
|
331
410
|
idempotent: true,
|
|
332
411
|
related_commands: ['notis tools toolkits', 'notis tools exec <tool-name>'],
|
|
333
|
-
backend_call: { type: 'tool', name:
|
|
412
|
+
backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
|
|
334
413
|
handler: toolsSearchHandler,
|
|
335
414
|
},
|
|
336
415
|
{
|
|
@@ -339,14 +418,14 @@ export const toolsCommandSpecs = [
|
|
|
339
418
|
when_to_use: 'Use this when you know the tool name and want its parameter schema before execution.',
|
|
340
419
|
args_schema: {
|
|
341
420
|
arguments: [{ token: '<tool-name>', description: 'Exact tool name to locate and describe.' }],
|
|
342
|
-
options: [
|
|
421
|
+
options: [],
|
|
343
422
|
},
|
|
344
|
-
examples: ['notis tools describe composio-gmail-
|
|
423
|
+
examples: ['notis tools describe composio-gmail-send_email', 'notis tools describe LOCAL_NOTIS_DATABASE_QUERY'],
|
|
345
424
|
output_schema: 'Returns one tool descriptor with name, description, and parameters.',
|
|
346
425
|
mutates: false,
|
|
347
426
|
idempotent: true,
|
|
348
427
|
related_commands: ['notis tools search <query>', 'notis tools exec <tool-name>'],
|
|
349
|
-
backend_call: { type: 'tool', name:
|
|
428
|
+
backend_call: { type: 'tool', name: COMPOSIO_GET_TOOL_SCHEMAS },
|
|
350
429
|
handler: toolsDescribeHandler,
|
|
351
430
|
},
|
|
352
431
|
{
|
|
@@ -357,25 +436,25 @@ export const toolsCommandSpecs = [
|
|
|
357
436
|
arguments: [{ token: '<tool-name>', description: 'Tool name returned by `notis tools search`.' }],
|
|
358
437
|
options: [
|
|
359
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 },
|
|
360
440
|
{ flags: '--get-schema', description: 'Display the tool parameter schema without executing.' },
|
|
361
441
|
{ flags: '--dry-run', description: 'Validate arguments against the tool schema without executing.' },
|
|
362
|
-
{ flags: '--watch <seconds>', description: 'Re-execute on an interval and stream results.' },
|
|
363
|
-
{ flags: '--toolkits <csv-or-json>', description: 'Optional toolkit namespace(s) to use when resolving the tool.' },
|
|
364
442
|
],
|
|
365
443
|
},
|
|
366
444
|
examples: [
|
|
367
|
-
'notis tools exec
|
|
368
|
-
'notis tools exec
|
|
369
|
-
'notis tools exec
|
|
370
|
-
'notis tools exec
|
|
371
|
-
'notis tools exec
|
|
372
|
-
'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',
|
|
373
452
|
],
|
|
374
453
|
output_schema: 'Returns the raw tool execution payload.',
|
|
375
454
|
mutates: true,
|
|
376
455
|
idempotent: true,
|
|
377
456
|
related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>', 'notis tools exec-parallel <calls>'],
|
|
378
|
-
backend_call: { type: 'tool', name:
|
|
457
|
+
backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
|
|
379
458
|
handler: toolsExecHandler,
|
|
380
459
|
},
|
|
381
460
|
{
|
|
@@ -386,16 +465,18 @@ export const toolsCommandSpecs = [
|
|
|
386
465
|
arguments: [
|
|
387
466
|
{ token: '<calls>', description: 'JSON array of {tool_name, arguments} objects.' },
|
|
388
467
|
],
|
|
389
|
-
options: [
|
|
468
|
+
options: [
|
|
469
|
+
{ flags: '--file <argument-path=local-path>', description: 'Unsupported for exec-parallel; use tools exec for file uploads.', collect: true },
|
|
470
|
+
],
|
|
390
471
|
},
|
|
391
472
|
examples: [
|
|
392
|
-
'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":{}}]\'',
|
|
393
474
|
],
|
|
394
475
|
output_schema: 'Returns an array of results, one per tool call.',
|
|
395
476
|
mutates: true,
|
|
396
477
|
idempotent: true,
|
|
397
478
|
related_commands: ['notis tools exec <tool-name>', 'notis tools search <query>'],
|
|
398
|
-
backend_call: { type: 'tool', name:
|
|
479
|
+
backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
|
|
399
480
|
handler: toolsExecParallelHandler,
|
|
400
481
|
},
|
|
401
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
|
}
|