scripter-x 1.0.13 → 1.0.15

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripter-x",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "ScripterX — local Flipkart session extractor (runs on your residential IP, syncs to marthunt)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/commands.js CHANGED
@@ -208,7 +208,12 @@ export async function run(io, api, args = {}) {
208
208
  if (a > 0) await new Promise((r) => setTimeout(r, 1000));
209
209
  try { saved = await saveSessions(api, cid, { campaignName: name }); } catch (e) { lastErr = e; }
210
210
  }
211
- if (saved) { io.print(` ✓ saved ${saved.count} session(s) to:`); io.print(` ${saved.path}`, 'accent'); }
211
+ if (saved) {
212
+ io.print(' ✓ saved session(s) to:');
213
+ for (const res of saved) {
214
+ io.print(` ${res.path} (${res.count} sessions)`, 'accent');
215
+ }
216
+ }
212
217
  else io.print(` ! couldn't auto-save${lastErr ? ` (${lastErr.message})` : ''} — run \`export ${name}\` to retry`);
213
218
  } else io.print(' ◉ no sessions extracted — nothing to save.');
214
219
  if (worker.logPath) io.print(` ◉ log saved to: ${worker.logPath}`);
@@ -329,7 +334,12 @@ export async function exportCmd(io, api, args = {}) {
329
334
  if (a > 0) await new Promise((r) => setTimeout(r, 800));
330
335
  try { saved = await saveSessions(api, c.id, { campaignName: c.name, out: args.out }); } catch (e) { lastErr = e; }
331
336
  }
332
- if (saved) { io.print(` ✓ exported ${saved.count} session(s) to:`); io.print(` ${saved.path}`, 'accent'); }
337
+ if (saved) {
338
+ io.print(' ✓ exported session(s) to:');
339
+ for (const res of saved) {
340
+ io.print(` ${res.path} (${res.count} sessions)`, 'accent');
341
+ }
342
+ }
333
343
  else if (lastErr) io.print(` ✗ ${lastErr.message}`);
334
344
  else io.print(' ! no extracted sessions in this campaign yet.');
335
345
  }
package/src/util.js CHANGED
@@ -37,21 +37,81 @@ export const log = {
37
37
  export async function saveSessions(api, cid, { campaignName, out } = {}) {
38
38
  let name = (campaignName || cid.slice(0, 8)).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || cid.slice(0, 8);
39
39
  const accounts = await api.exportCampaign(cid);
40
- const sessions = (accounts || []).map((a) => a.session).filter(Boolean);
41
- if (!sessions.length) return null;
40
+ if (!accounts || !accounts.length) return null;
42
41
 
42
+ let checkMinutes = false;
43
+ try {
44
+ const { campaign } = await api.getCampaign(cid);
45
+ checkMinutes = !!campaign.check_minutes;
46
+ } catch (e) {
47
+ // ignore
48
+ }
49
+
50
+ // Resolve base directory and optional forced filename.
51
+ let baseDir, forcedName = null;
43
52
  const defaultName = `${name}-${cid.slice(0, 8)}.json`;
44
- let dest;
45
53
  if (out) {
46
54
  const expanded = out.startsWith('~') ? join(homedir(), out.slice(1)) : out;
47
55
  const isDir = (existsSync(expanded) && statSync(expanded).isDirectory()) || /[/\\]$/.test(out);
48
- dest = isDir ? join(expanded, defaultName) : expanded;
56
+ if (isDir) {
57
+ baseDir = expanded;
58
+ } else {
59
+ baseDir = dirname(expanded);
60
+ forcedName = expanded.split(/[/\\]/).pop();
61
+ }
49
62
  } else {
50
63
  const downloads = join(homedir(), 'Downloads');
51
64
  const base = existsSync(downloads) ? downloads : homedir();
52
- dest = join(base, 'scripterx', defaultName);
65
+ baseDir = join(base, 'scripterx');
66
+ }
67
+
68
+ function write(suffix, sessionsToWrite) {
69
+ if (!sessionsToWrite || !sessionsToWrite.length) return null;
70
+ let fname;
71
+ if (forcedName) {
72
+ fname = suffix === '' ? forcedName : forcedName.replace(/\.json$/i, `${suffix}.json`);
73
+ } else {
74
+ fname = `${name}-${cid.slice(0, 8)}${suffix}.json`;
75
+ }
76
+ const dest = join(baseDir, fname);
77
+ mkdirSync(baseDir, { recursive: true });
78
+ writeFileSync(dest, JSON.stringify(sessionsToWrite, null, 2));
79
+ return dest;
80
+ }
81
+
82
+ // Partition by coupon_eligible when minutes check was requested.
83
+ const minutesFree = []; // coupon_eligible=true → no Minutes order, ₹100 coupon available
84
+ const minutesUsed = []; // coupon_eligible=false → already placed a Minutes order
85
+ const unchecked = []; // coupon_eligible absent → check wasn't done for this account
86
+
87
+ for (const a of accounts) {
88
+ const sess = a.session;
89
+ if (!sess) continue;
90
+ const eligible = a.coupon_eligible;
91
+ if (checkMinutes && eligible !== undefined && eligible !== null) {
92
+ (eligible ? minutesFree : minutesUsed).push(sess);
93
+ } else {
94
+ unchecked.push(sess);
95
+ }
53
96
  }
54
- mkdirSync(dirname(dest), { recursive: true });
55
- writeFileSync(dest, JSON.stringify(sessions, null, 2));
56
- return { path: dest, count: sessions.length };
97
+
98
+ const results = [];
99
+ if (checkMinutes && (minutesFree.length || minutesUsed.length)) {
100
+ const destFree = write('-minutes-free', minutesFree);
101
+ if (destFree) results.push({ label: '🟢 minutes-free (₹100 coupon)', path: destFree, count: minutesFree.length });
102
+
103
+ const destUsed = write('-minutes-used', minutesUsed);
104
+ if (destUsed) results.push({ label: '🔴 minutes-used (coupon gone)', path: destUsed, count: minutesUsed.length });
105
+
106
+ if (unchecked.length) {
107
+ const destUnk = write('-unchecked', unchecked);
108
+ if (destUnk) results.push({ label: '⚪ unchecked', path: destUnk, count: unchecked.length });
109
+ }
110
+ } else {
111
+ const allSessions = [...minutesFree, ...minutesUsed, ...unchecked];
112
+ const dest = write('', allSessions);
113
+ if (dest) results.push({ label: 'combined', path: dest, count: allSessions.length });
114
+ }
115
+
116
+ return results.length ? results : null;
57
117
  }
package/src/worker.js CHANGED
@@ -48,6 +48,8 @@ export class Worker {
48
48
  _claim() {
49
49
  if (this.stopped) return 0;
50
50
  if (this.stats.succeeded >= this.requested) return 0;
51
+ const active = this.stats.attempts - (this.stats.succeeded + this.stats.failed + this.stats.cancelled);
52
+ if (this.stats.succeeded + active >= this.requested) return 0;
51
53
  if (this.stats.attempts >= this.requested * ATTEMPT_CAP_MULT) return 0;
52
54
  return ++this.stats.attempts;
53
55
  }
@@ -119,9 +121,24 @@ export class Worker {
119
121
  const idNo = this._nextSeq();
120
122
  const res = { id_no: idNo, status: 'failed', cost: 0, detail: '' };
121
123
 
124
+ if (this.stats.succeeded >= this.requested) {
125
+ this._emit(slot, { phase: 'done', detail: 'goal reached' });
126
+ res.status = 'cancelled';
127
+ res.detail = 'goal reached';
128
+ return res;
129
+ }
130
+
122
131
  this._emit(slot, { mobile: '', phase: 'renting', detail: 'requesting a number', wait: 0 });
123
132
  const number = await this._rent(slot);
124
133
  if (!number) { res.status = 'cancelled'; res.detail = 'stopped while renting'; return res; }
134
+ if (this.stats.succeeded >= this.requested) {
135
+ this.log(`goal reached by another slot; releasing ${number.mobile} immediately`);
136
+ this._release(number, Date.now());
137
+ this._emit(slot, { phase: 'done', detail: 'goal reached' });
138
+ res.status = 'cancelled';
139
+ res.detail = 'goal reached';
140
+ return res;
141
+ }
125
142
  const rentedAt = Date.now();
126
143
  res.mobile = number.mobile;
127
144
  this._emit(slot, { mobile: number.mobile, phase: 'rented', detail: 'got a number' });
@@ -135,9 +152,13 @@ export class Worker {
135
152
  // releaseOnce — the single chokepoint that schedules the provider cancel. Called from
136
153
  // every terminal path (fail, success, error). Without this a blocked/errored number
137
154
  // could stay rented → we'd be charged.
138
- const releaseOnce = () => {
155
+ const releaseOnce = (charged = false, doneDetail = 'done') => {
139
156
  if (released) return;
140
157
  released = true;
158
+ if (charged) {
159
+ this._emit(slot, { phase: 'done', detail: doneDetail });
160
+ return;
161
+ }
141
162
  this._emit(slot, { phase: 'cancelling', detail: 'releasing number' });
142
163
  this._release(number, rentedAt);
143
164
  };
@@ -148,7 +169,7 @@ export class Worker {
148
169
  res.cost = charged ? number.cost : 0;
149
170
  res.detail = detail;
150
171
  this.log(`FAILED ${number.mobile}: ${detail} (charged: ${charged})`);
151
- releaseOnce();
172
+ releaseOnce(charged, 'failed');
152
173
  return res;
153
174
  };
154
175
 
@@ -219,10 +240,10 @@ export class Worker {
219
240
  }
220
241
 
221
242
  this._emit(slot, { phase: 'saving', detail: 'saving session' });
222
- res.status = 'success'; res.cost = number.cost; res.session = session;
223
243
  this.log(`SUCCESS ${number.mobile} — session extracted (₹${number.cost})${res.email_linked ? ` + email ${res.linked_email}` : ''}`);
224
- this._emit(slot, { phase: 'done', detail: res.email_linked ? `extracted + ${res.linked_email}` : 'extracted' });
225
- releaseOnce(); // success still releases the number (OTP already consumed)
244
+ const doneDetail = res.email_linked ? `extracted + ${res.linked_email}` : 'extracted';
245
+ this._emit(slot, { phase: 'done', detail: doneDetail });
246
+ releaseOnce(true, doneDetail); // success still releases the number (OTP already consumed)
226
247
  return res;
227
248
  } catch (e) {
228
249
  // ANY unexpected error → still release the number so we're never charged for a leak