skedyul 1.0.2 → 1.0.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.
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.invokeRemoteCommand = invokeRemoteCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const utils_1 = require("../utils");
40
+ const auth_1 = require("../utils/auth");
41
+ /**
42
+ * Simple MIME type lookup based on file extension.
43
+ */
44
+ function getMimeType(filePath) {
45
+ const ext = path.extname(filePath).toLowerCase();
46
+ const mimeTypes = {
47
+ '.pdf': 'application/pdf',
48
+ '.jpg': 'image/jpeg',
49
+ '.jpeg': 'image/jpeg',
50
+ '.png': 'image/png',
51
+ '.gif': 'image/gif',
52
+ '.webp': 'image/webp',
53
+ '.svg': 'image/svg+xml',
54
+ '.txt': 'text/plain',
55
+ '.html': 'text/html',
56
+ '.htm': 'text/html',
57
+ '.css': 'text/css',
58
+ '.js': 'application/javascript',
59
+ '.json': 'application/json',
60
+ '.xml': 'application/xml',
61
+ '.csv': 'text/csv',
62
+ '.doc': 'application/msword',
63
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
64
+ '.xls': 'application/vnd.ms-excel',
65
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
66
+ '.zip': 'application/zip',
67
+ '.mp3': 'audio/mpeg',
68
+ '.mp4': 'video/mp4',
69
+ '.wav': 'audio/wav',
70
+ };
71
+ return mimeTypes[ext] || 'application/octet-stream';
72
+ }
73
+ /**
74
+ * Upload a local file to the platform and return its file ID.
75
+ */
76
+ async function uploadLocalFile(filePath, serverUrl, token, appInstallationId) {
77
+ const absolutePath = path.resolve(filePath);
78
+ if (!fs.existsSync(absolutePath)) {
79
+ throw new Error(`File not found: ${absolutePath}`);
80
+ }
81
+ const content = fs.readFileSync(absolutePath);
82
+ const fileName = path.basename(absolutePath);
83
+ const mimeType = getMimeType(absolutePath);
84
+ console.error(`Uploading file: ${fileName} (${mimeType}, ${content.length} bytes)...`);
85
+ const url = `${serverUrl}/api/cli/upload-file`;
86
+ const response = await fetch(url, {
87
+ method: 'POST',
88
+ headers: {
89
+ 'Content-Type': 'application/json',
90
+ 'Authorization': `Bearer ${token}`,
91
+ },
92
+ body: JSON.stringify({
93
+ appInstallationId,
94
+ content: content.toString('base64'),
95
+ name: fileName,
96
+ mimeType,
97
+ }),
98
+ });
99
+ const text = await response.text();
100
+ let json;
101
+ try {
102
+ json = JSON.parse(text);
103
+ }
104
+ catch {
105
+ if (text.startsWith('<!DOCTYPE') || text.startsWith('<html')) {
106
+ throw new Error(`Server returned HTML instead of JSON (status ${response.status}). ` +
107
+ `This usually means the API endpoint doesn't exist or there's a server error.`);
108
+ }
109
+ throw new Error(`Invalid response from server: ${text.substring(0, 200)}`);
110
+ }
111
+ if (!response.ok) {
112
+ const errorResponse = json;
113
+ throw new Error(errorResponse.error ?? `Upload failed: ${response.status}`);
114
+ }
115
+ const uploadResponse = json;
116
+ if (!uploadResponse.success || !uploadResponse.id) {
117
+ throw new Error(uploadResponse.error ?? 'Upload failed: no file ID returned');
118
+ }
119
+ console.error(`Uploaded: ${fileName} -> ${uploadResponse.id}`);
120
+ return uploadResponse.id;
121
+ }
122
+ /**
123
+ * Process upload templates in args object.
124
+ * Recursively scans all string values and replaces {{upload:/path/to/file}} patterns
125
+ * with the uploaded file ID.
126
+ */
127
+ async function processUploadTemplates(args, serverUrl, token, appInstallationId) {
128
+ const result = {};
129
+ for (const [key, value] of Object.entries(args)) {
130
+ if (typeof value === 'string') {
131
+ const match = value.match(/^\{\{upload:(.+)\}\}$/);
132
+ if (match) {
133
+ const filePath = match[1];
134
+ const fileId = await uploadLocalFile(filePath, serverUrl, token, appInstallationId);
135
+ result[key] = fileId;
136
+ }
137
+ else {
138
+ result[key] = value;
139
+ }
140
+ }
141
+ else if (Array.isArray(value)) {
142
+ result[key] = await Promise.all(value.map(async (item) => {
143
+ if (typeof item === 'string') {
144
+ const match = item.match(/^\{\{upload:(.+)\}\}$/);
145
+ if (match) {
146
+ return await uploadLocalFile(match[1], serverUrl, token, appInstallationId);
147
+ }
148
+ }
149
+ else if (item && typeof item === 'object') {
150
+ return await processUploadTemplates(item, serverUrl, token, appInstallationId);
151
+ }
152
+ return item;
153
+ }));
154
+ }
155
+ else if (value && typeof value === 'object') {
156
+ result[key] = await processUploadTemplates(value, serverUrl, token, appInstallationId);
157
+ }
158
+ else {
159
+ result[key] = value;
160
+ }
161
+ }
162
+ return result;
163
+ }
164
+ async function callInvokeApi(serverUrl, token, body) {
165
+ const url = `${serverUrl}/api/cli/invoke-tool`;
166
+ const response = await fetch(url, {
167
+ method: 'POST',
168
+ headers: {
169
+ 'Content-Type': 'application/json',
170
+ 'Authorization': `Bearer ${token}`,
171
+ },
172
+ body: JSON.stringify(body),
173
+ });
174
+ const text = await response.text();
175
+ let json;
176
+ try {
177
+ json = JSON.parse(text);
178
+ }
179
+ catch {
180
+ if (text.startsWith('<!DOCTYPE') || text.startsWith('<html')) {
181
+ throw new Error(`Server returned HTML instead of JSON (status ${response.status}). ` +
182
+ `This usually means the API endpoint doesn't exist or there's a server error.`);
183
+ }
184
+ throw new Error(`Invalid response from server: ${text.substring(0, 200)}`);
185
+ }
186
+ if (!response.ok) {
187
+ const errorResponse = json;
188
+ let errorMessage = errorResponse.error ?? `API error: ${response.status}`;
189
+ if (errorResponse.availableTools && errorResponse.availableTools.length > 0) {
190
+ errorMessage += `\n\nAvailable tools:\n${errorResponse.availableTools.map(t => ` - ${t}`).join('\n')}`;
191
+ }
192
+ throw new Error(errorMessage);
193
+ }
194
+ return json;
195
+ }
196
+ function printHelp() {
197
+ console.log(`
198
+ skedyul invoke - Invoke a tool on a hosted app version
199
+
200
+ Usage:
201
+ skedyul invoke <tool-name> --appInstallationId <id> [options]
202
+
203
+ Arguments:
204
+ <tool-name> Name of the tool to invoke (e.g., 'parse_lab_report')
205
+
206
+ Required Options:
207
+ --appInstallationId, -i The app installation ID to invoke the tool on
208
+
209
+ Optional Options:
210
+ --args, -a JSON string of arguments to pass to the tool
211
+ Supports {{upload:/path/to/file}} syntax for file uploads
212
+ --timeout, -t Timeout in milliseconds (default: uses tool config)
213
+ --server Override the server URL
214
+ --help, -h Show this help message
215
+
216
+ File Upload Syntax:
217
+ Use {{upload:/path/to/file}} in any string field within --args to automatically
218
+ upload a local file and replace the template with the uploaded file ID.
219
+
220
+ Examples:
221
+ # Invoke a tool with no arguments
222
+ skedyul invoke get_appointments --appInstallationId inst_abc123
223
+
224
+ # Invoke a tool with arguments
225
+ skedyul invoke parse_lab_report \\
226
+ --appInstallationId inst_abc123 \\
227
+ --args '{"file_id": "fl_existing_id"}'
228
+
229
+ # Invoke with file upload (uploads file and injects file_id)
230
+ skedyul invoke parse_lab_report \\
231
+ --appInstallationId inst_abc123 \\
232
+ --args '{"file_id": "{{upload:/path/to/report.pdf}}"}'
233
+
234
+ # Multiple file uploads in one command
235
+ skedyul invoke process_documents \\
236
+ --appInstallationId inst_abc123 \\
237
+ --args '{"doc": "{{upload:./doc.pdf}}", "image": "{{upload:./photo.jpg}}"}'
238
+
239
+ # Invoke with custom timeout (30 seconds)
240
+ skedyul invoke long_running_task \\
241
+ --appInstallationId inst_abc123 \\
242
+ --timeout 30000
243
+
244
+ Note:
245
+ You must be logged in with 'skedyul auth login' and have access to the
246
+ workplace where the app is installed.
247
+ `);
248
+ }
249
+ async function invokeRemoteCommand(args) {
250
+ const { flags, positional } = (0, utils_1.parseArgs)(args);
251
+ if (flags.help || flags.h) {
252
+ printHelp();
253
+ return;
254
+ }
255
+ const toolName = positional[0];
256
+ if (!toolName) {
257
+ console.error('Error: Tool name is required');
258
+ console.error("Run 'skedyul invoke --help' for usage information.");
259
+ process.exit(1);
260
+ }
261
+ const appInstallationId = (flags.appInstallationId || flags.i);
262
+ if (!appInstallationId) {
263
+ console.error('Error: --appInstallationId is required');
264
+ console.error("Run 'skedyul invoke --help' for usage information.");
265
+ process.exit(1);
266
+ }
267
+ let toolArgs = {};
268
+ const argsValue = flags.args || flags.a;
269
+ if (argsValue && typeof argsValue === 'string') {
270
+ try {
271
+ toolArgs = JSON.parse(argsValue);
272
+ }
273
+ catch {
274
+ console.error('Error: Invalid JSON in --args');
275
+ process.exit(1);
276
+ }
277
+ }
278
+ let timeout;
279
+ const timeoutValue = flags.timeout || flags.t;
280
+ if (timeoutValue) {
281
+ const parsed = parseInt(String(timeoutValue), 10);
282
+ if (isNaN(parsed) || parsed <= 0) {
283
+ console.error('Error: --timeout must be a positive number');
284
+ process.exit(1);
285
+ }
286
+ timeout = parsed;
287
+ }
288
+ const credentials = (0, auth_1.getCredentials)();
289
+ if (!credentials) {
290
+ console.error('Error: Not logged in.');
291
+ console.error("Run 'skedyul auth login' to authenticate first.");
292
+ process.exit(1);
293
+ }
294
+ const serverUrl = (0, auth_1.getServerUrl)(flags.server);
295
+ const activeProfile = (0, auth_1.getActiveProfileName)();
296
+ console.error(`Invoking tool: ${toolName}`);
297
+ console.error(`App Installation: ${appInstallationId}`);
298
+ console.error(`Server: ${serverUrl}`);
299
+ if (activeProfile) {
300
+ console.error(`Profile: ${activeProfile}`);
301
+ }
302
+ try {
303
+ toolArgs = await processUploadTemplates(toolArgs, serverUrl, credentials.token, appInstallationId);
304
+ const response = await callInvokeApi(serverUrl, credentials.token, {
305
+ appInstallationId,
306
+ toolName,
307
+ args: toolArgs,
308
+ ...(timeout !== undefined && { timeout }),
309
+ });
310
+ if (response.success) {
311
+ console.log((0, utils_1.formatJson)({ result: response.result }));
312
+ }
313
+ else {
314
+ console.error(`Error: ${response.error ?? 'Unknown error'}`);
315
+ process.exit(1);
316
+ }
317
+ }
318
+ catch (error) {
319
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
320
+ process.exit(1);
321
+ }
322
+ }
package/dist/cli/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const invoke_1 = require("./commands/invoke");
5
+ const invoke_remote_1 = require("./commands/invoke-remote");
5
6
  const tools_1 = require("./commands/tools");
6
7
  const serve_1 = require("./commands/serve");
7
8
  const validate_1 = require("./commands/validate");
@@ -33,6 +34,7 @@ USAGE
33
34
  COMMANDS
34
35
  auth Authenticate with Skedyul (login, logout, status)
35
36
  config Manage global CLI configuration (ngrok, server URL)
37
+ invoke Invoke a tool on a hosted app version
36
38
  dev Development tools for building and testing apps locally
37
39
 
38
40
  GETTING STARTED
@@ -63,6 +65,7 @@ CONFIGURATION
63
65
  MORE HELP
64
66
  $ skedyul auth --help Show authentication commands
65
67
  $ skedyul config --help Show configuration commands
68
+ $ skedyul invoke --help Show invoke command options
66
69
  $ skedyul dev --help Show development commands
67
70
  $ skedyul <cmd> --help Show help for specific command
68
71
 
@@ -165,6 +168,10 @@ async function main() {
165
168
  await (0, config_1.configCommand)(args.slice(1));
166
169
  return;
167
170
  }
171
+ if (command === 'invoke') {
172
+ await (0, invoke_remote_1.invokeRemoteCommand)(args.slice(1));
173
+ return;
174
+ }
168
175
  if (command !== 'dev') {
169
176
  console.error(`Unknown command: ${command}`);
170
177
  console.error(`Run 'skedyul --help' for usage information.`);
@@ -1,18 +1,54 @@
1
- export interface StoredCredentials {
1
+ export interface Profile {
2
+ serverUrl: string;
2
3
  token: string;
3
4
  userId: string;
4
5
  username: string;
5
6
  email: string;
6
- serverUrl: string;
7
7
  expiresAt: string | null;
8
8
  createdAt: string;
9
9
  }
10
+ export interface ProfilesFile {
11
+ profiles: Record<string, Profile>;
12
+ }
10
13
  export interface AuthConfig {
14
+ activeProfile?: string;
11
15
  defaultServer: string;
12
16
  ngrokAuthtoken?: string;
13
17
  }
18
+ /** @deprecated Use Profile instead - kept for migration */
19
+ export interface StoredCredentials {
20
+ token: string;
21
+ userId: string;
22
+ username: string;
23
+ email: string;
24
+ serverUrl: string;
25
+ expiresAt: string | null;
26
+ createdAt: string;
27
+ }
28
+ /**
29
+ * Generate a sensible profile name from a server URL.
30
+ */
31
+ export declare function generateProfileName(serverUrl: string): string;
32
+ /**
33
+ * Ensure a profile name is unique by appending a number if needed.
34
+ */
35
+ export declare function ensureUniqueProfileName(baseName: string, existingProfiles: Record<string, Profile>): string;
36
+ export declare function getProfiles(): ProfilesFile;
37
+ export declare function saveProfiles(profilesFile: ProfilesFile): void;
38
+ export declare function getProfile(name: string): Profile | null;
39
+ export declare function saveProfile(name: string, profile: Profile): void;
40
+ export declare function deleteProfile(name: string): boolean;
41
+ export declare function listProfiles(): Array<{
42
+ name: string;
43
+ profile: Profile;
44
+ isActive: boolean;
45
+ isExpired: boolean;
46
+ }>;
47
+ export declare function clearAllProfiles(): void;
48
+ export declare function getActiveProfileName(): string | null;
49
+ export declare function setActiveProfile(name: string): boolean;
14
50
  export declare function getCredentials(): StoredCredentials | null;
15
- export declare function saveCredentials(credentials: StoredCredentials): void;
51
+ export declare function saveCredentials(credentials: StoredCredentials, profileName?: string): string;
16
52
  export declare function clearCredentials(): void;
17
53
  export declare function getConfig(): AuthConfig;
18
54
  export declare function saveConfig(config: AuthConfig): void;
@@ -25,7 +61,7 @@ export declare function getLocalConfig(): {
25
61
  };
26
62
  /**
27
63
  * Get the server URL to use.
28
- * Priority: CLI flag > local config > credentials > global config > default
64
+ * Priority: CLI flag > local config > active profile > global config > default
29
65
  */
30
66
  export declare function getServerUrl(override?: string): string;
31
67
  /**