@notis_ai/cli 0.2.0-beta.35.1 → 0.2.0-beta.48.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.
- package/README.md +84 -83
- 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 +3 -3
- 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 +1 -1
- package/skills/notis-apps/SKILL.md +27 -33
- package/skills/notis-apps/cli.md +51 -51
- package/skills/notis-cli/SKILL.md +66 -71
- package/skills/notis-query/cli.md +11 -11
- package/src/cli.js +10 -1
- package/src/command-specs/apps.js +9 -9
- package/src/command-specs/helpers.js +69 -46
- package/src/command-specs/meta.js +6 -6
- package/src/command-specs/tools.js +159 -50
- package/src/runtime/app-dev-server.js +2 -2
- package/src/runtime/app-platform.js +28 -0
- package/src/runtime/transport.js +122 -37
- package/template/packages/sdk/package.json +6 -0
- package/template/packages/sdk/src/hooks/useDatabase.ts +76 -0
- package/template/packages/sdk/src/hooks/useNotis.ts +10 -6
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
- package/template/packages/sdk/src/index.ts +9 -1
- package/template/packages/sdk/src/runtime.ts +83 -2
- /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,14 @@
|
|
|
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
|
+
fetchToolDiscovery,
|
|
4
7
|
fetchToolkits,
|
|
5
8
|
fetchToolSchema,
|
|
6
9
|
nextIdempotencyKey,
|
|
7
10
|
parseJson,
|
|
8
11
|
parseMaybeJson,
|
|
9
|
-
normalizeToolkits,
|
|
10
|
-
resolveSearchToolkits,
|
|
11
12
|
runToolCommand,
|
|
12
13
|
validateArguments,
|
|
13
14
|
} from './helpers.js';
|
|
@@ -42,6 +43,89 @@ const LONG_RUNNING_TOOL_NAMES = new Set([
|
|
|
42
43
|
'notis-default-edit_image_gemini',
|
|
43
44
|
]);
|
|
44
45
|
|
|
46
|
+
const EXTENSION_CONTENT_TYPES = new Map([
|
|
47
|
+
['.pdf', 'application/pdf'],
|
|
48
|
+
['.png', 'image/png'],
|
|
49
|
+
['.jpg', 'image/jpeg'],
|
|
50
|
+
['.jpeg', 'image/jpeg'],
|
|
51
|
+
['.gif', 'image/gif'],
|
|
52
|
+
['.webp', 'image/webp'],
|
|
53
|
+
['.txt', 'text/plain'],
|
|
54
|
+
['.csv', 'text/csv'],
|
|
55
|
+
['.json', 'application/json'],
|
|
56
|
+
['.md', 'text/markdown'],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
function guessContentType(filePath) {
|
|
60
|
+
const lower = filePath.toLowerCase();
|
|
61
|
+
const dotIndex = lower.lastIndexOf('.');
|
|
62
|
+
if (dotIndex === -1) return 'application/octet-stream';
|
|
63
|
+
return EXTENSION_CONTENT_TYPES.get(lower.slice(dotIndex)) || 'application/octet-stream';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function hashFileSha256(localPath) {
|
|
67
|
+
const hash = createHash('sha256');
|
|
68
|
+
for await (const chunk of createReadStream(localPath)) {
|
|
69
|
+
hash.update(chunk);
|
|
70
|
+
}
|
|
71
|
+
return hash.digest('hex');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function parseFileBindingSpec(spec, index) {
|
|
75
|
+
if (typeof spec !== 'string' || !spec.trim()) {
|
|
76
|
+
throw usageError('--file must be formatted as <argument-path>=<local-path>');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const equalsIndex = spec.indexOf('=');
|
|
80
|
+
if (equalsIndex <= 0 || equalsIndex === spec.length - 1) {
|
|
81
|
+
throw usageError('--file must be formatted as <argument-path>=<local-path>');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const argumentPath = spec.slice(0, equalsIndex).trim();
|
|
85
|
+
const localPath = spec.slice(equalsIndex + 1).trim();
|
|
86
|
+
if (!argumentPath) {
|
|
87
|
+
throw usageError('--file argument path is required');
|
|
88
|
+
}
|
|
89
|
+
if (!localPath) {
|
|
90
|
+
throw usageError('--file local path is required');
|
|
91
|
+
}
|
|
92
|
+
if (!existsSync(localPath)) {
|
|
93
|
+
throw usageError(`File not found: ${localPath}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let stats;
|
|
97
|
+
try {
|
|
98
|
+
stats = statSync(localPath);
|
|
99
|
+
accessSync(localPath, fsConstants.R_OK);
|
|
100
|
+
} catch {
|
|
101
|
+
throw usageError(`File is not readable: ${localPath}`);
|
|
102
|
+
}
|
|
103
|
+
if (!stats.isFile()) {
|
|
104
|
+
throw usageError(`File is not a regular file: ${localPath}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sha256 = await hashFileSha256(localPath);
|
|
108
|
+
const fileBasename = basename(localPath);
|
|
109
|
+
const fieldName = `file_${index}`;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
argument_path: argumentPath,
|
|
113
|
+
field_name: fieldName,
|
|
114
|
+
basename: fileBasename,
|
|
115
|
+
size: stats.size,
|
|
116
|
+
sha256,
|
|
117
|
+
contentType: guessContentType(fileBasename),
|
|
118
|
+
localPath,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function parseFileBindings(rawFileOptions) {
|
|
123
|
+
const values = Array.isArray(rawFileOptions)
|
|
124
|
+
? rawFileOptions
|
|
125
|
+
: (rawFileOptions ? [rawFileOptions] : []);
|
|
126
|
+
return Promise.all(values.map((value, index) => parseFileBindingSpec(value, index)));
|
|
127
|
+
}
|
|
128
|
+
|
|
45
129
|
function ensureLongRunningToolTimeout(ctx, toolNames) {
|
|
46
130
|
const names = Array.isArray(toolNames) ? toolNames : [toolNames];
|
|
47
131
|
if (!names.some((name) => LONG_RUNNING_TOOL_NAMES.has(name))) {
|
|
@@ -57,27 +141,55 @@ async function toolsToolkitsHandler(ctx) {
|
|
|
57
141
|
data: { toolkits },
|
|
58
142
|
humanSummary: `Found ${toolkits.length} toolkits`,
|
|
59
143
|
hints: [
|
|
60
|
-
{ command: 'notis tools search <query>', reason: 'Search
|
|
144
|
+
{ command: 'notis tools search <query>', reason: 'Search canonical tools and schemas' },
|
|
61
145
|
],
|
|
62
|
-
renderHuman: () =>
|
|
146
|
+
renderHuman: () =>
|
|
147
|
+
toolkits
|
|
148
|
+
.map((entry) => {
|
|
149
|
+
const status = entry.has_active_connection ? 'connected' : 'needs auth';
|
|
150
|
+
return `${entry.id} ${status} ${entry.description}`;
|
|
151
|
+
})
|
|
152
|
+
.join('\n'),
|
|
63
153
|
});
|
|
64
154
|
}
|
|
65
155
|
|
|
156
|
+
function discoveryToolRows(payload) {
|
|
157
|
+
const schemas = payload.tool_schemas || {};
|
|
158
|
+
const statuses = new Map(
|
|
159
|
+
(payload.toolkit_connection_statuses || [])
|
|
160
|
+
.filter((entry) => entry && typeof entry.toolkit === 'string')
|
|
161
|
+
.map((entry) => [entry.toolkit, entry])
|
|
162
|
+
);
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
const rows = [];
|
|
165
|
+
for (const result of payload.results || []) {
|
|
166
|
+
for (const name of [...(result.primary_tool_slugs || []), ...(result.related_tool_slugs || [])]) {
|
|
167
|
+
if (!name || seen.has(name)) continue;
|
|
168
|
+
seen.add(name);
|
|
169
|
+
const schema = schemas[name] || {};
|
|
170
|
+
const toolkit = schema.toolkit || name.split('-').slice(0, -1).join('-');
|
|
171
|
+
const status = statuses.get(toolkit);
|
|
172
|
+
rows.push({
|
|
173
|
+
name,
|
|
174
|
+
toolkit,
|
|
175
|
+
description: schema.description || '',
|
|
176
|
+
schema_available: Boolean(schemas[name]),
|
|
177
|
+
connection_status: status
|
|
178
|
+
? (status.has_active_connection ? 'connected' : 'needs auth')
|
|
179
|
+
: 'unknown',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return rows;
|
|
184
|
+
}
|
|
185
|
+
|
|
66
186
|
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 || [];
|
|
187
|
+
const payload = await fetchToolDiscovery(ctx.runtime, ctx.args.query || 'Find tools', ctx.options.knownFields || '');
|
|
188
|
+
const tools = discoveryToolRows(payload);
|
|
77
189
|
const firstTool = tools[0];
|
|
78
190
|
return ctx.output.emitSuccess({
|
|
79
191
|
command: ctx.spec.command_path.join(' '),
|
|
80
|
-
data:
|
|
192
|
+
data: payload,
|
|
81
193
|
humanSummary: `Found ${tools.length} tools`,
|
|
82
194
|
hints: firstTool
|
|
83
195
|
? [
|
|
@@ -87,31 +199,18 @@ async function toolsSearchHandler(ctx) {
|
|
|
87
199
|
: [],
|
|
88
200
|
renderHuman: () =>
|
|
89
201
|
tools
|
|
90
|
-
.map((tool) => `${tool.name}\n ${tool.
|
|
202
|
+
.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
203
|
.join('\n')
|
|
92
204
|
.trim(),
|
|
93
205
|
});
|
|
94
206
|
}
|
|
95
207
|
|
|
96
208
|
async function toolsDescribeHandler(ctx) {
|
|
97
|
-
const
|
|
98
|
-
const result = await runToolCommand({
|
|
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
|
-
}
|
|
209
|
+
const match = await fetchToolSchema(ctx.runtime, ctx.args.toolName);
|
|
111
210
|
|
|
112
211
|
return ctx.output.emitSuccess({
|
|
113
212
|
command: ctx.spec.command_path.join(' '),
|
|
114
|
-
data: { tool: match
|
|
213
|
+
data: { tool: match },
|
|
115
214
|
humanSummary: `Described tool ${ctx.args.toolName}`,
|
|
116
215
|
hints: [
|
|
117
216
|
{ command: `notis tools exec ${ctx.args.toolName} --dry-run --arguments '{}'`, reason: 'Validate arguments first' },
|
|
@@ -123,13 +222,13 @@ async function toolsDescribeHandler(ctx) {
|
|
|
123
222
|
|
|
124
223
|
async function toolsExecHandler(ctx) {
|
|
125
224
|
ensureLongRunningToolTimeout(ctx, ctx.args.toolName);
|
|
126
|
-
const
|
|
225
|
+
const fileBindings = await parseFileBindings(ctx.options.file);
|
|
127
226
|
|
|
128
227
|
if (ctx.options.getSchema) {
|
|
129
|
-
const tool = await fetchToolSchema(ctx.runtime, ctx.args.toolName
|
|
228
|
+
const tool = await fetchToolSchema(ctx.runtime, ctx.args.toolName);
|
|
130
229
|
return ctx.output.emitSuccess({
|
|
131
230
|
command: ctx.spec.command_path.join(' '),
|
|
132
|
-
data: { tool
|
|
231
|
+
data: { tool },
|
|
133
232
|
humanSummary: `Schema for ${ctx.args.toolName}`,
|
|
134
233
|
hints: [
|
|
135
234
|
{ command: `notis tools exec ${ctx.args.toolName} --dry-run --arguments '{}'`, reason: 'Validate arguments before executing' },
|
|
@@ -144,13 +243,16 @@ async function toolsExecHandler(ctx) {
|
|
|
144
243
|
: parseMaybeJson(rawArguments, 'arguments') || {};
|
|
145
244
|
|
|
146
245
|
if (ctx.options.dryRun) {
|
|
147
|
-
|
|
246
|
+
if (fileBindings.length) {
|
|
247
|
+
throw usageError('--file is only supported when executing a tool, not with --dry-run');
|
|
248
|
+
}
|
|
249
|
+
const tool = await fetchToolSchema(ctx.runtime, ctx.args.toolName);
|
|
148
250
|
const errors = validateArguments(tool.parameters, args);
|
|
149
251
|
|
|
150
252
|
if (errors.length) {
|
|
151
253
|
return ctx.output.emitSuccess({
|
|
152
254
|
command: ctx.spec.command_path.join(' '),
|
|
153
|
-
data: { valid: false, errors, tool: ctx.args.toolName
|
|
255
|
+
data: { valid: false, errors, tool: ctx.args.toolName },
|
|
154
256
|
humanSummary: `Dry run failed: ${errors.length} validation error(s)`,
|
|
155
257
|
hints: [
|
|
156
258
|
{ command: `notis tools exec ${ctx.args.toolName} --get-schema`, reason: 'Review the full parameter schema' },
|
|
@@ -161,7 +263,7 @@ async function toolsExecHandler(ctx) {
|
|
|
161
263
|
|
|
162
264
|
return ctx.output.emitSuccess({
|
|
163
265
|
command: ctx.spec.command_path.join(' '),
|
|
164
|
-
data: { valid: true, tool: ctx.args.toolName, arguments: args
|
|
266
|
+
data: { valid: true, tool: ctx.args.toolName, arguments: args },
|
|
165
267
|
humanSummary: `Dry run passed for ${ctx.args.toolName}`,
|
|
166
268
|
hints: [
|
|
167
269
|
{ command: `notis tools exec ${ctx.args.toolName} --arguments '${JSON.stringify(args)}'`, reason: 'Execute with these arguments' },
|
|
@@ -177,10 +279,10 @@ async function toolsExecHandler(ctx) {
|
|
|
177
279
|
arguments_: {
|
|
178
280
|
tool_name: ctx.args.toolName,
|
|
179
281
|
arguments: args,
|
|
180
|
-
...(toolkits.length ? { toolkits } : {}),
|
|
181
282
|
},
|
|
182
283
|
mutating: true,
|
|
183
284
|
idempotencyKey,
|
|
285
|
+
fileBindings,
|
|
184
286
|
});
|
|
185
287
|
|
|
186
288
|
return ctx.output.emitSuccess({
|
|
@@ -193,6 +295,10 @@ async function toolsExecHandler(ctx) {
|
|
|
193
295
|
}
|
|
194
296
|
|
|
195
297
|
async function toolsExecParallelHandler(ctx) {
|
|
298
|
+
const parallelFileBindings = await parseFileBindings(ctx.options.file);
|
|
299
|
+
if (parallelFileBindings.length) {
|
|
300
|
+
throw usageError('--file is supported by `notis tools exec` only; exec-parallel accepts JSON calls without file uploads.');
|
|
301
|
+
}
|
|
196
302
|
const calls = parseMaybeJson(ctx.args.calls, 'calls');
|
|
197
303
|
if (!Array.isArray(calls) || !calls.length) {
|
|
198
304
|
throw usageError('calls must be a non-empty JSON array of {tool_name, arguments} objects');
|
|
@@ -275,15 +381,15 @@ async function toolsLinkHandler(ctx) {
|
|
|
275
381
|
export const toolsCommandSpecs = [
|
|
276
382
|
{
|
|
277
383
|
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.',
|
|
384
|
+
summary: 'List toolkit namespaces and connection statuses available to the active user.',
|
|
385
|
+
when_to_use: 'Use this to inspect connection state before searching or executing generic tools.',
|
|
280
386
|
args_schema: { arguments: [], options: [] },
|
|
281
387
|
examples: ['notis tools toolkits', 'notis tools toolkits --json'],
|
|
282
|
-
output_schema: 'Returns
|
|
388
|
+
output_schema: 'Returns toolkit_connection_statuses from notis_find_tools, rendered as toolkit rows.',
|
|
283
389
|
mutates: false,
|
|
284
390
|
idempotent: true,
|
|
285
391
|
related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>'],
|
|
286
|
-
backend_call: { type: 'tool', name: '
|
|
392
|
+
backend_call: { type: 'tool', name: 'notis_find_tools' },
|
|
287
393
|
handler: toolsToolkitsHandler,
|
|
288
394
|
},
|
|
289
395
|
{
|
|
@@ -292,10 +398,10 @@ export const toolsCommandSpecs = [
|
|
|
292
398
|
when_to_use: 'Use this when you need a generic capability that does not have a first-class CLI command.',
|
|
293
399
|
args_schema: {
|
|
294
400
|
arguments: [{ token: '<query>', description: 'Natural language description of the tool you need.' }],
|
|
295
|
-
options: [{ flags: '--
|
|
401
|
+
options: [{ flags: '--known-fields <text>', key: 'knownFields', description: 'Optional known field hints, such as channel_name:general or user_email:a@example.com.' }],
|
|
296
402
|
},
|
|
297
|
-
examples: ['notis tools search "send an email"', 'notis tools search "
|
|
298
|
-
output_schema: 'Returns
|
|
403
|
+
examples: ['notis tools search "send an email"', 'notis tools search "post on LinkedIn" --known-fields "platform:linkedin"'],
|
|
404
|
+
output_schema: 'Returns the full notis_find_tools response. Human output renders canonical names, connection status, and schema availability.',
|
|
299
405
|
mutates: false,
|
|
300
406
|
idempotent: true,
|
|
301
407
|
related_commands: ['notis tools toolkits', 'notis tools exec <tool-name>'],
|
|
@@ -308,9 +414,9 @@ export const toolsCommandSpecs = [
|
|
|
308
414
|
when_to_use: 'Use this when you know the tool name and want its parameter schema before execution.',
|
|
309
415
|
args_schema: {
|
|
310
416
|
arguments: [{ token: '<tool-name>', description: 'Exact tool name to locate and describe.' }],
|
|
311
|
-
options: [
|
|
417
|
+
options: [],
|
|
312
418
|
},
|
|
313
|
-
examples: ['notis tools describe composio-gmail-default-send_email', 'notis tools describe notis-default-query
|
|
419
|
+
examples: ['notis tools describe composio-gmail-default-send_email', 'notis tools describe notis-default-query'],
|
|
314
420
|
output_schema: 'Returns one tool descriptor with name, description, and parameters.',
|
|
315
421
|
mutates: false,
|
|
316
422
|
idempotent: true,
|
|
@@ -326,9 +432,9 @@ export const toolsCommandSpecs = [
|
|
|
326
432
|
arguments: [{ token: '<tool-name>', description: 'Tool name returned by `notis tools search`.' }],
|
|
327
433
|
options: [
|
|
328
434
|
{ flags: '--arguments <json>', description: 'JSON object, @file path, or - for stdin.' },
|
|
435
|
+
{ flags: '--file <argument-path=local-path>', description: 'Upload a local file into a file-uploadable tool argument. Repeatable.', collect: true },
|
|
329
436
|
{ flags: '--get-schema', description: 'Display the tool parameter schema without executing.' },
|
|
330
437
|
{ 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
438
|
],
|
|
333
439
|
},
|
|
334
440
|
examples: [
|
|
@@ -338,6 +444,7 @@ export const toolsCommandSpecs = [
|
|
|
338
444
|
'notis tools exec notis-default-query --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
|
|
339
445
|
'notis tools exec notis-default-query --arguments @query.json',
|
|
340
446
|
'notis tools exec notis-default-query --arguments - < query.json',
|
|
447
|
+
'notis tools exec composio-dropbox-default-upload_file --arguments \'{"path":"/target/in/dropbox.pdf"}\' --file content=./Invoice.pdf',
|
|
341
448
|
],
|
|
342
449
|
output_schema: 'Returns the raw tool execution payload.',
|
|
343
450
|
mutates: true,
|
|
@@ -354,7 +461,9 @@ export const toolsCommandSpecs = [
|
|
|
354
461
|
arguments: [
|
|
355
462
|
{ token: '<calls>', description: 'JSON array of {tool_name, arguments} objects.' },
|
|
356
463
|
],
|
|
357
|
-
options: [
|
|
464
|
+
options: [
|
|
465
|
+
{ flags: '--file <argument-path=local-path>', description: 'Unsupported for exec-parallel; use tools exec for file uploads.', collect: true },
|
|
466
|
+
],
|
|
358
467
|
},
|
|
359
468
|
examples: [
|
|
360
469
|
'notis tools exec-parallel \'[{"tool_name":"notis-default-query","arguments":{"database_slug":"tasks","query":{}}},{"tool_name":"notis-default-list_databases","arguments":{}}]\'',
|
|
@@ -114,8 +114,8 @@ function reactVersionFromPeer(peerRange) {
|
|
|
114
114
|
function resolveHarnessReactVersion(projectDir) {
|
|
115
115
|
const candidates = [
|
|
116
116
|
join(projectDir, 'node_modules', '@notis', 'sdk', 'package.json'),
|
|
117
|
-
join(REPO_ROOT, 'packages', '
|
|
118
|
-
join(CLI_ROOT, 'template', 'packages', '
|
|
117
|
+
join(REPO_ROOT, 'packages', 'sdk', 'package.json'),
|
|
118
|
+
join(CLI_ROOT, 'template', 'packages', 'sdk', 'package.json'),
|
|
119
119
|
];
|
|
120
120
|
for (const candidate of candidates) {
|
|
121
121
|
const pkg = readJsonFile(candidate);
|
|
@@ -27,6 +27,7 @@ const MONOREPO_APPS_DIR = resolve(CLI_ROOT, '../..', 'apps');
|
|
|
27
27
|
const DIST_DIR = join(CLI_ROOT, 'dist');
|
|
28
28
|
const SCAFFOLD_CATALOG_FILE = join(DIST_DIR, 'scaffolds.json');
|
|
29
29
|
const SCAFFOLD_SOURCE_DIR = join(DIST_DIR, 'scaffolds');
|
|
30
|
+
const TEMPLATE_SDK_DIR = join(CLI_ROOT, 'template', 'packages', 'sdk');
|
|
30
31
|
export const NOTIS_APP_CATEGORIES = [
|
|
31
32
|
'Productivity',
|
|
32
33
|
'Sales & Marketing',
|
|
@@ -787,6 +788,7 @@ export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
|
|
|
787
788
|
if (existsSync(pkgPath)) {
|
|
788
789
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
789
790
|
pkg.name = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
791
|
+
ensureScaffoldLocalSdk(projectDir, pkg);
|
|
790
792
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
791
793
|
}
|
|
792
794
|
|
|
@@ -821,6 +823,32 @@ export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
|
|
|
821
823
|
return { projectDir };
|
|
822
824
|
}
|
|
823
825
|
|
|
826
|
+
function ensureScaffoldLocalSdk(projectDir, pkg) {
|
|
827
|
+
let shouldInstallLocalSdk = false;
|
|
828
|
+
for (const dependencyGroup of ['dependencies', 'devDependencies']) {
|
|
829
|
+
const dependencyValue = pkg[dependencyGroup]?.['@notis/sdk'];
|
|
830
|
+
if (typeof dependencyValue === 'string' && dependencyValue.startsWith('file:')) {
|
|
831
|
+
pkg[dependencyGroup]['@notis/sdk'] = 'file:./packages/sdk';
|
|
832
|
+
shouldInstallLocalSdk = true;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!shouldInstallLocalSdk) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const localSdkDir = join(projectDir, 'packages', 'sdk');
|
|
841
|
+
if (existsSync(join(localSdkDir, 'package.json'))) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (!existsSync(join(TEMPLATE_SDK_DIR, 'package.json'))) {
|
|
845
|
+
throw usageError(`SDK template not found at ${TEMPLATE_SDK_DIR}. Ensure @notis_ai/cli is installed correctly.`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
mkdirSync(dirname(localSdkDir), { recursive: true });
|
|
849
|
+
cpSync(TEMPLATE_SDK_DIR, localSdkDir, { recursive: true, dereference: true });
|
|
850
|
+
}
|
|
851
|
+
|
|
824
852
|
function resolveScaffoldSourceDir(fromSlug) {
|
|
825
853
|
const bundledDir = join(SCAFFOLD_SOURCE_DIR, fromSlug);
|
|
826
854
|
if (existsSync(bundledDir)) {
|
package/src/runtime/transport.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
2
3
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
3
4
|
import {
|
|
4
5
|
DEFAULT_PROFILE,
|
|
@@ -8,31 +9,71 @@ import {
|
|
|
8
9
|
saveConfig,
|
|
9
10
|
} from './profiles.js';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function canonicalizeToolName(toolName) {
|
|
19
|
-
if (typeof toolName !== 'string' || !toolName.length) {
|
|
20
|
-
return toolName;
|
|
21
|
-
}
|
|
12
|
+
function escapeMultipartHeaderValue(value) {
|
|
13
|
+
return String(value ?? '')
|
|
14
|
+
.replace(/"/g, '%22')
|
|
15
|
+
.replace(/\r/g, '%0D')
|
|
16
|
+
.replace(/\n/g, '%0A');
|
|
17
|
+
}
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
function multipartTextPart(boundary, name, value) {
|
|
20
|
+
return [
|
|
21
|
+
`--${boundary}`,
|
|
22
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(name)}"`,
|
|
23
|
+
'',
|
|
24
|
+
value,
|
|
25
|
+
'',
|
|
26
|
+
].join('\r\n');
|
|
27
|
+
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
function multipartFileHeader(boundary, binding) {
|
|
30
|
+
const filename = escapeMultipartHeaderValue(binding.basename || binding.field_name);
|
|
31
|
+
return [
|
|
32
|
+
`--${boundary}`,
|
|
33
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(binding.field_name)}"; filename="${filename}"`,
|
|
34
|
+
`Content-Type: ${binding.contentType || 'application/octet-stream'}`,
|
|
35
|
+
'',
|
|
36
|
+
'',
|
|
37
|
+
].join('\r\n');
|
|
38
|
+
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
async function* multipartFileBody(boundary, payload, bindingMetadata, fileBindings) {
|
|
41
|
+
yield Buffer.from(multipartTextPart(boundary, 'payload', JSON.stringify(payload)));
|
|
42
|
+
yield Buffer.from(multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)));
|
|
43
|
+
for (const binding of fileBindings) {
|
|
44
|
+
yield Buffer.from(multipartFileHeader(boundary, binding));
|
|
45
|
+
for await (const chunk of createReadStream(binding.localPath)) {
|
|
46
|
+
yield chunk;
|
|
47
|
+
}
|
|
48
|
+
yield Buffer.from('\r\n');
|
|
33
49
|
}
|
|
50
|
+
yield Buffer.from(`--${boundary}--\r\n`);
|
|
51
|
+
}
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
|
|
54
|
+
const boundary = `----notis-cli-${randomUUID().replace(/-/g, '')}`;
|
|
55
|
+
const textParts = [
|
|
56
|
+
multipartTextPart(boundary, 'payload', JSON.stringify(payload)),
|
|
57
|
+
multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)),
|
|
58
|
+
];
|
|
59
|
+
const fileHeaderParts = fileBindings.map((binding) => multipartFileHeader(boundary, binding));
|
|
60
|
+
const closingPart = `--${boundary}--\r\n`;
|
|
61
|
+
const contentLength = [
|
|
62
|
+
...textParts,
|
|
63
|
+
...fileHeaderParts,
|
|
64
|
+
...fileBindings.map(() => '\r\n'),
|
|
65
|
+
closingPart,
|
|
66
|
+
].reduce((total, part) => total + Buffer.byteLength(part), 0)
|
|
67
|
+
+ fileBindings.reduce((total, binding) => total + binding.size, 0);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
body: multipartFileBody(boundary, payload, bindingMetadata, fileBindings),
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
73
|
+
'Content-Length': String(contentLength),
|
|
74
|
+
},
|
|
75
|
+
duplex: 'half',
|
|
76
|
+
};
|
|
36
77
|
}
|
|
37
78
|
|
|
38
79
|
function normalizeBackendError(status, payload) {
|
|
@@ -197,6 +238,7 @@ export async function httpRequest({
|
|
|
197
238
|
method = 'POST',
|
|
198
239
|
path,
|
|
199
240
|
body,
|
|
241
|
+
multipart = false,
|
|
200
242
|
requireAuth = true,
|
|
201
243
|
}) {
|
|
202
244
|
await maybeRefreshDevPortalAuth(runtime);
|
|
@@ -211,21 +253,41 @@ export async function httpRequest({
|
|
|
211
253
|
};
|
|
212
254
|
|
|
213
255
|
const headers = {
|
|
214
|
-
'Content-Type': 'application/json',
|
|
215
256
|
'X-Notis-CLI-Version': runtime.cliVersion,
|
|
216
257
|
'X-Notis-Request-Id': requestId,
|
|
217
258
|
};
|
|
259
|
+
if (!multipart) {
|
|
260
|
+
headers['Content-Type'] = 'application/json';
|
|
261
|
+
}
|
|
218
262
|
|
|
219
263
|
if (requireAuth && runtime.jwt) {
|
|
220
264
|
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
221
265
|
}
|
|
222
266
|
|
|
267
|
+
const resolveBody = () => {
|
|
268
|
+
if (!multipart) {
|
|
269
|
+
return {
|
|
270
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
271
|
+
headers: {},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (typeof body === 'function') {
|
|
275
|
+
return body();
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
body,
|
|
279
|
+
headers: {},
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
223
283
|
try {
|
|
284
|
+
let requestBody = resolveBody();
|
|
224
285
|
let response = await fetch(`${runtime.apiBase}${path}`, {
|
|
225
286
|
method,
|
|
226
|
-
headers,
|
|
227
|
-
body:
|
|
287
|
+
headers: { ...headers, ...requestBody.headers },
|
|
288
|
+
body: requestBody.body,
|
|
228
289
|
signal: controller.signal,
|
|
290
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
229
291
|
});
|
|
230
292
|
|
|
231
293
|
let payload = null;
|
|
@@ -243,11 +305,13 @@ export async function httpRequest({
|
|
|
243
305
|
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
244
306
|
}
|
|
245
307
|
resetTimeout();
|
|
308
|
+
requestBody = resolveBody();
|
|
246
309
|
response = await fetch(`${runtime.apiBase}${path}`, {
|
|
247
310
|
method,
|
|
248
|
-
headers,
|
|
249
|
-
body:
|
|
311
|
+
headers: { ...headers, ...requestBody.headers },
|
|
312
|
+
body: requestBody.body,
|
|
250
313
|
signal: controller.signal,
|
|
314
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
251
315
|
});
|
|
252
316
|
try {
|
|
253
317
|
payload = await response.json();
|
|
@@ -302,22 +366,43 @@ export async function callTool({
|
|
|
302
366
|
toolName,
|
|
303
367
|
arguments_: argumentsPayload = {},
|
|
304
368
|
idempotencyKey,
|
|
369
|
+
fileBindings = [],
|
|
305
370
|
}) {
|
|
306
371
|
const requestId = idempotencyKey || (runtime.mutating ? randomUUID() : null);
|
|
372
|
+
const payload = {
|
|
373
|
+
tool_name: toolName,
|
|
374
|
+
arguments: argumentsPayload,
|
|
375
|
+
idempotency_key: requestId,
|
|
376
|
+
cli_context: {
|
|
377
|
+
output_mode: runtime.outputMode,
|
|
378
|
+
profile: runtime.profileName,
|
|
379
|
+
cwd: process.cwd(),
|
|
380
|
+
agent_mode: runtime.agentMode,
|
|
381
|
+
cli_version: runtime.cliVersion,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
if (Array.isArray(fileBindings) && fileBindings.length) {
|
|
386
|
+
const bindingMetadata = fileBindings.map((binding) => {
|
|
387
|
+
const {
|
|
388
|
+
contentType,
|
|
389
|
+
localPath,
|
|
390
|
+
...metadata
|
|
391
|
+
} = binding;
|
|
392
|
+
return metadata;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return httpRequest({
|
|
396
|
+
runtime,
|
|
397
|
+
path: '/cli_tools',
|
|
398
|
+
body: () => createMultipartFileUpload(payload, bindingMetadata, fileBindings),
|
|
399
|
+
multipart: true,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
307
403
|
return httpRequest({
|
|
308
404
|
runtime,
|
|
309
405
|
path: '/cli_tools',
|
|
310
|
-
body:
|
|
311
|
-
tool_name: canonicalizeToolName(toolName),
|
|
312
|
-
arguments: argumentsPayload,
|
|
313
|
-
idempotency_key: requestId,
|
|
314
|
-
cli_context: {
|
|
315
|
-
output_mode: runtime.outputMode,
|
|
316
|
-
profile: runtime.profileName,
|
|
317
|
-
cwd: process.cwd(),
|
|
318
|
-
agent_mode: runtime.agentMode,
|
|
319
|
-
cli_version: runtime.cliVersion,
|
|
320
|
-
},
|
|
321
|
-
},
|
|
406
|
+
body: payload,
|
|
322
407
|
});
|
|
323
408
|
}
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"src",
|
|
15
15
|
"template"
|
|
16
16
|
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"type-check": "tsc --noEmit"
|
|
19
|
+
},
|
|
17
20
|
"peerDependencies": {
|
|
18
21
|
"react": ">=18.0.0",
|
|
19
22
|
"react-dom": ">=18.0.0",
|
|
@@ -21,6 +24,9 @@
|
|
|
21
24
|
"@vitejs/plugin-react": ">=4.0.0"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.0.0",
|
|
28
|
+
"@types/react": "^19.0.0",
|
|
29
|
+
"@types/react-dom": "^19.0.0",
|
|
24
30
|
"typescript": "^5.0.0"
|
|
25
31
|
}
|
|
26
32
|
}
|