@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.
Files changed (84) hide show
  1. package/README.md +84 -83
  2. package/dist/scaffolds/notis-database/app/globals.css +44 -0
  3. package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
  4. package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
  5. package/dist/scaffolds/notis-database/index.html +12 -0
  6. package/dist/scaffolds/notis-database/lib/types.ts +134 -0
  7. package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
  8. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  9. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  10. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  11. package/dist/scaffolds/notis-database/notis.config.ts +27 -0
  12. package/dist/scaffolds/notis-database/package.json +31 -0
  13. package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
  14. package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
  15. package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
  16. package/dist/scaffolds/{notes → notis-database}/tsconfig.json +2 -1
  17. package/dist/scaffolds/notis-database/vite.config.ts +22 -0
  18. package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
  19. package/dist/scaffolds/{notes → notis-notes}/app/page.tsx +783 -80
  20. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
  21. package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
  22. package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
  23. package/dist/scaffolds/notis-notes/components.json +20 -0
  24. package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
  25. package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
  26. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  27. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  28. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  29. package/dist/scaffolds/{notes → notis-notes}/notis.config.ts +12 -7
  30. package/dist/scaffolds/{notes → notis-notes}/package.json +3 -3
  31. package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
  32. package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
  33. package/dist/scaffolds/notis-random/README.md +33 -0
  34. package/dist/scaffolds/notis-random/app/globals.css +11 -0
  35. package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
  36. package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
  37. package/dist/scaffolds/notis-random/app/page.tsx +222 -0
  38. package/dist/scaffolds/notis-random/index.html +12 -0
  39. package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
  40. package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
  41. package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
  42. package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
  43. package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
  44. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  45. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  46. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  47. package/dist/scaffolds/notis-random/notis.config.ts +43 -0
  48. package/dist/scaffolds/notis-random/package.json +32 -0
  49. package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
  50. package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
  51. package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
  52. package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
  53. package/dist/scaffolds/notis-random/tsconfig.json +23 -0
  54. package/dist/scaffolds/notis-random/vite.config.ts +11 -0
  55. package/dist/scaffolds.json +27 -4
  56. package/package.json +1 -1
  57. package/skills/notis-apps/SKILL.md +27 -33
  58. package/skills/notis-apps/cli.md +51 -51
  59. package/skills/notis-cli/SKILL.md +66 -71
  60. package/skills/notis-query/cli.md +11 -11
  61. package/src/cli.js +10 -1
  62. package/src/command-specs/apps.js +9 -9
  63. package/src/command-specs/helpers.js +69 -46
  64. package/src/command-specs/meta.js +6 -6
  65. package/src/command-specs/tools.js +159 -50
  66. package/src/runtime/app-dev-server.js +2 -2
  67. package/src/runtime/app-platform.js +28 -0
  68. package/src/runtime/transport.js +122 -37
  69. package/template/packages/sdk/package.json +6 -0
  70. package/template/packages/sdk/src/hooks/useDatabase.ts +76 -0
  71. package/template/packages/sdk/src/hooks/useNotis.ts +10 -6
  72. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
  73. package/template/packages/sdk/src/index.ts +9 -1
  74. package/template/packages/sdk/src/runtime.ts +83 -2
  75. /package/dist/scaffolds/{notes → notis-database}/app/layout.tsx +0 -0
  76. /package/dist/scaffolds/{notes → notis-database}/components/ui/badge.tsx +0 -0
  77. /package/dist/scaffolds/{notes → notis-database}/components/ui/button.tsx +0 -0
  78. /package/dist/scaffolds/{notes → notis-database}/components/ui/card.tsx +0 -0
  79. /package/dist/scaffolds/{notes → notis-database}/components.json +0 -0
  80. /package/dist/scaffolds/{notes → notis-database}/lib/utils.ts +0 -0
  81. /package/dist/scaffolds/{notes → notis-database}/postcss.config.mjs +0 -0
  82. /package/dist/scaffolds/{notes → notis-notes}/app/globals.css +0 -0
  83. /package/dist/scaffolds/{notes → notis-notes}/tailwind.config.ts +0 -0
  84. /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 for tools in these toolkits' },
144
+ { command: 'notis tools search <query>', reason: 'Search canonical tools and schemas' },
61
145
  ],
62
- renderHuman: () => toolkits.map((entry) => `${entry.id} ${entry.description}`).join('\n'),
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 toolkits = await resolveSearchToolkits(ctx.runtime, ctx.options.toolkits);
68
- const result = await runToolCommand({
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: { toolkits, tools },
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.toolkit_id || 'unknown toolkit'}\n ${tool.description || 'No description'}\n`)
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 toolkits = await resolveSearchToolkits(ctx.runtime, ctx.options.toolkits);
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, toolkits },
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 toolkits = normalizeToolkits(ctx.options.toolkits);
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, ctx.options.toolkits);
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, toolkits },
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
- const tool = await fetchToolSchema(ctx.runtime, ctx.args.toolName, ctx.options.toolkits);
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, toolkits },
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, toolkits },
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 available toolkit namespaces.',
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: 'notis_find_toolkits' },
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: '--toolkits <csv-or-json>', description: 'Optional subset of toolkit ids to search.' }],
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 "update framer page" --toolkits mcp-framer'],
298
- output_schema: 'Returns tool matches with descriptions and parameter schemas.',
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: [{ flags: '--toolkits <csv-or-json>', description: 'Optional subset of toolkit ids to search.' }],
417
+ options: [],
312
418
  },
313
- examples: ['notis tools describe composio-gmail-default-send_email', 'notis tools describe notis-default-query --toolkits notis-default'],
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', 'notis-sdk', 'package.json'),
118
- join(CLI_ROOT, 'template', 'packages', 'notis-sdk', 'package.json'),
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)) {
@@ -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
- const META_TOOL_NAMES = new Set([
12
- 'notis_find_toolkits',
13
- 'notis_find_tools',
14
- 'notis_execute_tool',
15
- 'notis_find_additional_tools',
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
- if (META_TOOL_NAMES.has(toolName) || toolName.startsWith('notis-default-')) {
24
- return toolName;
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
- if (toolName.startsWith('notis_')) {
28
- return `notis-default-${toolName.slice('notis_'.length)}`;
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
- if (toolName.includes('_notis_')) {
32
- return `notis-default-${toolName.replace(/_notis_/g, '_')}`;
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
- return toolName;
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: body ? JSON.stringify(body) : undefined,
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: body ? JSON.stringify(body) : undefined,
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
  }