@notis_ai/cli 0.2.2 → 0.2.4

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 (81) hide show
  1. package/README.md +19 -18
  2. package/config/notis_app_boundary_rules.json +50 -0
  3. package/dist/scaffolds/notis-database/app/globals.css +44 -0
  4. package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
  5. package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
  6. package/dist/scaffolds/notis-database/index.html +12 -0
  7. package/dist/scaffolds/notis-database/lib/types.ts +134 -0
  8. package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
  9. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  10. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  11. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  12. package/dist/scaffolds/notis-database/notis.config.ts +27 -0
  13. package/dist/scaffolds/notis-database/package.json +31 -0
  14. package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
  15. package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
  16. package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
  17. package/dist/scaffolds/{notes → notis-database}/tsconfig.json +2 -1
  18. package/dist/scaffolds/notis-database/vite.config.ts +22 -0
  19. package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
  20. package/dist/scaffolds/{notes → notis-notes}/app/page.tsx +783 -80
  21. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
  22. package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
  23. package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
  24. package/dist/scaffolds/notis-notes/components.json +20 -0
  25. package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
  26. package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
  27. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  28. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  29. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  30. package/dist/scaffolds/{notes → notis-notes}/notis.config.ts +12 -7
  31. package/dist/scaffolds/{notes → notis-notes}/package.json +2 -2
  32. package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
  33. package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
  34. package/dist/scaffolds/notis-random/README.md +33 -0
  35. package/dist/scaffolds/notis-random/app/globals.css +11 -0
  36. package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
  37. package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
  38. package/dist/scaffolds/notis-random/app/page.tsx +222 -0
  39. package/dist/scaffolds/notis-random/index.html +12 -0
  40. package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
  41. package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
  42. package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
  43. package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
  44. package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
  45. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  46. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  47. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  48. package/dist/scaffolds/notis-random/notis.config.ts +43 -0
  49. package/dist/scaffolds/notis-random/package.json +32 -0
  50. package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
  51. package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
  52. package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
  53. package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
  54. package/dist/scaffolds/notis-random/tsconfig.json +23 -0
  55. package/dist/scaffolds/notis-random/vite.config.ts +11 -0
  56. package/dist/scaffolds.json +27 -4
  57. package/package.json +3 -1
  58. package/skills/notis-apps/SKILL.md +2 -2
  59. package/skills/notis-cli/SKILL.md +18 -16
  60. package/skills/notis-query/cli.md +9 -10
  61. package/src/cli.js +30 -2
  62. package/src/command-specs/apps.js +42 -15
  63. package/src/command-specs/helpers.js +73 -45
  64. package/src/command-specs/meta.js +7 -7
  65. package/src/command-specs/tools.js +196 -83
  66. package/src/runtime/app-boundary-validator.js +65 -14
  67. package/src/runtime/profiles.js +10 -4
  68. package/src/runtime/transport.js +122 -37
  69. package/template/app/page.tsx +1 -1
  70. package/template/notis.config.ts +1 -1
  71. package/template/packages/sdk/src/hooks/useTool.ts +1 -1
  72. /package/dist/scaffolds/{notes → notis-database}/app/layout.tsx +0 -0
  73. /package/dist/scaffolds/{notes → notis-database}/components/ui/badge.tsx +0 -0
  74. /package/dist/scaffolds/{notes → notis-database}/components/ui/button.tsx +0 -0
  75. /package/dist/scaffolds/{notes → notis-database}/components/ui/card.tsx +0 -0
  76. /package/dist/scaffolds/{notes → notis-database}/components.json +0 -0
  77. /package/dist/scaffolds/{notes → notis-database}/lib/utils.ts +0 -0
  78. /package/dist/scaffolds/{notes → notis-database}/postcss.config.mjs +0 -0
  79. /package/dist/scaffolds/{notes → notis-notes}/app/globals.css +0 -0
  80. /package/dist/scaffolds/{notes → notis-notes}/tailwind.config.ts +0 -0
  81. /package/dist/scaffolds/{notes → notis-notes}/vite.config.ts +0 -0
@@ -1,13 +1,18 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { accessSync, constants as fsConstants, createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { basename } from 'node:path';
2
4
  import { usageError } from '../runtime/errors.js';
3
5
  import {
6
+ COMPOSIO_GET_TOOL_SCHEMAS,
7
+ COMPOSIO_MULTI_EXECUTE_TOOL,
8
+ COMPOSIO_SEARCH_TOOLS,
9
+ fetchToolDiscovery,
4
10
  fetchToolkits,
5
11
  fetchToolSchema,
12
+ localNotisToolSlug,
6
13
  nextIdempotencyKey,
7
14
  parseJson,
8
15
  parseMaybeJson,
9
- normalizeToolkits,
10
- resolveSearchToolkits,
11
16
  runToolCommand,
12
17
  validateArguments,
13
18
  } from './helpers.js';
@@ -36,15 +41,98 @@ async function resolveJsonInput(value, label) {
36
41
 
37
42
  const LONG_RUNNING_TOOL_TIMEOUT_MS = 600000;
38
43
  const LONG_RUNNING_TOOL_NAMES = new Set([
39
- 'notis-default-generate_image_openai',
40
- 'notis-default-edit_image_openai',
41
- 'notis-default-generate_image_gemini',
42
- 'notis-default-edit_image_gemini',
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 for tools in these toolkits' },
148
+ { command: 'notis tools search <query>', reason: 'Search canonical tools and schemas' },
61
149
  ],
62
- renderHuman: () => toolkits.map((entry) => `${entry.id} ${entry.description}`).join('\n'),
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 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 || [];
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: { toolkits, tools },
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.toolkit_id || 'unknown toolkit'}\n ${tool.description || 'No description'}\n`)
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 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
- }
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, toolkits },
115
- humanSummary: `Described tool ${ctx.args.toolName}`,
218
+ data: { tool: match },
219
+ humanSummary: `Described tool ${requestedToolName}`,
116
220
  hints: [
117
- { command: `notis tools exec ${ctx.args.toolName} --dry-run --arguments '{}'`, reason: 'Validate arguments first' },
118
- { command: `notis tools exec ${ctx.args.toolName} --arguments '{}'`, reason: 'Execute this tool' },
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
- ensureLongRunningToolTimeout(ctx, ctx.args.toolName);
126
- const toolkits = normalizeToolkits(ctx.options.toolkits);
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, ctx.args.toolName, ctx.options.toolkits);
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, toolkits },
133
- humanSummary: `Schema for ${ctx.args.toolName}`,
237
+ data: { tool },
238
+ humanSummary: `Schema for ${requestedToolName}`,
134
239
  hints: [
135
- { command: `notis tools exec ${ctx.args.toolName} --dry-run --arguments '{}'`, reason: 'Validate arguments before executing' },
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
- const tool = await fetchToolSchema(ctx.runtime, ctx.args.toolName, ctx.options.toolkits);
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: ctx.args.toolName, toolkits },
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 ${ctx.args.toolName} --get-schema`, reason: 'Review the full parameter schema' },
264
+ { command: `notis tools exec ${requestedToolName} --get-schema`, reason: 'Review the full parameter schema' },
157
265
  ],
158
266
  renderHuman: () => errors.map((e) => ` - ${e}`).join('\n'),
159
267
  });
@@ -161,10 +269,10 @@ async function toolsExecHandler(ctx) {
161
269
 
162
270
  return ctx.output.emitSuccess({
163
271
  command: ctx.spec.command_path.join(' '),
164
- data: { valid: true, tool: ctx.args.toolName, arguments: args, toolkits },
165
- humanSummary: `Dry run passed for ${ctx.args.toolName}`,
272
+ data: { valid: true, tool: requestedToolName, arguments: args },
273
+ humanSummary: `Dry run passed for ${requestedToolName}`,
166
274
  hints: [
167
- { command: `notis tools exec ${ctx.args.toolName} --arguments '${JSON.stringify(args)}'`, reason: 'Execute with these arguments' },
275
+ { command: `notis tools exec ${requestedToolName} --arguments '${JSON.stringify(args)}'`, reason: 'Execute with these arguments' },
168
276
  ],
169
277
  });
170
278
  }
@@ -173,26 +281,29 @@ async function toolsExecHandler(ctx) {
173
281
 
174
282
  const result = await runToolCommand({
175
283
  runtime: ctx.runtime,
176
- toolName: 'notis_execute_tool',
284
+ toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
177
285
  arguments_: {
178
- tool_name: ctx.args.toolName,
179
- arguments: args,
180
- ...(toolkits.length ? { toolkits } : {}),
286
+ tools: [{ tool_slug: requestedToolName, arguments: args }],
181
287
  },
182
288
  mutating: true,
183
289
  idempotencyKey,
290
+ fileBindings,
184
291
  });
185
292
 
186
293
  return ctx.output.emitSuccess({
187
294
  command: ctx.spec.command_path.join(' '),
188
295
  data: result.payload,
189
- humanSummary: `Executed tool ${ctx.args.toolName}`,
296
+ humanSummary: `Executed tool ${requestedToolName}`,
190
297
  meta: { mutating: true, idempotency_key: idempotencyKey },
191
298
  renderHuman: () => JSON.stringify(result.payload, null, 2),
192
299
  });
193
300
  }
194
301
 
195
302
  async function toolsExecParallelHandler(ctx) {
303
+ const parallelFileBindings = await parseFileBindings(ctx.options.file);
304
+ if (parallelFileBindings.length) {
305
+ throw usageError('--file is supported by `notis tools exec` only; exec-parallel accepts JSON calls without file uploads.');
306
+ }
196
307
  const calls = parseMaybeJson(ctx.args.calls, 'calls');
197
308
  if (!Array.isArray(calls) || !calls.length) {
198
309
  throw usageError('calls must be a non-empty JSON array of {tool_name, arguments} objects');
@@ -203,16 +314,15 @@ async function toolsExecParallelHandler(ctx) {
203
314
  throw usageError(`calls[${i}] missing required "tool_name" string`);
204
315
  }
205
316
  }
206
- ensureLongRunningToolTimeout(ctx, calls.map((call) => call.tool_name));
317
+ ensureLongRunningToolTimeout(ctx, calls.map((call) => localNotisToolSlug(call.tool_name)));
207
318
 
208
319
  const data = await Promise.all(
209
320
  calls.map((call) =>
210
321
  runToolCommand({
211
322
  runtime: ctx.runtime,
212
- toolName: 'notis_execute_tool',
323
+ toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
213
324
  arguments_: {
214
- tool_name: call.tool_name,
215
- arguments: call.arguments || {},
325
+ tools: [{ tool_slug: localNotisToolSlug(call.tool_name), arguments: call.arguments || {} }],
216
326
  },
217
327
  mutating: true,
218
328
  idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
@@ -275,15 +385,15 @@ async function toolsLinkHandler(ctx) {
275
385
  export const toolsCommandSpecs = [
276
386
  {
277
387
  command_path: ['tools', 'toolkits'],
278
- summary: 'List toolkit namespaces available to the active user.',
279
- when_to_use: 'Use this before searching or executing generic tools.',
388
+ summary: 'List toolkit namespaces and connection statuses available to the active user.',
389
+ when_to_use: 'Use this to inspect connection state before searching or executing generic tools.',
280
390
  args_schema: { arguments: [], options: [] },
281
391
  examples: ['notis tools toolkits', 'notis tools toolkits --json'],
282
- output_schema: 'Returns available toolkit namespaces.',
392
+ output_schema: 'Returns toolkit_connection_statuses from COMPOSIO_SEARCH_TOOLS, rendered as toolkit rows.',
283
393
  mutates: false,
284
394
  idempotent: true,
285
395
  related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>'],
286
- backend_call: { type: 'tool', name: 'notis_find_toolkits' },
396
+ backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
287
397
  handler: toolsToolkitsHandler,
288
398
  },
289
399
  {
@@ -292,14 +402,14 @@ export const toolsCommandSpecs = [
292
402
  when_to_use: 'Use this when you need a generic capability that does not have a first-class CLI command.',
293
403
  args_schema: {
294
404
  arguments: [{ token: '<query>', description: 'Natural language description of the tool you need.' }],
295
- options: [{ flags: '--toolkits <csv-or-json>', description: 'Optional subset of toolkit ids to search.' }],
405
+ options: [{ flags: '--known-fields <text>', key: 'knownFields', description: 'Optional known field hints, such as channel_name:general or user_email:a@example.com.' }],
296
406
  },
297
- examples: ['notis tools search "send an email"', 'notis tools search "update framer page" --toolkits mcp-framer'],
298
- output_schema: 'Returns tool matches with descriptions and parameter schemas.',
407
+ examples: ['notis tools search "send an email"', 'notis tools search "post on LinkedIn" --known-fields "platform:linkedin"'],
408
+ output_schema: 'Returns the full COMPOSIO_SEARCH_TOOLS response. Human output renders canonical names, connection status, and schema availability.',
299
409
  mutates: false,
300
410
  idempotent: true,
301
411
  related_commands: ['notis tools toolkits', 'notis tools exec <tool-name>'],
302
- backend_call: { type: 'tool', name: 'notis_find_tools' },
412
+ backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
303
413
  handler: toolsSearchHandler,
304
414
  },
305
415
  {
@@ -308,14 +418,14 @@ export const toolsCommandSpecs = [
308
418
  when_to_use: 'Use this when you know the tool name and want its parameter schema before execution.',
309
419
  args_schema: {
310
420
  arguments: [{ token: '<tool-name>', description: 'Exact tool name to locate and describe.' }],
311
- options: [{ flags: '--toolkits <csv-or-json>', description: 'Optional subset of toolkit ids to search.' }],
421
+ options: [],
312
422
  },
313
- examples: ['notis tools describe composio-gmail-default-send_email', 'notis tools describe notis-default-query --toolkits notis-default'],
423
+ examples: ['notis tools describe composio-gmail-send_email', 'notis tools describe LOCAL_NOTIS_DATABASE_QUERY'],
314
424
  output_schema: 'Returns one tool descriptor with name, description, and parameters.',
315
425
  mutates: false,
316
426
  idempotent: true,
317
427
  related_commands: ['notis tools search <query>', 'notis tools exec <tool-name>'],
318
- backend_call: { type: 'tool', name: 'notis_find_tools' },
428
+ backend_call: { type: 'tool', name: COMPOSIO_GET_TOOL_SCHEMAS },
319
429
  handler: toolsDescribeHandler,
320
430
  },
321
431
  {
@@ -326,24 +436,25 @@ export const toolsCommandSpecs = [
326
436
  arguments: [{ token: '<tool-name>', description: 'Tool name returned by `notis tools search`.' }],
327
437
  options: [
328
438
  { flags: '--arguments <json>', description: 'JSON object, @file path, or - for stdin.' },
439
+ { flags: '--file <argument-path=local-path>', description: 'Upload a local file into a file-uploadable tool argument. Repeatable.', collect: true },
329
440
  { flags: '--get-schema', description: 'Display the tool parameter schema without executing.' },
330
441
  { flags: '--dry-run', description: 'Validate arguments against the tool schema without executing.' },
331
- { flags: '--toolkits <csv-or-json>', description: 'Optional toolkit namespace(s) to use when resolving the tool.' },
332
442
  ],
333
443
  },
334
444
  examples: [
335
- 'notis tools exec notis-default-query --arguments \'{"database_slug":"tasks","query":{}}\'',
336
- 'notis tools exec notis-default-get_database --arguments \'{"database_slug":"tasks"}\'',
337
- 'notis tools exec notis-default-query --get-schema',
338
- 'notis tools exec notis-default-query --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
339
- 'notis tools exec notis-default-query --arguments @query.json',
340
- 'notis tools exec notis-default-query --arguments - < query.json',
445
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments \'{"database_slug":"tasks","query":{}}\'',
446
+ 'notis tools exec LOCAL_NOTIS_DATABASE_GET_DATABASE --arguments \'{"database_slug":"tasks"}\'',
447
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --get-schema',
448
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
449
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments @query.json',
450
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments - < query.json',
451
+ 'notis tools exec composio-dropbox-upload_file --arguments \'{"path":"/target/in/dropbox.pdf"}\' --file content=./Invoice.pdf',
341
452
  ],
342
453
  output_schema: 'Returns the raw tool execution payload.',
343
454
  mutates: true,
344
455
  idempotent: true,
345
456
  related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>', 'notis tools exec-parallel <calls>'],
346
- backend_call: { type: 'tool', name: 'notis_execute_tool' },
457
+ backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
347
458
  handler: toolsExecHandler,
348
459
  },
349
460
  {
@@ -354,16 +465,18 @@ export const toolsCommandSpecs = [
354
465
  arguments: [
355
466
  { token: '<calls>', description: 'JSON array of {tool_name, arguments} objects.' },
356
467
  ],
357
- options: [],
468
+ options: [
469
+ { flags: '--file <argument-path=local-path>', description: 'Unsupported for exec-parallel; use tools exec for file uploads.', collect: true },
470
+ ],
358
471
  },
359
472
  examples: [
360
- 'notis tools exec-parallel \'[{"tool_name":"notis-default-query","arguments":{"database_slug":"tasks","query":{}}},{"tool_name":"notis-default-list_databases","arguments":{}}]\'',
473
+ 'notis tools exec-parallel \'[{"tool_name":"LOCAL_NOTIS_DATABASE_QUERY","arguments":{"database_slug":"tasks","query":{}}},{"tool_name":"LOCAL_NOTIS_DATABASE_LIST_DATABASES","arguments":{}}]\'',
361
474
  ],
362
475
  output_schema: 'Returns an array of results, one per tool call.',
363
476
  mutates: true,
364
477
  idempotent: true,
365
478
  related_commands: ['notis tools exec <tool-name>', 'notis tools search <query>'],
366
- backend_call: { type: 'tool', name: 'notis_execute_tool' },
479
+ backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
367
480
  handler: toolsExecParallelHandler,
368
481
  },
369
482
  {
@@ -4,10 +4,30 @@ import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import { usageError } from './errors.js';
6
6
 
7
- const RULES_PATH = resolve(
8
- dirname(fileURLToPath(import.meta.url)),
9
- '../../../../server/config/notis_app_boundary_rules.json',
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
- return (entries || []).map((entry) => ({
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
- const boundaryRules = loadBoundaryRules();
35
- const javascriptRules = compileRules(boundaryRules.javascript);
36
- const cssRules = compileRules(boundaryRules.css);
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, cssRules, relPath);
136
+ return applyRules(content, rules.css, relPath);
86
137
  }
87
138
  if (extension === '.js' || extension === '.jsx' || extension === '.ts' || extension === '.tsx') {
88
- return applyRules(content, javascriptRules, relPath);
139
+ return applyRules(content, rules.javascript, relPath);
89
140
  }
90
141
  return [];
91
142
  }
@@ -212,15 +212,21 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
212
212
  });
213
213
  }
214
214
 
215
+ // An explicit NOTIS_JWT is a complete credential override: use it verbatim and do
216
+ // NOT attempt a stored-profile dev_portal token refresh (whose refresh_token may be
217
+ // stale or belong to a different session). Without this, a long-lived shell with an
218
+ // expired profile refresh_token fails every command with invalid_grant even though
219
+ // the supplied NOTIS_JWT is valid.
220
+ const usingEnvJwt = Boolean(process.env.NOTIS_JWT);
215
221
  return {
216
222
  config,
217
223
  profileName,
218
224
  apiBase,
219
225
  jwt,
220
- authMode: profile.auth_mode,
221
- refreshToken: profile.refresh_token,
222
- accessExpiresAt: profile.access_expires_at,
223
- refreshExpiresAt: profile.refresh_expires_at,
226
+ authMode: usingEnvJwt ? 'jwt' : profile.auth_mode,
227
+ refreshToken: usingEnvJwt ? undefined : profile.refresh_token,
228
+ accessExpiresAt: usingEnvJwt ? undefined : profile.access_expires_at,
229
+ refreshExpiresAt: usingEnvJwt ? undefined : profile.refresh_expires_at,
224
230
  agentMode,
225
231
  nonInteractive,
226
232
  outputMode,