tuneprompt 1.0.5 → 1.0.7

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.
@@ -91,9 +91,6 @@ async function initCommand() {
91
91
  const envContent = `OPENAI_API_KEY=your_key_here
92
92
  ANTHROPIC_API_KEY=your_key_here
93
93
  OPENROUTER_API_KEY=your_key_here
94
-
95
- # For self-hosted or local testing:
96
- # TUNEPROMPT_API_URL=http://localhost:3000
97
94
  `;
98
95
  fs.writeFileSync(envPath, envContent);
99
96
  console.log(chalk_1.default.green('✓ Created .env'));
@@ -65,64 +65,25 @@ async function runTests(options = {}) {
65
65
  const results = await runner.runTests(testCases);
66
66
  spinner.stop();
67
67
  // Save to database
68
+ // Save to database
68
69
  const db = new database_1.TestDatabase();
69
70
  db.saveRun(results);
70
- db.close();
71
+ // Calculate results for cloud upload (and for sync logic)
72
+ const currentRunId = results.id; // Assuming results has ID
71
73
  // Report results
72
74
  const reporter = new reporter_1.TestReporter();
73
75
  reporter.printResults(results, config.outputFormat);
74
- // Calculate results for cloud upload
75
- const testResults = results.results.map((result) => {
76
- // Map from internal TestResult to cloud service TestResult
77
- const mappedResult = {
78
- test_name: result.testCase.description,
79
- test_description: result.testCase.description,
80
- prompt: typeof result.testCase.prompt === 'string'
81
- ? result.testCase.prompt
82
- : JSON.stringify(result.testCase.prompt),
83
- input_data: result.testCase.variables,
84
- expected_output: result.expectedOutput,
85
- actual_output: result.actualOutput,
86
- score: result.score,
87
- method: result.testCase.config?.method || 'exact',
88
- status: result.status,
89
- model: result.metadata.provider || '',
90
- tokens_used: result.metadata.tokens,
91
- latency_ms: result.metadata.duration,
92
- cost_usd: result.metadata.cost,
93
- error_message: result.error,
94
- error_type: undefined, // No error type in current TestResult interface
95
- };
96
- return mappedResult;
97
- });
98
- // Calculate total cost from all test results
99
- const totalCost = results.results.reduce((sum, result) => {
100
- return sum + (result.metadata.cost || 0);
101
- }, 0);
102
- const resultsSummary = {
103
- totalTests: results.results.length,
104
- passedTests: results.passed,
105
- failedTests: results.failed,
106
- durationMs: Date.now() - startTime,
107
- totalCost: totalCost || 0.05, // fallback value
108
- tests: testResults,
109
- };
110
- // Print results to console (existing logic)
111
- console.log(chalk_1.default.green(`\n✅ ${resultsSummary.passedTests} passed`));
112
- console.log(chalk_1.default.red(`❌ ${resultsSummary.failedTests} failed\n`));
113
- // Show upsell hint if tests failed
114
- displayRunSummary(results.results);
115
- // NEW: Cloud upload logic
116
76
  const isCI = options.ci ||
117
77
  process.env.CI === 'true' ||
118
78
  !!process.env.GITHUB_ACTIONS ||
119
79
  !!process.env.GITLAB_CI;
120
80
  const shouldUpload = options.cloud || isCI;
121
81
  if (shouldUpload) {
122
- await uploadToCloud(resultsSummary, options);
82
+ await syncPendingRuns(db, options);
123
83
  }
84
+ db.close();
124
85
  // Exit with error code if tests failed
125
- if (resultsSummary.failedTests > 0) {
86
+ if (results.failed > 0) {
126
87
  process.exit(1);
127
88
  }
128
89
  }
@@ -139,35 +100,34 @@ exports.runCommand = new commander_1.Command('run')
139
100
  .action(async (options) => {
140
101
  await runTests(options);
141
102
  });
142
- async function uploadToCloud(results, options) {
103
+ async function syncPendingRuns(db, options) {
104
+ const pendingRuns = db.getPendingUploads();
105
+ if (pendingRuns.length === 0)
106
+ return;
107
+ console.log(chalk_1.default.blue(`\n☁️ Syncing ${pendingRuns.length} pending run(s) to Cloud...`));
143
108
  const cloudService = new cloud_service_1.CloudService();
144
109
  await cloudService.init();
145
- const isAuth = await cloudService.isAuthenticated();
146
- if (!isAuth) {
147
- console.log(chalk_1.default.yellow('\n⚠️ Not authenticated with Cloud.'));
148
- console.log(chalk_1.default.gray('Results saved locally. Run `tuneprompt activate` to enable cloud sync\n'));
110
+ if (!(await cloudService.isAuthenticated())) {
111
+ console.log(chalk_1.default.yellow('⚠️ Not authenticated. Run `tuneprompt activate` first.'));
149
112
  return;
150
113
  }
151
- // Get or create project
114
+ // Get project ID once
152
115
  let projectId;
153
116
  try {
154
117
  const projects = await cloudService.getProjects();
155
118
  if (projects.length === 0) {
156
- console.log(chalk_1.default.blue('📁 Creating default project...'));
157
119
  const project = await cloudService.createProject('Default Project');
158
120
  projectId = project.id;
159
- console.log(chalk_1.default.green(`✅ Project created: ${projectId}`));
160
121
  }
161
122
  else {
162
- projectId = projects[0].id; // Use first project
163
- console.log(chalk_1.default.gray(`📋 Using existing project: ${projectId}`));
123
+ projectId = projects[0].id;
164
124
  }
165
125
  }
166
- catch (error) {
167
- console.log(chalk_1.default.yellow('⚠️ Failed to get project'), error);
126
+ catch (err) {
127
+ console.log(chalk_1.default.yellow('⚠️ Failed to get project info'));
168
128
  return;
169
129
  }
170
- // Get Git context
130
+ // Common Git/Env context
171
131
  let gitContext = {};
172
132
  try {
173
133
  gitContext = {
@@ -176,10 +136,7 @@ async function uploadToCloud(results, options) {
176
136
  commit_message: (0, child_process_1.execSync)('git log -1 --pretty=%B', { encoding: 'utf-8' }).trim(),
177
137
  };
178
138
  }
179
- catch {
180
- // Not a git repo
181
- }
182
- // Detect CI provider
139
+ catch { }
183
140
  let ciProvider;
184
141
  if (process.env.GITHUB_ACTIONS)
185
142
  ciProvider = 'github';
@@ -189,28 +146,43 @@ async function uploadToCloud(results, options) {
189
146
  ciProvider = 'jenkins';
190
147
  else if (process.env.CIRCLECI)
191
148
  ciProvider = 'circleci';
192
- // Prepare run data
193
- const runData = {
194
- project_id: projectId,
195
- environment: options.ci || process.env.CI ? 'ci' : 'local',
196
- ci_provider: ciProvider,
197
- total_tests: results.totalTests,
198
- passed_tests: results.passedTests,
199
- failed_tests: results.failedTests,
200
- duration_ms: results.durationMs,
201
- cost_usd: results.totalCost,
202
- started_at: new Date(Date.now() - results.durationMs).toISOString(),
203
- completed_at: new Date().toISOString(),
204
- test_results: results.tests,
205
- ...gitContext,
206
- };
207
- console.log(chalk_1.default.blue('\n☁️ Uploading results to Cloud...'));
208
- const uploadResult = await cloudService.uploadRun(runData);
209
- if (uploadResult.success) {
210
- console.log(chalk_1.default.green('✅ Results uploaded successfully'));
211
- console.log(chalk_1.default.gray(`View at: ${uploadResult.url}\n`));
212
- }
213
- else {
214
- console.log(chalk_1.default.yellow('⚠️ Failed to upload results:'), uploadResult.error);
149
+ // Upload each run
150
+ for (const run of pendingRuns) {
151
+ const runData = {
152
+ project_id: projectId,
153
+ environment: options.ci ? 'ci' : 'local',
154
+ ci_provider: ciProvider,
155
+ total_tests: run.totalTests,
156
+ passed_tests: run.passed,
157
+ failed_tests: run.failed,
158
+ duration_ms: run.duration,
159
+ cost_usd: run.results.reduce((sum, r) => sum + (r.metadata.cost || 0), 0) || 0.05, // fallback
160
+ started_at: new Date(run.timestamp.getTime() - run.duration).toISOString(),
161
+ completed_at: run.timestamp.toISOString(),
162
+ test_results: run.results.map(r => ({
163
+ test_name: r.testCase.description,
164
+ test_description: r.testCase.description,
165
+ prompt: typeof r.testCase.prompt === 'string' ? r.testCase.prompt : JSON.stringify(r.testCase.prompt),
166
+ input_data: r.testCase.variables,
167
+ expected_output: r.expectedOutput,
168
+ actual_output: r.actualOutput,
169
+ score: r.score,
170
+ method: r.testCase.config?.method || 'exact',
171
+ status: r.status,
172
+ model: r.metadata.provider || '',
173
+ tokens_used: r.metadata.tokens,
174
+ latency_ms: r.metadata.duration,
175
+ cost_usd: r.metadata.cost,
176
+ })),
177
+ ...gitContext // Applying current git context to old runs slightly inaccurate but acceptable
178
+ };
179
+ const uploadResult = await cloudService.uploadRun(runData);
180
+ if (uploadResult.success) {
181
+ db.markAsUploaded(run.id);
182
+ console.log(chalk_1.default.green(` ✓ Uploaded run from ${run.timestamp.toLocaleTimeString()}`));
183
+ }
184
+ else {
185
+ console.log(chalk_1.default.red(` ✗ Failed to upload run ${run.id}: ${uploadResult.error}`));
186
+ }
215
187
  }
216
188
  }
@@ -2,6 +2,7 @@ import { FailedTest, OptimizationResult } from '../types/fix';
2
2
  export declare class PromptOptimizer {
3
3
  private anthropic?;
4
4
  private openai?;
5
+ private openrouter?;
5
6
  constructor();
6
7
  /**
7
8
  * Main optimization method
@@ -44,19 +44,34 @@ const constraintExtractor_1 = require("./constraintExtractor");
44
44
  class PromptOptimizer {
45
45
  anthropic;
46
46
  openai;
47
+ openrouter;
47
48
  constructor() {
48
49
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
49
- if (anthropicKey && !anthropicKey.startsWith('api_key') && anthropicKey !== 'phc_xxxxx') {
50
+ if (anthropicKey &&
51
+ !anthropicKey.includes('your_key') &&
52
+ !anthropicKey.startsWith('api_key') &&
53
+ anthropicKey !== 'phc_xxxxx') {
50
54
  this.anthropic = new sdk_1.default({
51
55
  apiKey: anthropicKey
52
56
  });
53
57
  }
54
58
  const openaiKey = process.env.OPENAI_API_KEY;
55
- if (openaiKey && !openaiKey.startsWith('api_key')) {
59
+ if (openaiKey && !openaiKey.includes('your_key')) {
56
60
  this.openai = new openai_1.default({
57
61
  apiKey: openaiKey
58
62
  });
59
63
  }
64
+ const openrouterKey = process.env.OPENROUTER_API_KEY;
65
+ if (openrouterKey && !openrouterKey.includes('your_key')) {
66
+ this.openrouter = new openai_1.default({
67
+ baseURL: 'https://openrouter.ai/api/v1',
68
+ apiKey: openrouterKey,
69
+ defaultHeaders: {
70
+ 'HTTP-Referer': 'https://tuneprompt.xyz',
71
+ 'X-Title': 'TunePrompt CLI',
72
+ },
73
+ });
74
+ }
60
75
  }
61
76
  /**
62
77
  * Main optimization method
@@ -118,7 +133,7 @@ class PromptOptimizer {
118
133
  if (provider === 'anthropic' && this.anthropic) {
119
134
  console.log(`⚡ Using Anthropic for candidate generation...`);
120
135
  const response = await this.anthropic.messages.create({
121
- model: 'claude-sonnet-4-20250514',
136
+ model: 'claude-3-5-sonnet-20240620',
122
137
  max_tokens: 4000,
123
138
  temperature: 0.7, // Some creativity for prompt rewriting
124
139
  messages: [{
@@ -174,16 +189,34 @@ class PromptOptimizer {
174
189
  }
175
190
  ];
176
191
  }
177
- else if (provider === 'openrouter') {
178
- // For OpenRouter, we'll use the shadowTester to get a response
192
+ else if (provider === 'openrouter' && this.openrouter) {
179
193
  console.log(`⚡ Using OpenRouter for candidate generation...`);
180
- // Since OpenRouter is used in shadow testing, we'll use a different approach
181
- // For now, we'll return a basic fallback since OpenRouter doesn't support structured outputs as well
182
- return [{
183
- prompt: this.createFallbackPrompt(failedTest),
184
- reasoning: 'Generated using fallback method',
194
+ const response = await this.openrouter.chat.completions.create({
195
+ model: 'anthropic/claude-3-sonnet', // Default robust model on OpenRouter
196
+ messages: [{
197
+ role: 'user',
198
+ content: metaPrompt
199
+ }],
200
+ response_format: { type: 'json_object' }
201
+ });
202
+ const content = response.choices[0]?.message?.content;
203
+ if (!content) {
204
+ // Fallback if model doesn't support JSON mode or returns empty
205
+ throw new Error('No content returned from OpenRouter');
206
+ }
207
+ const parsed = JSON.parse(content);
208
+ return [
209
+ {
210
+ prompt: parsed.candidateA.prompt,
211
+ reasoning: parsed.candidateA.reasoning,
185
212
  score: 0
186
- }];
213
+ },
214
+ {
215
+ prompt: parsed.candidateB.prompt,
216
+ reasoning: parsed.candidateB.reasoning,
217
+ score: 0
218
+ }
219
+ ];
187
220
  }
188
221
  }
189
222
  catch (error) {
@@ -195,7 +228,7 @@ class PromptOptimizer {
195
228
  console.error('All providers failed for candidate generation');
196
229
  return [{
197
230
  prompt: this.createFallbackPrompt(failedTest),
198
- reasoning: 'Fallback prompt with basic improvements',
231
+ reasoning: 'Generated using fallback method',
199
232
  score: 0
200
233
  }];
201
234
  }
@@ -10,7 +10,7 @@ class CloudService {
10
10
  backendUrl;
11
11
  subscriptionId;
12
12
  constructor() {
13
- this.backendUrl = process.env.TUNEPROMPT_API_URL || process.env.BACKEND_URL || 'https://api.tuneprompt.com';
13
+ this.backendUrl = process.env.TUNEPROMPT_API_URL || process.env.BACKEND_URL || 'https://i8e3mu8jlk.execute-api.ap-south-1.amazonaws.com/dev';
14
14
  }
15
15
  async init() {
16
16
  // Load subscription ID from local storage (Phase 2 activation)
@@ -21,9 +21,16 @@ class CloudService {
21
21
  return license?.subscriptionId;
22
22
  }
23
23
  async uploadRun(data) {
24
- if (!this.subscriptionId) {
24
+ // Enforce Pro plan check
25
+ const license = (0, license_1.loadLicense)();
26
+ if (!this.subscriptionId || !license) {
25
27
  return { success: false, error: 'Not activated. Run `tuneprompt activate` first.' };
26
28
  }
29
+ // Check for specific pro plans (if we ever add free tiers)
30
+ const proPlans = ['pro-monthly', 'pro-yearly', 'lifetime'];
31
+ if (!proPlans.includes(license.plan)) {
32
+ return { success: false, error: 'Cloud features are restricted to Pro users. Please upgrade your plan.' };
33
+ }
27
34
  try {
28
35
  const response = await axios_1.default.post(`${this.backendUrl}/api/cloud/ingest-run`, data, {
29
36
  headers: {
@@ -5,6 +5,8 @@ export declare class TestDatabase {
5
5
  private migrate;
6
6
  saveRun(run: TestRun): void;
7
7
  getRecentRuns(limit?: number): TestRun[];
8
+ getPendingUploads(): TestRun[];
9
+ markAsUploaded(runId: string): void;
8
10
  private getRunResults;
9
11
  close(): void;
10
12
  }
@@ -87,26 +87,38 @@ class TestDatabase {
87
87
  CREATE INDEX IF NOT EXISTS idx_run_timestamp ON test_runs(timestamp);
88
88
  CREATE INDEX IF NOT EXISTS idx_result_run ON test_results(run_id);
89
89
  `);
90
+ // Migration for uploaded status
91
+ try {
92
+ this.db.exec(`ALTER TABLE test_runs ADD COLUMN uploaded INTEGER DEFAULT 0`);
93
+ }
94
+ catch (e) {
95
+ // Column might already exist
96
+ }
97
+ // Migration for provider column
98
+ try {
99
+ this.db.exec(`ALTER TABLE test_results ADD COLUMN provider TEXT`);
100
+ }
101
+ catch (e) {
102
+ // Column might already exist
103
+ }
90
104
  // Run external migrations (Phase 2)
91
- // Note: runMigrations is async but contains synchronous better-sqlite3 calls
92
- // so it executes immediately. We catch any promise rejection just in case.
93
105
  (0, migrate_1.runMigrations)(this.db).catch((err) => {
94
106
  console.error('Phase 2 migration failed:', err);
95
107
  });
96
108
  }
97
109
  saveRun(run) {
98
110
  const insertRun = this.db.prepare(`
99
- INSERT INTO test_runs (id, timestamp, total_tests, passed, failed, duration)
100
- VALUES (?, ?, ?, ?, ?, ?)
111
+ INSERT INTO test_runs (id, timestamp, total_tests, passed, failed, duration, uploaded)
112
+ VALUES (?, ?, ?, ?, ?, ?, 0)
101
113
  `);
102
114
  insertRun.run(run.id, run.timestamp.getTime(), run.totalTests, run.passed, run.failed, run.duration);
103
115
  const insertResult = this.db.prepare(`
104
116
  INSERT INTO test_results
105
- (id, run_id, description, prompt, variables, expect, config, file_path, status, score, actual_output, expected_output, error, duration, tokens, cost)
106
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
117
+ (id, run_id, description, prompt, variables, expect, config, file_path, status, score, actual_output, expected_output, error, duration, tokens, cost, provider)
118
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
107
119
  `);
108
120
  for (const result of run.results) {
109
- insertResult.run(result.id, run.id, result.testCase.description, typeof result.testCase.prompt === 'string' ? result.testCase.prompt : JSON.stringify(result.testCase.prompt), result.testCase.variables ? JSON.stringify(result.testCase.variables) : null, typeof result.testCase.expect === 'string' ? result.testCase.expect : JSON.stringify(result.testCase.expect), result.testCase.config ? JSON.stringify(result.testCase.config) : null, result.testCase.filePath || null, result.status, result.score, result.actualOutput, result.expectedOutput, result.error || null, result.metadata.duration, result.metadata.tokens || null, result.metadata.cost || null);
121
+ insertResult.run(result.id, run.id, result.testCase.description, typeof result.testCase.prompt === 'string' ? result.testCase.prompt : JSON.stringify(result.testCase.prompt), result.testCase.variables ? JSON.stringify(result.testCase.variables) : null, typeof result.testCase.expect === 'string' ? result.testCase.expect : JSON.stringify(result.testCase.expect), result.testCase.config ? JSON.stringify(result.testCase.config) : null, result.testCase.filePath || null, result.status, result.score, result.actualOutput, result.expectedOutput, result.error || null, result.metadata.duration, result.metadata.tokens || null, result.metadata.cost || null, result.metadata.provider || null);
110
122
  }
111
123
  }
112
124
  getRecentRuns(limit = 10) {
@@ -125,6 +137,25 @@ class TestDatabase {
125
137
  results: this.getRunResults(run.id)
126
138
  }));
127
139
  }
140
+ getPendingUploads() {
141
+ const runs = this.db.prepare(`
142
+ SELECT * FROM test_runs
143
+ WHERE uploaded = 0 OR uploaded IS NULL
144
+ ORDER BY timestamp ASC
145
+ `).all();
146
+ return runs.map(run => ({
147
+ id: run.id,
148
+ timestamp: new Date(run.timestamp),
149
+ totalTests: run.total_tests,
150
+ passed: run.passed,
151
+ failed: run.failed,
152
+ duration: run.duration,
153
+ results: this.getRunResults(run.id)
154
+ }));
155
+ }
156
+ markAsUploaded(runId) {
157
+ this.db.prepare(`UPDATE test_runs SET uploaded = 1 WHERE id = ?`).run(runId);
158
+ }
128
159
  getRunResults(runId) {
129
160
  const results = this.db.prepare(`
130
161
  SELECT * FROM test_results WHERE run_id = ?
@@ -167,7 +198,8 @@ class TestDatabase {
167
198
  duration: r.duration,
168
199
  timestamp: new Date(),
169
200
  tokens: r.tokens,
170
- cost: r.cost
201
+ cost: r.cost,
202
+ provider: r.provider
171
203
  }
172
204
  };
173
205
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuneprompt",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Industrial-grade testing framework for LLM prompts",
5
5
  "repository": {
6
6
  "type": "git",