@notis_ai/cli 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +106 -170
  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/layout.tsx +6 -0
  5. package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
  6. package/dist/scaffolds/notis-database/components/ui/badge.tsx +28 -0
  7. package/dist/scaffolds/notis-database/components/ui/button.tsx +53 -0
  8. package/dist/scaffolds/notis-database/components/ui/card.tsx +56 -0
  9. package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
  10. package/dist/scaffolds/notis-database/components.json +20 -0
  11. package/dist/scaffolds/notis-database/index.html +12 -0
  12. package/dist/scaffolds/notis-database/lib/types.ts +134 -0
  13. package/dist/scaffolds/notis-database/lib/utils.ts +6 -0
  14. package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
  15. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  16. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  17. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  18. package/dist/scaffolds/notis-database/notis.config.ts +27 -0
  19. package/dist/scaffolds/notis-database/package.json +31 -0
  20. package/dist/scaffolds/notis-database/postcss.config.mjs +8 -0
  21. package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
  22. package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
  23. package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
  24. package/dist/scaffolds/notis-database/tsconfig.json +23 -0
  25. package/dist/scaffolds/notis-database/vite.config.ts +22 -0
  26. package/dist/scaffolds/notis-notes/app/globals.css +3 -0
  27. package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
  28. package/dist/scaffolds/notis-notes/app/page.tsx +2168 -0
  29. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
  30. package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
  31. package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
  32. package/dist/scaffolds/notis-notes/components.json +20 -0
  33. package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
  34. package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
  35. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  36. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  37. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  38. package/dist/scaffolds/notis-notes/notis.config.ts +36 -0
  39. package/dist/scaffolds/notis-notes/package.json +31 -0
  40. package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
  41. package/dist/scaffolds/notis-notes/tailwind.config.ts +58 -0
  42. package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
  43. package/dist/scaffolds/notis-notes/vite.config.ts +10 -0
  44. package/dist/scaffolds/notis-random/README.md +33 -0
  45. package/dist/scaffolds/notis-random/app/globals.css +11 -0
  46. package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
  47. package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
  48. package/dist/scaffolds/notis-random/app/page.tsx +222 -0
  49. package/dist/scaffolds/notis-random/index.html +12 -0
  50. package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
  51. package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
  52. package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
  53. package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
  54. package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
  55. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  56. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  57. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  58. package/dist/scaffolds/notis-random/notis.config.ts +43 -0
  59. package/dist/scaffolds/notis-random/package.json +32 -0
  60. package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
  61. package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
  62. package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
  63. package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
  64. package/dist/scaffolds/notis-random/tsconfig.json +23 -0
  65. package/dist/scaffolds/notis-random/vite.config.ts +11 -0
  66. package/dist/scaffolds.json +36 -0
  67. package/package.json +8 -3
  68. package/skills/notis-apps/SKILL.md +162 -0
  69. package/skills/notis-apps/cli.md +208 -0
  70. package/skills/notis-cli/SKILL.md +260 -0
  71. package/skills/notis-query/cli.md +39 -0
  72. package/src/cli.js +31 -3
  73. package/src/command-specs/apps.js +248 -56
  74. package/src/command-specs/helpers.js +72 -104
  75. package/src/command-specs/index.js +0 -6
  76. package/src/command-specs/meta.js +14 -13
  77. package/src/command-specs/tools.js +196 -115
  78. package/src/runtime/app-boundary-validator.js +65 -14
  79. package/src/runtime/app-dev-server.js +32 -4
  80. package/src/runtime/app-platform.js +404 -24
  81. package/src/runtime/profiles.js +12 -6
  82. package/src/runtime/transport.js +124 -39
  83. package/template/.harness/index.html.tmpl +1 -50
  84. package/template/app/page.tsx +41 -6
  85. package/template/metadata/cover.png +0 -0
  86. package/template/metadata/screenshot-1.png +0 -0
  87. package/template/metadata/screenshot-2.png +0 -0
  88. package/template/metadata/screenshot-3.png +0 -0
  89. package/template/notis.config.ts +10 -6
  90. package/template/package.json +2 -2
  91. package/template/packages/{notis-sdk → sdk}/package.json +6 -0
  92. package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectActionBar.tsx +2 -1
  93. package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectCheckbox.tsx +2 -2
  94. package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectDragOverlay.tsx +2 -2
  95. package/template/packages/{notis-sdk → sdk}/src/config.ts +1 -1
  96. package/template/packages/{notis-sdk → sdk}/src/hooks/useDatabase.ts +1 -13
  97. package/template/packages/{notis-sdk → sdk}/src/hooks/useMultiSelect.ts +4 -3
  98. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +4 -3
  99. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +3 -3
  100. package/template/packages/{notis-sdk → sdk}/src/hooks/useTool.ts +22 -7
  101. package/template/packages/{notis-sdk → sdk}/src/hooks/useUpsertDocument.ts +0 -8
  102. package/template/packages/{notis-sdk → sdk}/src/index.ts +2 -15
  103. package/template/packages/{notis-sdk → sdk}/src/provider.tsx +1 -1
  104. package/template/packages/{notis-sdk → sdk}/src/runtime.ts +5 -26
  105. package/src/command-specs/auth.js +0 -178
  106. package/src/command-specs/db.js +0 -163
  107. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  108. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  109. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  110. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  111. /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
  112. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
  113. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTopBarSearch.ts +0 -0
  114. /package/template/packages/{notis-sdk → sdk}/src/styles.css +0 -0
  115. /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
  116. /package/template/packages/{notis-sdk → sdk}/src/vite.ts +0 -0
  117. /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
@@ -1,13 +1,18 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { accessSync, constants as fsConstants, createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { basename } from 'node:path';
2
4
  import { usageError } from '../runtime/errors.js';
3
5
  import {
6
+ COMPOSIO_GET_TOOL_SCHEMAS,
7
+ COMPOSIO_MULTI_EXECUTE_TOOL,
8
+ COMPOSIO_SEARCH_TOOLS,
9
+ fetchToolDiscovery,
4
10
  fetchToolkits,
5
11
  fetchToolSchema,
12
+ localNotisToolSlug,
6
13
  nextIdempotencyKey,
7
14
  parseJson,
8
15
  parseMaybeJson,
9
- normalizeToolkits,
10
- resolveSearchToolkits,
11
16
  runToolCommand,
12
17
  validateArguments,
13
18
  } from './helpers.js';
@@ -36,15 +41,98 @@ async function resolveJsonInput(value, label) {
36
41
 
37
42
  const LONG_RUNNING_TOOL_TIMEOUT_MS = 600000;
38
43
  const LONG_RUNNING_TOOL_NAMES = new Set([
39
- '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,69 +269,41 @@ async function toolsExecHandler(ctx) {
161
269
 
162
270
  return ctx.output.emitSuccess({
163
271
  command: ctx.spec.command_path.join(' '),
164
- data: { valid: true, tool: 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
  }
171
279
 
172
280
  const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
173
281
 
174
- if (ctx.options.watch) {
175
- const intervalSec = Math.max(1, parseInt(ctx.options.watch, 10));
176
- const intervalMs = intervalSec * 1000;
177
- process.on('SIGINT', () => process.exit(0));
178
-
179
- while (true) {
180
- try {
181
- const result = await runToolCommand({
182
- runtime: ctx.runtime,
183
- toolName: 'notis_execute_tool',
184
- arguments_: {
185
- tool_name: ctx.args.toolName,
186
- arguments: args,
187
- ...(toolkits.length ? { toolkits } : {}),
188
- },
189
- mutating: true,
190
- idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
191
- });
192
- ctx.output.emitSuccess({
193
- command: ctx.spec.command_path.join(' '),
194
- data: result.payload,
195
- humanSummary: `[${new Date().toISOString()}] ${ctx.args.toolName}`,
196
- renderHuman: () => JSON.stringify(result.payload, null, 2),
197
- });
198
- } catch (error) {
199
- process.stderr.write(`[${new Date().toISOString()}] Error: ${error.message}\n`);
200
- }
201
- await new Promise((r) => setTimeout(r, intervalMs));
202
- }
203
- }
204
-
205
282
  const result = await runToolCommand({
206
283
  runtime: ctx.runtime,
207
- toolName: 'notis_execute_tool',
284
+ toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
208
285
  arguments_: {
209
- tool_name: ctx.args.toolName,
210
- arguments: args,
211
- ...(toolkits.length ? { toolkits } : {}),
286
+ tools: [{ tool_slug: requestedToolName, arguments: args }],
212
287
  },
213
288
  mutating: true,
214
289
  idempotencyKey,
290
+ fileBindings,
215
291
  });
216
292
 
217
293
  return ctx.output.emitSuccess({
218
294
  command: ctx.spec.command_path.join(' '),
219
295
  data: result.payload,
220
- humanSummary: `Executed tool ${ctx.args.toolName}`,
296
+ humanSummary: `Executed tool ${requestedToolName}`,
221
297
  meta: { mutating: true, idempotency_key: idempotencyKey },
222
298
  renderHuman: () => JSON.stringify(result.payload, null, 2),
223
299
  });
224
300
  }
225
301
 
226
302
  async function toolsExecParallelHandler(ctx) {
303
+ const parallelFileBindings = await parseFileBindings(ctx.options.file);
304
+ if (parallelFileBindings.length) {
305
+ throw usageError('--file is supported by `notis tools exec` only; exec-parallel accepts JSON calls without file uploads.');
306
+ }
227
307
  const calls = parseMaybeJson(ctx.args.calls, 'calls');
228
308
  if (!Array.isArray(calls) || !calls.length) {
229
309
  throw usageError('calls must be a non-empty JSON array of {tool_name, arguments} objects');
@@ -234,16 +314,15 @@ async function toolsExecParallelHandler(ctx) {
234
314
  throw usageError(`calls[${i}] missing required "tool_name" string`);
235
315
  }
236
316
  }
237
- ensureLongRunningToolTimeout(ctx, calls.map((call) => call.tool_name));
317
+ ensureLongRunningToolTimeout(ctx, calls.map((call) => localNotisToolSlug(call.tool_name)));
238
318
 
239
319
  const data = await Promise.all(
240
320
  calls.map((call) =>
241
321
  runToolCommand({
242
322
  runtime: ctx.runtime,
243
- toolName: 'notis_execute_tool',
323
+ toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
244
324
  arguments_: {
245
- tool_name: call.tool_name,
246
- arguments: call.arguments || {},
325
+ tools: [{ tool_slug: localNotisToolSlug(call.tool_name), arguments: call.arguments || {} }],
247
326
  },
248
327
  mutating: true,
249
328
  idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
@@ -306,15 +385,15 @@ async function toolsLinkHandler(ctx) {
306
385
  export const toolsCommandSpecs = [
307
386
  {
308
387
  command_path: ['tools', 'toolkits'],
309
- summary: 'List toolkit namespaces available to the active user.',
310
- when_to_use: 'Use this before searching or executing generic tools.',
388
+ summary: 'List toolkit namespaces and connection statuses available to the active user.',
389
+ when_to_use: 'Use this to inspect connection state before searching or executing generic tools.',
311
390
  args_schema: { arguments: [], options: [] },
312
391
  examples: ['notis tools toolkits', 'notis tools toolkits --json'],
313
- output_schema: 'Returns available toolkit namespaces.',
392
+ output_schema: 'Returns toolkit_connection_statuses from COMPOSIO_SEARCH_TOOLS, rendered as toolkit rows.',
314
393
  mutates: false,
315
394
  idempotent: true,
316
395
  related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>'],
317
- backend_call: { type: 'tool', name: 'notis_find_toolkits' },
396
+ backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
318
397
  handler: toolsToolkitsHandler,
319
398
  },
320
399
  {
@@ -323,14 +402,14 @@ export const toolsCommandSpecs = [
323
402
  when_to_use: 'Use this when you need a generic capability that does not have a first-class CLI command.',
324
403
  args_schema: {
325
404
  arguments: [{ token: '<query>', description: 'Natural language description of the tool you need.' }],
326
- options: [{ flags: '--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.' }],
327
406
  },
328
- examples: ['notis tools search "send an email"', 'notis tools search "update framer page" --toolkits mcp-framer'],
329
- 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.',
330
409
  mutates: false,
331
410
  idempotent: true,
332
411
  related_commands: ['notis tools toolkits', 'notis tools exec <tool-name>'],
333
- backend_call: { type: 'tool', name: 'notis_find_tools' },
412
+ backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
334
413
  handler: toolsSearchHandler,
335
414
  },
336
415
  {
@@ -339,14 +418,14 @@ export const toolsCommandSpecs = [
339
418
  when_to_use: 'Use this when you know the tool name and want its parameter schema before execution.',
340
419
  args_schema: {
341
420
  arguments: [{ token: '<tool-name>', description: 'Exact tool name to locate and describe.' }],
342
- options: [{ flags: '--toolkits <csv-or-json>', description: 'Optional subset of toolkit ids to search.' }],
421
+ options: [],
343
422
  },
344
- 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'],
345
424
  output_schema: 'Returns one tool descriptor with name, description, and parameters.',
346
425
  mutates: false,
347
426
  idempotent: true,
348
427
  related_commands: ['notis tools search <query>', 'notis tools exec <tool-name>'],
349
- backend_call: { type: 'tool', name: 'notis_find_tools' },
428
+ backend_call: { type: 'tool', name: COMPOSIO_GET_TOOL_SCHEMAS },
350
429
  handler: toolsDescribeHandler,
351
430
  },
352
431
  {
@@ -357,25 +436,25 @@ export const toolsCommandSpecs = [
357
436
  arguments: [{ token: '<tool-name>', description: 'Tool name returned by `notis tools search`.' }],
358
437
  options: [
359
438
  { flags: '--arguments <json>', description: 'JSON object, @file path, or - for stdin.' },
439
+ { flags: '--file <argument-path=local-path>', description: 'Upload a local file into a file-uploadable tool argument. Repeatable.', collect: true },
360
440
  { flags: '--get-schema', description: 'Display the tool parameter schema without executing.' },
361
441
  { flags: '--dry-run', description: 'Validate arguments against the tool schema without executing.' },
362
- { flags: '--watch <seconds>', description: 'Re-execute on an interval and stream results.' },
363
- { flags: '--toolkits <csv-or-json>', description: 'Optional toolkit namespace(s) to use when resolving the tool.' },
364
442
  ],
365
443
  },
366
444
  examples: [
367
- 'notis tools exec notis-default-query --arguments \'{"database_slug":"tasks","query":{}}\'',
368
- 'notis tools exec notis-default-query --get-schema',
369
- 'notis tools exec notis-default-query --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
370
- 'notis tools exec notis-default-query --arguments @query.json',
371
- 'notis tools exec notis-default-query --arguments - < query.json',
372
- 'notis tools exec notis-default-query --watch 10 --arguments \'{"database_slug":"tasks","query":{}}\'',
445
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments \'{"database_slug":"tasks","query":{}}\'',
446
+ 'notis tools exec LOCAL_NOTIS_DATABASE_GET_DATABASE --arguments \'{"database_slug":"tasks"}\'',
447
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --get-schema',
448
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
449
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments @query.json',
450
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments - < query.json',
451
+ 'notis tools exec composio-dropbox-upload_file --arguments \'{"path":"/target/in/dropbox.pdf"}\' --file content=./Invoice.pdf',
373
452
  ],
374
453
  output_schema: 'Returns the raw tool execution payload.',
375
454
  mutates: true,
376
455
  idempotent: true,
377
456
  related_commands: ['notis tools search <query>', 'notis tools describe <tool-name>', 'notis tools exec-parallel <calls>'],
378
- backend_call: { type: 'tool', name: 'notis_execute_tool' },
457
+ backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
379
458
  handler: toolsExecHandler,
380
459
  },
381
460
  {
@@ -386,16 +465,18 @@ export const toolsCommandSpecs = [
386
465
  arguments: [
387
466
  { token: '<calls>', description: 'JSON array of {tool_name, arguments} objects.' },
388
467
  ],
389
- options: [],
468
+ options: [
469
+ { flags: '--file <argument-path=local-path>', description: 'Unsupported for exec-parallel; use tools exec for file uploads.', collect: true },
470
+ ],
390
471
  },
391
472
  examples: [
392
- 'notis tools exec-parallel \'[{"tool_name":"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":{}}]\'',
393
474
  ],
394
475
  output_schema: 'Returns an array of results, one per tool call.',
395
476
  mutates: true,
396
477
  idempotent: true,
397
478
  related_commands: ['notis tools exec <tool-name>', 'notis tools search <query>'],
398
- backend_call: { type: 'tool', name: 'notis_execute_tool' },
479
+ backend_call: { type: 'tool', name: COMPOSIO_MULTI_EXECUTE_TOOL },
399
480
  handler: toolsExecParallelHandler,
400
481
  },
401
482
  {
@@ -4,10 +4,30 @@ import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import { usageError } from './errors.js';
6
6
 
7
- const 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
  }