scripter-x 1.0.23 → 1.0.24

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.23",
3
+ "version": "1.0.24",
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
@@ -3,7 +3,7 @@
3
3
  // stdin/stdout). Each command: async (io, api, args) => void.
4
4
  import * as config from './config.js';
5
5
  import { ApiClient, AuthError } from './api.js';
6
- import { saveSessions, CANCEL } from './util.js';
6
+ import { saveSessions, saveSessionsLocal, CANCEL } from './util.js';
7
7
  import * as tp from './providers/tempotp.js';
8
8
  import * as oc from './providers/otpcart.js';
9
9
  import * as kuku from './providers/kuku.js';
@@ -205,18 +205,27 @@ export async function run(io, api, args = {}) {
205
205
 
206
206
  io.print(` ✓ done — ${stats.succeeded} succeeded · ${stats.failed} failed · ${stats.cancelled} cancelled · ₹${stats.charges} spent`);
207
207
  if (stats.succeeded > 0) {
208
- let saved = null, lastErr = null;
209
- for (let a = 0; a < 5 && !saved; a++) {
210
- if (a > 0) await new Promise((r) => setTimeout(r, 1000));
211
- try { saved = await saveSessions(api, cid, { campaignName: name }); } catch (e) { lastErr = e; }
208
+ // PRIMARY: save LOCALLY from the worker's in-memory buffer — never depends on the
209
+ // server, so an expired dashboard JWT can't lose the JSONs the user paid for.
210
+ let saved = null;
211
+ try {
212
+ saved = saveSessionsLocal(worker.results, cid, { campaignName: name, out: args.out, checkMinutes });
213
+ } catch (e) {
214
+ io.print(` ! local save error: ${e.message}`, 'danger');
215
+ }
216
+ // FALLBACK: only if the local buffer was somehow empty, try the server export.
217
+ if (!saved && worker.results.length === 0) {
218
+ for (let a = 0; a < 3 && !saved; a++) {
219
+ if (a > 0) await new Promise((r) => setTimeout(r, 1000));
220
+ try { saved = await saveSessions(api, cid, { campaignName: name, out: args.out }); } catch { /* */ }
221
+ }
212
222
  }
213
223
  if (saved) {
214
224
  io.print(' ✓ saved session(s) to:');
215
- for (const res of saved) {
216
- io.print(` ${res.path} (${res.count} sessions)`, 'accent');
217
- }
225
+ for (const res of saved) io.print(` ${res.label} ${res.path} (${res.count} sessions)`, 'accent');
226
+ } else {
227
+ io.print(' ! could not write the session file — check disk permissions', 'danger');
218
228
  }
219
- else io.print(` ! couldn't auto-save${lastErr ? ` (${lastErr.message})` : ''} — run \`export ${name}\` to retry`);
220
229
  } else io.print(' ◉ no sessions extracted — nothing to save.');
221
230
  if (worker.logPath) io.print(` ◉ log saved to: ${worker.logPath}`);
222
231
  }
package/src/util.js CHANGED
@@ -31,34 +31,19 @@ export const log = {
31
31
  err: (m) => console.log(` ${paint.err('✗')} ${m}`),
32
32
  };
33
33
 
34
- // Resolve + write a campaign's sessions to a file. Returns {path, count} or null
35
- // (null when there are no extracted sessions). Mirrors the Python CLI's behavior:
36
- // out=<file> → that file; out=<dir>/ default name inside; no out → ~/Downloads/scripterx/
37
- export async function saveSessions(api, cid, { campaignName, out } = {}) {
38
- let name = (campaignName || cid.slice(0, 8)).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || cid.slice(0, 8);
39
- const accounts = await api.exportCampaign(cid);
34
+ // Core: partition an accounts array by Minutes status and write the JSON file(s).
35
+ // accounts = [{ session, coupon_eligible? }]. Pure local I/O no server.
36
+ // Returns [{label, path, count}] or null.
37
+ function writeAccounts(accounts, { name, out, checkMinutes } = {}) {
40
38
  if (!accounts || !accounts.length) return null;
41
39
 
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
40
  // Resolve base directory and optional forced filename.
51
41
  let baseDir, forcedName = null;
52
- const defaultName = `${name}-${cid.slice(0, 8)}.json`;
53
42
  if (out) {
54
43
  const expanded = out.startsWith('~') ? join(homedir(), out.slice(1)) : out;
55
44
  const isDir = (existsSync(expanded) && statSync(expanded).isDirectory()) || /[/\\]$/.test(out);
56
- if (isDir) {
57
- baseDir = expanded;
58
- } else {
59
- baseDir = dirname(expanded);
60
- forcedName = expanded.split(/[/\\]/).pop();
61
- }
45
+ if (isDir) baseDir = expanded;
46
+ else { baseDir = dirname(expanded); forcedName = expanded.split(/[/\\]/).pop(); }
62
47
  } else {
63
48
  const downloads = join(homedir(), 'Downloads');
64
49
  const base = existsSync(downloads) ? downloads : homedir();
@@ -67,22 +52,19 @@ export async function saveSessions(api, cid, { campaignName, out } = {}) {
67
52
 
68
53
  function write(suffix, sessionsToWrite) {
69
54
  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
- }
55
+ const fname = forcedName
56
+ ? (suffix === '' ? forcedName : forcedName.replace(/\.json$/i, `${suffix}.json`))
57
+ : `${name}${suffix}.json`;
76
58
  const dest = join(baseDir, fname);
77
59
  mkdirSync(baseDir, { recursive: true });
78
60
  writeFileSync(dest, JSON.stringify(sessionsToWrite, null, 2));
79
61
  return dest;
80
62
  }
81
63
 
82
- // Partition by coupon_eligible when minutes check was requested.
64
+ // Partition by coupon_eligible when the Minutes check was requested.
83
65
  const minutesFree = []; // coupon_eligible=true → no Minutes order, ₹100 coupon available
84
66
  const minutesUsed = []; // coupon_eligible=false → already placed a Minutes order
85
- const unchecked = []; // coupon_eligible absent → check wasn't done for this account
67
+ const unchecked = []; // coupon_eligible absent → check wasn't done
86
68
 
87
69
  let hasChecked = false;
88
70
  for (const a of accounts) {
@@ -101,10 +83,8 @@ export async function saveSessions(api, cid, { campaignName, out } = {}) {
101
83
  if (hasChecked || (checkMinutes && (minutesFree.length || minutesUsed.length))) {
102
84
  const destFree = write('-minutes-free', minutesFree);
103
85
  if (destFree) results.push({ label: '🟢 minutes-free (₹100 coupon)', path: destFree, count: minutesFree.length });
104
-
105
86
  const destUsed = write('-minutes-used', minutesUsed);
106
87
  if (destUsed) results.push({ label: '🔴 minutes-used (coupon gone)', path: destUsed, count: minutesUsed.length });
107
-
108
88
  if (unchecked.length) {
109
89
  const destUnk = write('-unchecked', unchecked);
110
90
  if (destUnk) results.push({ label: '⚪ unchecked', path: destUnk, count: unchecked.length });
@@ -114,6 +94,27 @@ export async function saveSessions(api, cid, { campaignName, out } = {}) {
114
94
  const dest = write('', allSessions);
115
95
  if (dest) results.push({ label: 'combined', path: dest, count: allSessions.length });
116
96
  }
117
-
118
97
  return results.length ? results : null;
119
98
  }
99
+
100
+ function campaignFileStem(campaignName, cid) {
101
+ const name = (campaignName || cid.slice(0, 8)).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || cid.slice(0, 8);
102
+ return `${name}-${cid.slice(0, 8)}`;
103
+ }
104
+
105
+ // LOCAL autosave — the primary save path. Writes directly from the worker's
106
+ // in-memory results, so a campaign's JSONs are ALWAYS saved even if the dashboard
107
+ // JWT expired mid-run (no server round-trip). Returns [{label,path,count}] or null.
108
+ export function saveSessionsLocal(accounts, cid, { campaignName, out, checkMinutes } = {}) {
109
+ return writeAccounts(accounts, { name: campaignFileStem(campaignName, cid), out, checkMinutes });
110
+ }
111
+
112
+ // Resolve + write a campaign's sessions to a file, fetching from the SERVER.
113
+ // Used by the standalone `export` command (no local buffer available there).
114
+ export async function saveSessions(api, cid, { campaignName, out } = {}) {
115
+ const accounts = await api.exportCampaign(cid);
116
+ if (!accounts || !accounts.length) return null;
117
+ let checkMinutes = false;
118
+ try { const { campaign } = await api.getCampaign(cid); checkMinutes = !!campaign.check_minutes; } catch { /* ignore */ }
119
+ return writeAccounts(accounts, { name: campaignFileStem(campaignName, cid), out, checkMinutes });
120
+ }
package/src/worker.js CHANGED
@@ -39,6 +39,10 @@ export class Worker {
39
39
  this.slots = {};
40
40
  this.seq = 0;
41
41
  this.stopped = false;
42
+ // Local buffer of every successful session — the source of truth for autosave.
43
+ // We NEVER depend on a server round-trip to save: even if the dashboard JWT
44
+ // expires mid-run, the file still gets written from here.
45
+ this.results = [];
42
46
  this.logger = new CampaignLogger({ campaignId, name: name || campaignId, provider: provider.name });
43
47
  }
44
48
 
@@ -69,6 +73,21 @@ export class Worker {
69
73
  else { this.stats.failed++; this.stats.charges += res.cost || 0; } // failed w/ SMS = charged by provider
70
74
  this.onEvent('progress', this.stats);
71
75
  this.onEvent('row', res);
76
+ // Buffer every successful session LOCALLY first — autosave reads from here, so a
77
+ // failed/expired server ingest can never lose the JSON the user paid for.
78
+ if (res.status === 'success' && res.session) {
79
+ this.results.push({
80
+ id_no: res.id_no, mobile: res.mobile || '', session: res.session,
81
+ minutes_checked: !!res.minutes_checked,
82
+ coupon_eligible: res.coupon_eligible ?? undefined,
83
+ has_minutes_order: res.has_minutes_order ?? undefined,
84
+ minutes_order_count: res.minutes_order_count ?? undefined,
85
+ minutes_orders: res.minutes_orders ?? undefined,
86
+ linked_email: res.linked_email ?? undefined,
87
+ email_linked: res.email_linked ?? undefined,
88
+ email_reason: res.email_reason ?? undefined,
89
+ });
90
+ }
72
91
  try {
73
92
  await this.api.ingestAccount(this.cid, {
74
93
  id_no: res.id_no, mobile: res.mobile || '', status: res.status, cost: res.cost || 0,