@jam-nodes/playground 0.1.0

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 (62) hide show
  1. package/dist/commands/credentials.d.ts +6 -0
  2. package/dist/commands/credentials.d.ts.map +1 -0
  3. package/dist/commands/credentials.js +205 -0
  4. package/dist/commands/credentials.js.map +1 -0
  5. package/dist/commands/index.d.ts +5 -0
  6. package/dist/commands/index.d.ts.map +1 -0
  7. package/dist/commands/index.js +5 -0
  8. package/dist/commands/index.js.map +1 -0
  9. package/dist/commands/init.d.ts +6 -0
  10. package/dist/commands/init.d.ts.map +1 -0
  11. package/dist/commands/init.js +76 -0
  12. package/dist/commands/init.js.map +1 -0
  13. package/dist/commands/list.d.ts +6 -0
  14. package/dist/commands/list.d.ts.map +1 -0
  15. package/dist/commands/list.js +111 -0
  16. package/dist/commands/list.js.map +1 -0
  17. package/dist/commands/run.d.ts +6 -0
  18. package/dist/commands/run.d.ts.map +1 -0
  19. package/dist/commands/run.js +245 -0
  20. package/dist/commands/run.js.map +1 -0
  21. package/dist/credentials/env-provider.d.ts +54 -0
  22. package/dist/credentials/env-provider.d.ts.map +1 -0
  23. package/dist/credentials/env-provider.js +177 -0
  24. package/dist/credentials/env-provider.js.map +1 -0
  25. package/dist/credentials/index.d.ts +2 -0
  26. package/dist/credentials/index.d.ts.map +1 -0
  27. package/dist/credentials/index.js +2 -0
  28. package/dist/credentials/index.js.map +1 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +24 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/ui/index.d.ts +2 -0
  34. package/dist/ui/index.d.ts.map +1 -0
  35. package/dist/ui/index.js +2 -0
  36. package/dist/ui/index.js.map +1 -0
  37. package/dist/ui/prompts.d.ts +49 -0
  38. package/dist/ui/prompts.d.ts.map +1 -0
  39. package/dist/ui/prompts.js +189 -0
  40. package/dist/ui/prompts.js.map +1 -0
  41. package/dist/utils/index.d.ts +2 -0
  42. package/dist/utils/index.d.ts.map +1 -0
  43. package/dist/utils/index.js +2 -0
  44. package/dist/utils/index.js.map +1 -0
  45. package/dist/utils/mock-generator.d.ts +11 -0
  46. package/dist/utils/mock-generator.d.ts.map +1 -0
  47. package/dist/utils/mock-generator.js +199 -0
  48. package/dist/utils/mock-generator.js.map +1 -0
  49. package/package.json +53 -0
  50. package/src/commands/credentials.ts +242 -0
  51. package/src/commands/index.ts +4 -0
  52. package/src/commands/init.ts +80 -0
  53. package/src/commands/list.ts +124 -0
  54. package/src/commands/run.ts +273 -0
  55. package/src/credentials/env-provider.ts +208 -0
  56. package/src/credentials/index.ts +10 -0
  57. package/src/index.ts +32 -0
  58. package/src/ui/index.ts +9 -0
  59. package/src/ui/prompts.ts +259 -0
  60. package/src/utils/index.ts +1 -0
  61. package/src/utils/mock-generator.ts +227 -0
  62. package/tsconfig.json +15 -0
@@ -0,0 +1,273 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { createRegistry, type NodeExecutionContext } from '@jam-nodes/core';
5
+ import { builtInNodes } from '@jam-nodes/nodes';
6
+ import {
7
+ getCredentials,
8
+ saveCredentials,
9
+ getCredentialSource,
10
+ } from '../credentials/index.js';
11
+ import {
12
+ promptForNodeInput,
13
+ promptForCredentials,
14
+ promptSaveCredentials,
15
+ selectNode,
16
+ } from '../ui/index.js';
17
+ import { generateMockOutput } from '../utils/index.js';
18
+
19
+ // Create and populate the registry
20
+ const registry = createRegistry();
21
+ for (const node of builtInNodes) {
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ registry.register(node as any);
24
+ }
25
+
26
+ /**
27
+ * Service names required by each node type
28
+ */
29
+ const NODE_SERVICE_REQUIREMENTS: Record<string, string[]> = {
30
+ search_contacts: ['apollo'],
31
+ reddit_monitor: ['forumScout'],
32
+ twitter_monitor: ['twitter'],
33
+ linkedin_monitor: ['forumScout'],
34
+ sora_video: ['openai'],
35
+ seo_keyword_research: ['dataForSeo'],
36
+ seo_audit: ['dataForSeo'],
37
+ social_keyword_generator: ['anthropic'],
38
+ draft_emails: ['anthropic'],
39
+ social_ai_analyze: ['anthropic'],
40
+ };
41
+
42
+ /**
43
+ * Run command - executes a node with input
44
+ */
45
+ export const runCommand = new Command('run')
46
+ .description('Run a jam-node interactively')
47
+ .argument('[node-type]', 'Node type to run (interactive selection if omitted)')
48
+ .option('-i, --input <json>', 'Input as JSON string')
49
+ .option('-m, --mock', 'Use mock mode (returns sample data without calling APIs)')
50
+ .option('--no-confirm', 'Skip confirmation prompt')
51
+ .action(async (nodeType: string | undefined, options) => {
52
+ try {
53
+ // If no node type specified, show selection
54
+ if (!nodeType) {
55
+ const nodes = registry.getAllMetadata();
56
+ nodeType = await selectNode(nodes);
57
+ }
58
+
59
+ // Get node definition
60
+ const definition = registry.getDefinition(nodeType);
61
+ if (!definition) {
62
+ console.error(chalk.red(`Unknown node type: ${nodeType}`));
63
+ console.log(chalk.dim('Use "jam-playground list" to see available nodes'));
64
+ process.exit(1);
65
+ }
66
+
67
+ // Display node info
68
+ console.log();
69
+ console.log(chalk.bold.cyan('┌─────────────────────────────────────────────────────────────┐'));
70
+ console.log(chalk.bold.cyan('│ ') + chalk.bold(definition.name).padEnd(57) + chalk.bold.cyan('│'));
71
+ console.log(chalk.bold.cyan('│ ') + chalk.dim(definition.description.slice(0, 55)).padEnd(57) + chalk.bold.cyan('│'));
72
+ console.log(chalk.bold.cyan('└─────────────────────────────────────────────────────────────┘'));
73
+
74
+ // Mock mode notice
75
+ if (options.mock) {
76
+ console.log();
77
+ console.log(chalk.yellow('⚡ Mock mode enabled - returning sample data without API calls'));
78
+ }
79
+
80
+ // Check for required services/credentials
81
+ const requiredServices = NODE_SERVICE_REQUIREMENTS[nodeType] || [];
82
+ const credentials: Record<string, Record<string, string>> = {};
83
+
84
+ if (requiredServices.length > 0 && !options.mock) {
85
+ console.log();
86
+ console.log(chalk.dim(`This node requires: ${requiredServices.join(', ')}`));
87
+
88
+ for (const service of requiredServices) {
89
+ const source = await getCredentialSource(service);
90
+
91
+ if (source) {
92
+ console.log(chalk.green(`✓ ${service} credentials found (${source})`));
93
+ const creds = await getCredentials(service);
94
+ if (creds) {
95
+ credentials[service] = creds;
96
+ }
97
+ } else {
98
+ console.log(chalk.yellow(`⚠ ${service} credentials not found`));
99
+
100
+ // Prompt for credentials
101
+ const creds = await promptForCredentials(service, [
102
+ { name: 'apiKey', message: `${service} API Key:`, type: 'password' },
103
+ ]);
104
+
105
+ credentials[service] = creds;
106
+
107
+ // Ask if they want to save
108
+ const shouldSave = await promptSaveCredentials();
109
+ if (shouldSave) {
110
+ saveCredentials(service, creds);
111
+ console.log(chalk.green(`✓ ${service} credentials saved`));
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ // Get input
118
+ let input: Record<string, unknown>;
119
+
120
+ if (options.input) {
121
+ try {
122
+ input = JSON.parse(options.input);
123
+ } catch {
124
+ console.error(chalk.red('Invalid JSON input'));
125
+ process.exit(1);
126
+ }
127
+ } else {
128
+ // Interactive input
129
+ input = await promptForNodeInput(definition.inputSchema);
130
+ }
131
+
132
+ // Display input summary
133
+ console.log();
134
+ console.log(chalk.dim('Input:'));
135
+ console.log(chalk.dim(JSON.stringify(input, null, 2)));
136
+
137
+ // Execute node
138
+ const spinner = ora({
139
+ text: `Executing ${definition.name}...`,
140
+ color: 'cyan',
141
+ }).start();
142
+
143
+ try {
144
+ let result;
145
+
146
+ if (options.mock) {
147
+ // Mock mode - generate sample output
148
+ await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate delay
149
+ result = {
150
+ success: true,
151
+ output: generateMockOutput(nodeType, definition.outputSchema),
152
+ };
153
+ } else {
154
+ // Create execution context
155
+ const context: NodeExecutionContext = {
156
+ userId: 'playground',
157
+ workflowExecutionId: `playground_${Date.now()}`,
158
+ variables: {},
159
+ resolveNestedPath: (path: string) => {
160
+ // Simple implementation - look in variables
161
+ const parts = path.split('.');
162
+ let current: unknown = context.variables;
163
+ for (const part of parts) {
164
+ if (current && typeof current === 'object') {
165
+ current = (current as Record<string, unknown>)[part];
166
+ } else {
167
+ return undefined;
168
+ }
169
+ }
170
+ return current;
171
+ },
172
+ services: createMockServices(credentials),
173
+ };
174
+
175
+ // Validate input
176
+ const validatedInput = definition.inputSchema.parse(input);
177
+
178
+ // Execute
179
+ result = await definition.executor(validatedInput, context);
180
+ }
181
+
182
+ spinner.stop();
183
+
184
+ // Display result
185
+ console.log();
186
+ if (result.success) {
187
+ console.log(chalk.green.bold('✓ Success!'));
188
+ console.log();
189
+ console.log(chalk.dim('Output:'));
190
+ console.log(formatJson(result.output));
191
+ } else {
192
+ console.log(chalk.red.bold('✗ Failed'));
193
+ console.log(chalk.red(result.error || 'Unknown error'));
194
+ }
195
+ } catch (error) {
196
+ spinner.stop();
197
+ console.log();
198
+ console.log(chalk.red.bold('✗ Execution failed'));
199
+ console.log(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
200
+
201
+ if (error instanceof Error && error.stack) {
202
+ console.log(chalk.dim(error.stack));
203
+ }
204
+ }
205
+ } catch (error) {
206
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
207
+ process.exit(1);
208
+ }
209
+ });
210
+
211
+ /**
212
+ * Format JSON with syntax highlighting
213
+ */
214
+ function formatJson(data: unknown): string {
215
+ const json = JSON.stringify(data, null, 2);
216
+ return json
217
+ .replace(/"([^"]+)":/g, chalk.cyan('"$1":'))
218
+ .replace(/: "([^"]+)"/g, ': ' + chalk.green('"$1"'))
219
+ .replace(/: (\d+)/g, ': ' + chalk.yellow('$1'))
220
+ .replace(/: (true|false)/g, ': ' + chalk.magenta('$1'))
221
+ .replace(/: (null)/g, ': ' + chalk.dim('$1'));
222
+ }
223
+
224
+ /**
225
+ * Create mock service implementations that use credentials
226
+ */
227
+ function createMockServices(
228
+ credentials: Record<string, Record<string, string>>
229
+ ): Record<string, unknown> {
230
+ // These are placeholder services that would need real implementations
231
+ // For playground purposes, they demonstrate the service injection pattern
232
+ return {
233
+ apollo: credentials['apollo']
234
+ ? {
235
+ searchContacts: async () => {
236
+ console.log(chalk.dim('Apollo API call would happen here...'));
237
+ return [];
238
+ },
239
+ enrichContact: async () => {
240
+ return null;
241
+ },
242
+ }
243
+ : undefined,
244
+ forumScout: credentials['forumScout']
245
+ ? {
246
+ searchReddit: async () => [],
247
+ searchLinkedIn: async () => [],
248
+ }
249
+ : undefined,
250
+ twitter: credentials['twitter']
251
+ ? {
252
+ search: async () => [],
253
+ }
254
+ : undefined,
255
+ openai: credentials['openai']
256
+ ? {
257
+ generateVideo: async () => ({ url: 'mock_url' }),
258
+ }
259
+ : undefined,
260
+ anthropic: credentials['anthropic']
261
+ ? {
262
+ complete: async () => 'mock response',
263
+ generateKeywords: async () => ['keyword1', 'keyword2'],
264
+ }
265
+ : undefined,
266
+ dataForSeo: credentials['dataForSeo']
267
+ ? {
268
+ getKeywords: async () => [],
269
+ runAudit: async () => ({ issues: [] }),
270
+ }
271
+ : undefined,
272
+ };
273
+ }
@@ -0,0 +1,208 @@
1
+ import Conf from 'conf';
2
+ import { config } from 'dotenv';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+
6
+ /**
7
+ * Encrypted credential store using conf
8
+ * Stores credentials in ~/.config/jam-nodes-playground/
9
+ */
10
+ const store = new Conf<{
11
+ credentials: Record<string, Record<string, string>>;
12
+ }>({
13
+ projectName: 'jam-nodes-playground',
14
+ defaults: {
15
+ credentials: {},
16
+ },
17
+ });
18
+
19
+ /**
20
+ * Load environment variables from .env files
21
+ * Searches in current directory and parent directories
22
+ */
23
+ function loadEnvFiles(): void {
24
+ // Load .env from current working directory
25
+ config();
26
+
27
+ // Also try .env.local
28
+ const localEnvPath = path.join(process.cwd(), '.env.local');
29
+ if (fs.existsSync(localEnvPath)) {
30
+ config({ path: localEnvPath });
31
+ }
32
+ }
33
+
34
+ // Load env files on import
35
+ loadEnvFiles();
36
+
37
+ /**
38
+ * Known credential name to environment variable mappings
39
+ */
40
+ const ENV_VAR_MAPPINGS: Record<string, string[]> = {
41
+ hunter: ['JAM_HUNTER_API_KEY', 'HUNTER_API_KEY'],
42
+ apollo: ['JAM_APOLLO_API_KEY', 'APOLLO_API_KEY'],
43
+ sendgrid: ['JAM_SENDGRID_API_KEY', 'SENDGRID_API_KEY'],
44
+ openai: ['JAM_OPENAI_API_KEY', 'OPENAI_API_KEY'],
45
+ anthropic: ['JAM_ANTHROPIC_API_KEY', 'ANTHROPIC_API_KEY'],
46
+ twitter: ['JAM_TWITTER_BEARER_TOKEN', 'TWITTER_BEARER_TOKEN'],
47
+ reddit: ['JAM_REDDIT_CLIENT_ID', 'REDDIT_CLIENT_ID'],
48
+ linkedin: ['JAM_LINKEDIN_ACCESS_TOKEN', 'LINKEDIN_ACCESS_TOKEN'],
49
+ dataforseo: ['JAM_DATAFORSEO_LOGIN', 'DATAFORSEO_LOGIN'],
50
+ hubspot: ['JAM_HUBSPOT_API_KEY', 'HUBSPOT_API_KEY'],
51
+ clearbit: ['JAM_CLEARBIT_API_KEY', 'CLEARBIT_API_KEY'],
52
+ dropcontact: ['JAM_DROPCONTACT_API_KEY', 'DROPCONTACT_API_KEY'],
53
+ };
54
+
55
+ /**
56
+ * Check environment variables for credentials
57
+ */
58
+ function getFromEnv(name: string): Record<string, string> | null {
59
+ const envVars = ENV_VAR_MAPPINGS[name.toLowerCase()];
60
+
61
+ if (envVars) {
62
+ for (const varName of envVars) {
63
+ const value = process.env[varName];
64
+ if (value) {
65
+ return { apiKey: value };
66
+ }
67
+ }
68
+ }
69
+
70
+ // Try generic pattern: JAM_{NAME}_API_KEY
71
+ const genericKey = `JAM_${name.toUpperCase()}_API_KEY`;
72
+ if (process.env[genericKey]) {
73
+ return { apiKey: process.env[genericKey] };
74
+ }
75
+
76
+ // Special handling for services with multiple env vars
77
+ if (name.toLowerCase() === 'dataforseo') {
78
+ const login = process.env['JAM_DATAFORSEO_LOGIN'] || process.env['DATAFORSEO_LOGIN'];
79
+ const password = process.env['JAM_DATAFORSEO_PASSWORD'] || process.env['DATAFORSEO_PASSWORD'];
80
+ if (login && password) {
81
+ return { login, password };
82
+ }
83
+ }
84
+
85
+ if (name.toLowerCase() === 'reddit') {
86
+ const clientId = process.env['JAM_REDDIT_CLIENT_ID'] || process.env['REDDIT_CLIENT_ID'];
87
+ const clientSecret = process.env['JAM_REDDIT_CLIENT_SECRET'] || process.env['REDDIT_CLIENT_SECRET'];
88
+ if (clientId && clientSecret) {
89
+ return { clientId, clientSecret };
90
+ }
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Get credentials for a service.
98
+ * Checks in order:
99
+ * 1. Environment variables
100
+ * 2. Saved credentials in config store
101
+ * 3. Returns null (caller should prompt user)
102
+ *
103
+ * @param name - Service name (e.g., 'hunter', 'apollo')
104
+ * @returns Credentials object or null if not found
105
+ */
106
+ export async function getCredentials(
107
+ name: string
108
+ ): Promise<Record<string, string> | null> {
109
+ // 1. Check environment variables
110
+ const envCreds = getFromEnv(name);
111
+ if (envCreds) {
112
+ return envCreds;
113
+ }
114
+
115
+ // 2. Check saved credentials
116
+ const saved = store.get(`credentials.${name.toLowerCase()}`) as Record<string, string> | undefined;
117
+ if (saved && Object.keys(saved).length > 0) {
118
+ return saved;
119
+ }
120
+
121
+ // 3. Return null (caller should prompt)
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Save credentials for a service.
127
+ *
128
+ * @param name - Service name
129
+ * @param credentials - Credential data to save
130
+ */
131
+ export function saveCredentials(
132
+ name: string,
133
+ credentials: Record<string, string>
134
+ ): void {
135
+ store.set(`credentials.${name.toLowerCase()}`, credentials);
136
+ }
137
+
138
+ /**
139
+ * Delete credentials for a service.
140
+ *
141
+ * @param name - Service name
142
+ * @returns True if credentials were deleted
143
+ */
144
+ export function deleteCredentials(name: string): boolean {
145
+ const key = `credentials.${name.toLowerCase()}`;
146
+ if (store.has(key)) {
147
+ store.delete(key);
148
+ return true;
149
+ }
150
+ return false;
151
+ }
152
+
153
+ /**
154
+ * List all saved credential names.
155
+ *
156
+ * @returns Array of service names with saved credentials
157
+ */
158
+ export function listSavedCredentials(): string[] {
159
+ const credentials = store.get('credentials');
160
+ return Object.keys(credentials);
161
+ }
162
+
163
+ /**
164
+ * Check if credentials exist for a service.
165
+ *
166
+ * @param name - Service name
167
+ * @returns True if credentials are available (env or saved)
168
+ */
169
+ export async function hasCredentials(name: string): Promise<boolean> {
170
+ const creds = await getCredentials(name);
171
+ return creds !== null;
172
+ }
173
+
174
+ /**
175
+ * Get credential source for display.
176
+ *
177
+ * @param name - Service name
178
+ * @returns Source description or null
179
+ */
180
+ export async function getCredentialSource(
181
+ name: string
182
+ ): Promise<'env' | 'saved' | null> {
183
+ const envCreds = getFromEnv(name);
184
+ if (envCreds) {
185
+ return 'env';
186
+ }
187
+
188
+ const saved = store.get(`credentials.${name.toLowerCase()}`) as Record<string, string> | undefined;
189
+ if (saved && Object.keys(saved).length > 0) {
190
+ return 'saved';
191
+ }
192
+
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * Clear all saved credentials.
198
+ */
199
+ export function clearAllCredentials(): void {
200
+ store.set('credentials', {});
201
+ }
202
+
203
+ /**
204
+ * Get the path to the credential store.
205
+ */
206
+ export function getStorePath(): string {
207
+ return store.path;
208
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ getCredentials,
3
+ saveCredentials,
4
+ deleteCredentials,
5
+ listSavedCredentials,
6
+ hasCredentials,
7
+ getCredentialSource,
8
+ clearAllCredentials,
9
+ getStorePath,
10
+ } from './env-provider.js';
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import {
5
+ listCommand,
6
+ runCommand,
7
+ initCommand,
8
+ credentialsCommand,
9
+ } from './commands/index.js';
10
+
11
+ const program = new Command()
12
+ .name('jam-playground')
13
+ .description('Test jam-nodes interactively')
14
+ .version('0.1.0');
15
+
16
+ // Add commands
17
+ program.addCommand(listCommand);
18
+ program.addCommand(runCommand);
19
+ program.addCommand(initCommand);
20
+ program.addCommand(credentialsCommand);
21
+
22
+ // Default action (no command) - show help
23
+ program.action(() => {
24
+ console.log();
25
+ console.log(chalk.bold.cyan(' jam-nodes Playground'));
26
+ console.log(chalk.dim(' Test workflow nodes interactively'));
27
+ console.log();
28
+ program.outputHelp();
29
+ });
30
+
31
+ // Parse arguments
32
+ program.parse();
@@ -0,0 +1,9 @@
1
+ export {
2
+ promptForCredentials,
3
+ promptCredentialSource,
4
+ promptSaveCredentials,
5
+ promptForNodeInput,
6
+ confirmRun,
7
+ selectNode,
8
+ generatePromptsFromSchema,
9
+ } from './prompts.js';