tokenwait 0.1.2 → 0.1.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.
package/bin/tokenwait.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  // TokenWait CLI.
4
- // npx tokenwait install [--agents claude,codex,gemini] [--server URL] [--yes] [--no-login]
4
+ // npx tokenwait install [--agents claude,codex,gemini] [--server URL] [--yes] [--no-login] [--ide all|none]
5
+ // npx tokenwait ide [--list] [--yes] дополнение TokenWait для VS Code / Cursor / JetBrains IDE: поставить или обновить
6
+ // npx tokenwait update обновить хуки агентов и уже поставленные дополнения IDE
5
7
  // tokenwait uninstall [--purge]
6
8
  // tokenwait login | logout | status [--json]
7
9
  // tokenwait link-start --json | me --json | sync (для расширений IDE)
@@ -14,6 +16,7 @@ const { spawn } = require('child_process');
14
16
  const util = require('../lib/util');
15
17
  const agents = require('../lib/agents');
16
18
  const api = require('../lib/api');
19
+ const ide = require('../lib/ide');
17
20
  const worker = require('../lib/worker');
18
21
  const { pick } = require('../lib/rotation');
19
22
 
@@ -81,7 +84,8 @@ What it changes:
81
84
  • stores ads, counters and an install key in ~/.tokenwait
82
85
  What it never does:
83
86
  • read your code, prompts, responses or transcripts
84
- • send anything except ad ids, impression counts, waiting time and the install key
87
+ • send anything except ad ids, impression counts, waiting time, the install key,
88
+ the package version and which agents and IDE add-ons are connected
85
89
  Undo at any time: npx tokenwait uninstall
86
90
  `;
87
91
 
@@ -91,6 +95,7 @@ async function cmdInstall() {
91
95
  if (!flag('yes')) {
92
96
  console.log(CONSENT);
93
97
  console.log('Agents: ' + ids.map((id) => agents.AGENTS[id].name).join(', ') + '\n');
98
+ if (ids.includes('codex')) console.log('⚠ ' + agents.codexNote() + '\n');
94
99
  const a = await ask('Install TokenWait? [y/N] ');
95
100
  if (!/^y(es)?$/i.test(a)) return console.log('Cancelled. Nothing was changed.');
96
101
  }
@@ -108,7 +113,7 @@ async function cmdInstall() {
108
113
  const r = agents.install(id, node, app);
109
114
  results.push({ agent: id, ok: true, file: r.file });
110
115
  if (!json) console.log(`✓ ${agents.AGENTS[id].name}: ${r.file}`);
111
- if (!json && agents.AGENTS[id].note) console.log(' ' + agents.AGENTS[id].note);
116
+ if (!json && id === 'codex') console.log(' ⚠ ' + agents.codexNote());
112
117
  } catch (e) {
113
118
  results.push({ agent: id, ok: false, error: e.message });
114
119
  if (!json) console.log(`✗ ${agents.AGENTS[id].name}: ${e.message}`);
@@ -119,14 +124,119 @@ async function cmdInstall() {
119
124
  } catch (e) {
120
125
  if (!json) console.log('Could not reach the server yet (' + e.message + '). Ads will load automatically later.');
121
126
  }
127
+ // дополнения для IDE: спрашиваем в обычной установке; при --yes (так зовут установщик сами плагины) — только с --ide all
128
+ let ideResults = [];
129
+ const ideOpt = opt('ide');
130
+ if (ideOpt !== 'none' && !json && (!flag('yes') || ideOpt === 'all')) ideResults = await offerIde(!flag('yes'));
122
131
  if (json) return out({ ok: results.every((r) => r.ok), agents: results });
123
132
  console.log('\nRestart your agent sessions to pick up the changes.');
133
+ if (ideResults.some((r) => r.ok && r.target.kind === 'jetbrains' && (r.action === 'install' || r.action === 'update')))
134
+ console.log('Restart your JetBrains IDE to load the TokenWait plugin.');
124
135
  if (!flag('no-login') && api.credentials()) {
125
136
  const a = await ask('Sign in now (GitHub, Google or Apple) so earnings go to your account? [Y/n] ');
126
137
  if (!/^n/i.test(a)) await cmdLogin();
127
138
  }
128
139
  }
129
140
 
141
+ // ---------- дополнения для IDE ----------
142
+
143
+ const ACTION_TEXT = { install: 'installed', update: 'updated', current: 'up to date', 'too-old': 'IDE version too old', skipped: 'not installed' };
144
+
145
+ function describeTargets(targets) {
146
+ return targets.map((t) => `${t.name}${t.installed ? ` (TokenWait ${t.installed})` : ''}`).join(', ');
147
+ }
148
+
149
+ function printIdeResults(results, versions) {
150
+ for (const r of results) {
151
+ const want = r.target.kind === 'jetbrains' ? versions.jetbrains : versions.vscode;
152
+ if (r.ok) console.log(`${r.action === 'current' || r.action === 'skipped' || r.action === 'too-old' ? '·' : '✓'} ${r.target.name}: ${ACTION_TEXT[r.action] || r.action}${r.action === 'install' || r.action === 'update' ? ' ' + want : ''}`);
153
+ else console.log(`✗ ${r.target.name}: ${r.error}`);
154
+ }
155
+ }
156
+
157
+ /** В обычной установке: найти IDE и предложить поставить туда дополнение. */
158
+ async function offerIde(interactive) {
159
+ const targets = ide.detectAll();
160
+ if (!targets.length) return [];
161
+ let versions;
162
+ try {
163
+ versions = await ide.latest(api.server());
164
+ } catch (e) {
165
+ console.log('\nIDE add-ons: could not reach the server (' + e.message + '). Later: npx tokenwait ide');
166
+ return [];
167
+ }
168
+ const todo = targets.filter((t) => ['install', 'update'].includes(ide.plan(t, versions)));
169
+ if (!todo.length) return [];
170
+ console.log('\nFound: ' + describeTargets(todo));
171
+ console.log('The TokenWait add-on shows the sponsored line in the IDE status bar (for Codex and Gemini too) and lets you sign in from the IDE.');
172
+ if (interactive) {
173
+ const a = await ask('Install or update the TokenWait add-on there? [Y/n] ');
174
+ if (/^n/i.test(a)) {
175
+ console.log('Skipped. Later: npx tokenwait ide');
176
+ return [];
177
+ }
178
+ }
179
+ const res = await ide.apply(api.server(), todo, versions);
180
+ printIdeResults(res, versions);
181
+ return res;
182
+ }
183
+
184
+ async function cmdIde() {
185
+ const targets = ide.detectAll();
186
+ const versions = await ide.latest(api.server());
187
+ if (flag('list') || !targets.length) {
188
+ if (json) return out({ ok: true, latest: versions, ides: targets.map((t) => ({ kind: t.kind, id: t.id, name: t.name, installed: t.installed, action: ide.plan(t, versions) })) });
189
+ if (!targets.length) return console.log('No supported IDE found (VS Code, Cursor, Windsurf, VSCodium, JetBrains IDEs).');
190
+ console.log(`Latest: VS Code add-on ${versions.vscode}, JetBrains plugin ${versions.jetbrains}`);
191
+ const LIST_TEXT = { install: 'can be installed', update: 'update available', current: 'up to date', 'too-old': 'IDE version too old' };
192
+ for (const t of targets) console.log(` ${t.name.padEnd(26)} ${(t.installed ? 'TokenWait ' + t.installed : 'not installed').padEnd(18)} ${LIST_TEXT[ide.plan(t, versions)]}`);
193
+ return;
194
+ }
195
+ const todo = targets.filter((t) => ['install', 'update'].includes(ide.plan(t, versions)));
196
+ if (!todo.length) {
197
+ if (json) return out({ ok: true, results: [] });
198
+ return console.log('TokenWait add-ons are up to date in: ' + targets.map((t) => t.name).join(', '));
199
+ }
200
+ if (!flag('yes') && !json) {
201
+ console.log('Will install or update the TokenWait add-on in: ' + describeTargets(todo));
202
+ console.log(`Files come from ${api.server()}/downloads.`);
203
+ const a = await ask('Continue? [Y/n] ');
204
+ if (/^n/i.test(a)) return console.log('Cancelled. Nothing was changed.');
205
+ }
206
+ const res = await ide.apply(api.server(), todo, versions);
207
+ if (json) return out({ ok: res.every((r) => r.ok), results: res.map((r) => ({ name: r.target.name, action: r.action, ok: r.ok, error: r.error })) });
208
+ printIdeResults(res, versions);
209
+ if (res.some((r) => r.ok && r.target.kind === 'jetbrains')) console.log('Restart your JetBrains IDE to load the new plugin.');
210
+ if (res.some((r) => r.ok && r.target.kind === 'vscode')) console.log('Reload VS Code windows (Developer: Reload Window) to start the new version.');
211
+ }
212
+
213
+ /** Обновить: копию пакета, хуки уже подключённых агентов и уже поставленные дополнения IDE. */
214
+ async function cmdUpdate() {
215
+ const ids = Object.keys(agents.AGENTS).filter((id) => agents.isInstalled(id));
216
+ if (!ids.length && !json) console.log('TokenWait is not installed for any agent yet. Run: npx tokenwait install');
217
+ const app = copyApp();
218
+ for (const id of ids) {
219
+ try {
220
+ agents.install(id, process.execPath, app);
221
+ if (!json) console.log(`✓ ${agents.AGENTS[id].name}: hooks updated (TokenWait ${VERSION})`);
222
+ } catch (e) {
223
+ if (!json) console.log(`✗ ${agents.AGENTS[id].name}: ${e.message}`);
224
+ }
225
+ }
226
+ let res = [];
227
+ let versions = null;
228
+ try {
229
+ versions = await ide.latest(api.server());
230
+ res = await ide.apply(api.server(), ide.detectAll(), versions, { onlyInstalled: true });
231
+ } catch (e) {
232
+ if (!json) console.log('IDE add-ons: ' + e.message);
233
+ }
234
+ if (json) return out({ ok: res.every((r) => r.ok), version: VERSION, latest: versions, ide: res.map((r) => ({ name: r.target.name, action: r.action, ok: r.ok, error: r.error })) });
235
+ if (versions) printIdeResults(res.filter((r) => r.action !== 'skipped'), versions);
236
+ if (versions && ide.cmpVersion(VERSION, versions.core) < 0) console.log(`\nA newer package is available (${versions.core}). Run: npx tokenwait@latest update`);
237
+ if (res.some((r) => r.ok && r.target.kind === 'jetbrains' && r.action === 'update')) console.log('Restart your JetBrains IDE to load the new plugin.');
238
+ }
239
+
130
240
  async function cmdUninstall() {
131
241
  const res = [];
132
242
  for (const id of Object.keys(agents.AGENTS)) {
@@ -211,10 +321,12 @@ function help() {
211
321
  npx tokenwait install set up Claude Code / Codex CLI / Gemini CLI
212
322
  npx tokenwait login link this machine to your account (GitHub, Google or Apple)
213
323
  npx tokenwait status what is installed, impressions and earnings
324
+ npx tokenwait ide install or update the add-on for VS Code, Cursor and JetBrains IDEs
325
+ npx tokenwait update update agent hooks and installed IDE add-ons
214
326
  npx tokenwait logout unlink this machine
215
327
  npx tokenwait uninstall remove everything from agent configs (--purge also deletes ~/.tokenwait)
216
328
 
217
- Options: --agents claude,codex,gemini --server URL --yes --json`);
329
+ Options: --agents claude,codex,gemini --server URL --yes --json --ide all|none --list`);
218
330
  }
219
331
 
220
332
  const commands = {
@@ -223,6 +335,8 @@ const commands = {
223
335
  login: cmdLogin,
224
336
  logout: cmdLogout,
225
337
  status: cmdStatus,
338
+ ide: cmdIde,
339
+ update: cmdUpdate,
226
340
  'link-start': async () => out(await cmdLinkStart()),
227
341
  me: async () => out(await api.me()),
228
342
  sync: async () => {
package/lib/agents.js CHANGED
@@ -33,7 +33,7 @@ const AGENTS = {
33
33
  file: () => path.join(os.homedir(), '.codex', 'hooks.json'),
34
34
  events: { UserPromptSubmit: 'start', Stop: 'stop' },
35
35
  statusLine: false,
36
- note: 'Codex runs new hooks only after you approve them: start codex and run /hooks once.',
36
+ note: 'codex', // текст — codexNote(): Codex один раз спрашивает разрешение на хуки
37
37
  },
38
38
  gemini: {
39
39
  name: 'Gemini CLI',
@@ -191,4 +191,23 @@ function restoreLegacySpinner(s) {
191
191
  }
192
192
  }
193
193
 
194
- module.exports = { AGENTS, detected, install, uninstall, isInstalled, parseStatusLine, HOOK_MARK, _p: p };
194
+ // Codex запускает новые хуки только после одобрения пользователем — предупреждаем заранее, чтобы окно не было неожиданным.
195
+ const CODEX_NOTE = {
196
+ en: 'Codex will ask once to approve the 2 TokenWait hooks (start and end of a response). Please allow them: in the terminal run /hooks; in the Codex panel of your IDE a hook review window opens: choose “Allow all”. Without this TokenWait cannot see when Codex is thinking, so no ads are shown and nothing is earned for Codex.',
197
+ ru: 'Codex один раз попросит разрешить 2 хука TokenWait (начало и конец ответа). Разрешите их: в терминале — командой /hooks, в панели Codex в IDE откроется окно «Проверка хуков» — нажмите «Разрешить все». Без этого TokenWait не видит, когда Codex думает: реклама для Codex не показывается и не засчитывается.',
198
+ };
199
+
200
+ /** Язык системы для сообщений в терминале. */
201
+ function sysLang() {
202
+ const v = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '';
203
+ if (v) return /^ru/i.test(v) ? 'ru' : 'en';
204
+ try {
205
+ return /^ru/i.test(Intl.DateTimeFormat().resolvedOptions().locale) ? 'ru' : 'en';
206
+ } catch (_) {
207
+ return 'en';
208
+ }
209
+ }
210
+
211
+ const codexNote = (lang = sysLang()) => CODEX_NOTE[lang === 'ru' ? 'ru' : 'en'];
212
+
213
+ module.exports = { codexNote, sysLang, AGENTS, detected, install, uninstall, isInstalled, parseStatusLine, HOOK_MARK, _p: p };
package/lib/api.js CHANGED
@@ -24,8 +24,34 @@ function credentials() {
24
24
  return c && c.server === server() && c.client_id && c.client_secret ? c : null;
25
25
  }
26
26
 
27
- async function request(method, path, body, auth = true, timeoutMs = 5000) {
28
- const headers = { accept: 'application/json', 'user-agent': 'tokenwait/' + require('../package.json').version };
27
+ /**
28
+ * Что стоит на этой установке — для статистики оператора (без данных о человеке):
29
+ * подключённые агенты и дополнения IDE, которые выходили на связь за последние 7 дней (~/.tokenwait/ide.json).
30
+ */
31
+ function installInfo() {
32
+ const out = {};
33
+ try {
34
+ const agents = require('./agents');
35
+ out['x-tokenwait-agents'] = Object.keys(agents.AGENTS).filter((id) => agents.isInstalled(id)).join(',');
36
+ } catch (_) {}
37
+ const ide = readJson(p('ide.json'), {});
38
+ const fresh = Object.entries(ide && typeof ide === 'object' ? ide : {})
39
+ .filter(([k, v]) => /^(jetbrains|vscode)$/.test(k) && v && Date.now() - Number(v.t) < 7 * 86400_000 && /^\d+\.\d+\.\d+$/.test(String(v.v)))
40
+ .map(([k, v]) => k + '/' + v.v);
41
+ out['x-tokenwait-ide'] = fresh.join(',');
42
+ return out;
43
+ }
44
+
45
+ /** Отметка дополнения IDE: вызывают JetBrains и VS Code при запуске и раз в час. */
46
+ function markIde(kind, version) {
47
+ if (!/^(jetbrains|vscode)$/.test(kind)) return;
48
+ const ide = readJson(p('ide.json'), {});
49
+ ide[kind] = { v: String(version), t: Date.now() };
50
+ writeJson(p('ide.json'), ide);
51
+ }
52
+
53
+ async function request(method, path, body, auth = true, timeoutMs = 5000, extraHeaders = {}) {
54
+ const headers = { accept: 'application/json', 'user-agent': 'tokenwait/' + require('../package.json').version, ...extraHeaders };
29
55
  if (body !== undefined) headers['content-type'] = 'application/json';
30
56
  if (auth) {
31
57
  const c = credentials() || (await register());
@@ -55,7 +81,7 @@ async function register() {
55
81
  }
56
82
 
57
83
  async function fetchAds() {
58
- const r = await request('GET', '/v1/ads');
84
+ const r = await request('GET', '/v1/ads', undefined, true, 5000, installInfo());
59
85
  const data = {
60
86
  fetched_at: Date.now(),
61
87
  ttl_seconds: Math.min(Math.max(Number(r.ttl_seconds) || 600, 60), 3600),
@@ -76,6 +102,8 @@ async function me() {
76
102
  }
77
103
 
78
104
  module.exports = {
105
+ installInfo,
106
+ markIde,
79
107
  HttpError,
80
108
  server,
81
109
  config,
package/lib/ide.js ADDED
@@ -0,0 +1,309 @@
1
+ 'use strict';
2
+ // Дополнения TokenWait для IDE: найти установленные IDE и поставить или обновить в них дополнение.
3
+ //
4
+ // VS Code и его сборки (Cursor, Windsurf, VSCodium) — штатной командой `<cli> --install-extension file.vsix --force`.
5
+ // JetBrains IDE (IDEA, PhpStorm, WebStorm, PyCharm, GoLand, …) — распаковкой плагина в папку плагинов последней
6
+ // версии каждой IDE; новая версия подхватывается при перезапуске IDE.
7
+ //
8
+ // Файлы берутся с сервера TokenWait: /downloads/versions.json, /downloads/tokenwait-vscode.vsix,
9
+ // /downloads/tokenwait-jetbrains.zip. Без согласия пользователя ничего не ставится (см. bin/tokenwait.js).
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const zlib = require('zlib');
14
+ const { execFileSync } = require('child_process');
15
+
16
+ const VSIX_ID = 'tokenwait.tokenwait';
17
+ const JB_FOLDER = 'tokenwait';
18
+
19
+ // ---------- версии ----------
20
+
21
+ function cmpVersion(a, b) {
22
+ const pa = String(a || '0').split('.').map((x) => parseInt(x, 10) || 0);
23
+ const pb = String(b || '0').split('.').map((x) => parseInt(x, 10) || 0);
24
+ for (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) < (pb[i] || 0) ? -1 : 1;
25
+ return 0;
26
+ }
27
+
28
+ // ---------- распаковка zip (без зависимостей) ----------
29
+
30
+ /** Файлы архива: [{name, data}]. Пути с «..» и абсолютные отбрасываются. */
31
+ function unzip(buf) {
32
+ let eocd = -1;
33
+ for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) {
34
+ if (buf.readUInt32LE(i) === 0x06054b50) {
35
+ eocd = i;
36
+ break;
37
+ }
38
+ }
39
+ if (eocd < 0) throw new Error('not a zip archive');
40
+ const count = buf.readUInt16LE(eocd + 10);
41
+ let off = buf.readUInt32LE(eocd + 16);
42
+ const files = [];
43
+ for (let k = 0; k < count; k++) {
44
+ if (buf.readUInt32LE(off) !== 0x02014b50) throw new Error('broken zip archive');
45
+ const method = buf.readUInt16LE(off + 10);
46
+ const csize = buf.readUInt32LE(off + 20);
47
+ const nlen = buf.readUInt16LE(off + 28);
48
+ const xlen = buf.readUInt16LE(off + 30);
49
+ const clen = buf.readUInt16LE(off + 32);
50
+ const local = buf.readUInt32LE(off + 42);
51
+ const name = buf.toString('utf8', off + 46, off + 46 + nlen).replace(/\\/g, '/');
52
+ off += 46 + nlen + xlen + clen;
53
+ if (name.endsWith('/')) continue;
54
+ if (name.startsWith('/') || /^[A-Za-z]:/.test(name) || name.split('/').includes('..')) throw new Error('unsafe path in archive: ' + name);
55
+ if (buf.readUInt32LE(local) !== 0x04034b50) throw new Error('broken zip archive');
56
+ const start = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
57
+ const raw = buf.subarray(start, start + csize);
58
+ let data;
59
+ if (method === 0) data = Buffer.from(raw);
60
+ else if (method === 8) data = zlib.inflateRawSync(raw);
61
+ else throw new Error('unsupported zip compression: ' + method);
62
+ files.push({ name, data });
63
+ }
64
+ return files;
65
+ }
66
+
67
+ // ---------- JetBrains ----------
68
+
69
+ const JB_PRODUCTS = {
70
+ IntelliJIdea: 'IntelliJ IDEA',
71
+ IdeaIC: 'IntelliJ IDEA CE',
72
+ PhpStorm: 'PhpStorm',
73
+ WebStorm: 'WebStorm',
74
+ PyCharm: 'PyCharm',
75
+ PyCharmCE: 'PyCharm CE',
76
+ GoLand: 'GoLand',
77
+ RubyMine: 'RubyMine',
78
+ CLion: 'CLion',
79
+ Rider: 'Rider',
80
+ DataGrip: 'DataGrip',
81
+ DataSpell: 'DataSpell',
82
+ RustRover: 'RustRover',
83
+ Aqua: 'Aqua',
84
+ };
85
+ const JB_DIR = new RegExp(`^(${Object.keys(JB_PRODUCTS).join('|')})(\\d{4})\\.(\\d+)$`);
86
+
87
+ /** Где лежат настройки и плагины JetBrains IDE на этой ОС. */
88
+ function jbLayout(home = os.homedir(), platform = process.platform, env = process.env) {
89
+ if (platform === 'darwin') {
90
+ const base = path.join(home, 'Library', 'Application Support', 'JetBrains');
91
+ return { config: base, plugins: (dir) => path.join(base, dir, 'plugins') };
92
+ }
93
+ if (platform === 'win32') {
94
+ const base = path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'JetBrains');
95
+ return { config: base, plugins: (dir) => path.join(base, dir, 'plugins') };
96
+ }
97
+ const config = path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'JetBrains');
98
+ const data = path.join(env.XDG_DATA_HOME || path.join(home, '.local', 'share'), 'JetBrains');
99
+ return { config, plugins: (dir) => path.join(data, dir) };
100
+ }
101
+
102
+ function jbInstalledVersion(pluginsDir) {
103
+ try {
104
+ for (const f of fs.readdirSync(path.join(pluginsDir, JB_FOLDER, 'lib'))) {
105
+ const m = /^tokenwait-(\d+\.\d+\.\d+)\.jar$/.exec(f);
106
+ if (m) return m[1];
107
+ }
108
+ } catch (_) {}
109
+ return null;
110
+ }
111
+
112
+ /** Последняя версия каждой JetBrains IDE, которую запускали на этой машине. */
113
+ function detectJetbrains(opts = {}) {
114
+ const layout = jbLayout(opts.home, opts.platform, opts.env);
115
+ let dirs = [];
116
+ try {
117
+ dirs = fs.readdirSync(layout.config);
118
+ } catch (_) {
119
+ return [];
120
+ }
121
+ const latest = new Map();
122
+ for (const dir of dirs) {
123
+ const m = JB_DIR.exec(dir);
124
+ if (!m) continue;
125
+ const year = Number(m[2]);
126
+ const minor = Number(m[3]);
127
+ const prev = latest.get(m[1]);
128
+ if (!prev || year * 100 + minor > prev.year * 100 + prev.minor) latest.set(m[1], { dir, year, minor });
129
+ }
130
+ return [...latest.entries()].map(([product, v]) => {
131
+ const pluginsDir = layout.plugins(v.dir);
132
+ return {
133
+ kind: 'jetbrains',
134
+ id: product,
135
+ name: `${JB_PRODUCTS[product]} ${v.year}.${v.minor}`,
136
+ build: (v.year % 100) * 10 + v.minor, // 2024.3 -> 243 (номер ветки, как since-build)
137
+ pluginsDir,
138
+ installed: jbInstalledVersion(pluginsDir),
139
+ };
140
+ });
141
+ }
142
+
143
+ /** Распаковать плагин в папку плагинов IDE, заменив прежнюю версию. */
144
+ function installJetbrains(target, zipBuf) {
145
+ const files = unzip(zipBuf);
146
+ const top = new Set(files.map((f) => f.name.split('/')[0]));
147
+ if (top.size !== 1 || !top.has(JB_FOLDER)) throw new Error('unexpected plugin archive layout');
148
+ fs.mkdirSync(target.pluginsDir, { recursive: true });
149
+ const tmp = path.join(target.pluginsDir, '.tokenwait-new');
150
+ fs.rmSync(tmp, { recursive: true, force: true });
151
+ for (const f of files) {
152
+ const dst = path.join(tmp, f.name.slice(JB_FOLDER.length + 1));
153
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
154
+ fs.writeFileSync(dst, f.data);
155
+ }
156
+ const dst = path.join(target.pluginsDir, JB_FOLDER);
157
+ fs.rmSync(dst, { recursive: true, force: true });
158
+ fs.renameSync(tmp, dst);
159
+ }
160
+
161
+ // ---------- VS Code и его сборки ----------
162
+
163
+ const VSCODES = [
164
+ { id: 'vscode', name: 'VS Code', bin: 'code', mac: 'Visual Studio Code.app/Contents/Resources/app/bin/code', win: 'Programs/Microsoft VS Code/bin/code.cmd' },
165
+ { id: 'cursor', name: 'Cursor', bin: 'cursor', mac: 'Cursor.app/Contents/Resources/app/bin/cursor', win: 'Programs/cursor/resources/app/bin/cursor.cmd' },
166
+ { id: 'windsurf', name: 'Windsurf', bin: 'windsurf', mac: 'Windsurf.app/Contents/Resources/app/bin/windsurf', win: 'Programs/Windsurf/bin/windsurf.cmd' },
167
+ { id: 'vscodium', name: 'VSCodium', bin: 'codium', mac: 'VSCodium.app/Contents/Resources/app/bin/codium', win: 'Programs/VSCodium/bin/codium.cmd' },
168
+ ];
169
+
170
+ function which(bin, env = process.env, platform = process.platform) {
171
+ const exts = platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
172
+ for (const dir of String(env.PATH || '').split(path.delimiter)) {
173
+ if (!dir) continue;
174
+ for (const ext of exts) {
175
+ const f = path.join(dir, bin + ext);
176
+ try {
177
+ if (fs.statSync(f).isFile()) return f;
178
+ } catch (_) {}
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+
184
+ function runCli(cli, args, platform = process.platform) {
185
+ return execFileSync(cli, args, {
186
+ encoding: 'utf8',
187
+ timeout: 120000,
188
+ stdio: ['ignore', 'pipe', 'pipe'],
189
+ windowsHide: true,
190
+ shell: platform === 'win32' && /\.cmd$/i.test(cli),
191
+ });
192
+ }
193
+
194
+ function vscodeInstalledVersion(cli, platform) {
195
+ try {
196
+ const out = runCli(cli, ['--list-extensions', '--show-versions'], platform);
197
+ const line = out.split(/\r?\n/).find((l) => l.toLowerCase().startsWith(VSIX_ID + '@'));
198
+ return line ? line.split('@')[1].trim() : null;
199
+ } catch (_) {
200
+ return null;
201
+ }
202
+ }
203
+
204
+ /** VS Code и его сборки, у которых есть командная строка. */
205
+ function detectVscodes(opts = {}) {
206
+ const home = opts.home || os.homedir();
207
+ const platform = opts.platform || process.platform;
208
+ const env = opts.env || process.env;
209
+ const seen = new Set();
210
+ const out = [];
211
+ for (const v of VSCODES) {
212
+ let cli = which(v.bin, env, platform);
213
+ if (!cli && platform === 'darwin') {
214
+ for (const apps of ['/Applications', path.join(home, 'Applications')]) {
215
+ const f = path.join(apps, v.mac);
216
+ if (fs.existsSync(f)) {
217
+ cli = f;
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ if (!cli && platform === 'win32') {
223
+ const f = path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), v.win);
224
+ if (fs.existsSync(f)) cli = f;
225
+ }
226
+ if (!cli) continue;
227
+ let real = cli;
228
+ try {
229
+ real = fs.realpathSync(cli);
230
+ } catch (_) {}
231
+ if (seen.has(real)) continue;
232
+ seen.add(real);
233
+ out.push({ kind: 'vscode', id: v.id, name: v.name, cli, installed: vscodeInstalledVersion(cli, platform) });
234
+ }
235
+ return out;
236
+ }
237
+
238
+ function installVscode(target, vsixFile, platform = process.platform) {
239
+ runCli(target.cli, ['--install-extension', vsixFile, '--force'], platform);
240
+ }
241
+
242
+ // ---------- всё вместе ----------
243
+
244
+ function detectAll(opts = {}) {
245
+ return [...detectVscodes(opts), ...detectJetbrains(opts)];
246
+ }
247
+
248
+ async function download(url, timeoutMs = 60000) {
249
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), headers: { 'user-agent': 'tokenwait/' + require('../package.json').version } });
250
+ if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
251
+ return Buffer.from(await res.arrayBuffer());
252
+ }
253
+
254
+ /** Версии дополнений на сервере: {core, jetbrains, vscode, jetbrains_since_build}. */
255
+ async function latest(server) {
256
+ const buf = await download(server + '/downloads/versions.json', 10000);
257
+ const v = JSON.parse(buf.toString('utf8'));
258
+ return {
259
+ core: String(v.core || ''),
260
+ jetbrains: String(v.jetbrains || ''),
261
+ vscode: String(v.vscode || ''),
262
+ jetbrains_since_build: Number(v.jetbrains_since_build) || 0,
263
+ };
264
+ }
265
+
266
+ /** Что делать с этой IDE: 'install' | 'update' | 'current' | 'too-old'. */
267
+ function plan(target, versions) {
268
+ if (target.kind === 'jetbrains' && versions.jetbrains_since_build && target.build < versions.jetbrains_since_build) return 'too-old';
269
+ const want = target.kind === 'jetbrains' ? versions.jetbrains : versions.vscode;
270
+ if (!target.installed) return 'install';
271
+ return cmpVersion(target.installed, want) < 0 ? 'update' : 'current';
272
+ }
273
+
274
+ /**
275
+ * Поставить или обновить дополнение в выбранных IDE. Архивы скачиваются один раз.
276
+ * Возвращает [{target, action, ok, error?}].
277
+ */
278
+ async function apply(server, targets, versions, opts = {}) {
279
+ const results = [];
280
+ let vsix = null;
281
+ let jbZip = null;
282
+ for (const t of targets) {
283
+ const action = plan(t, versions);
284
+ if (action === 'current' || action === 'too-old' || (opts.onlyInstalled && action === 'install')) {
285
+ results.push({ target: t, action: action === 'install' ? 'skipped' : action, ok: true });
286
+ continue;
287
+ }
288
+ try {
289
+ if (t.kind === 'vscode') {
290
+ if (!vsix) {
291
+ const buf = await download(`${server}/downloads/tokenwait-vscode.vsix?v=${encodeURIComponent(versions.vscode)}`);
292
+ vsix = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'tokenwait-')), `tokenwait-${versions.vscode || 'latest'}.vsix`);
293
+ fs.writeFileSync(vsix, buf);
294
+ }
295
+ installVscode(t, vsix);
296
+ } else {
297
+ if (!jbZip) jbZip = await download(`${server}/downloads/tokenwait-jetbrains.zip?v=${encodeURIComponent(versions.jetbrains)}`);
298
+ installJetbrains(t, jbZip);
299
+ }
300
+ results.push({ target: t, action, ok: true });
301
+ } catch (e) {
302
+ results.push({ target: t, action, ok: false, error: String(e.message || e).split('\n')[0].slice(0, 200) });
303
+ }
304
+ }
305
+ if (vsix) fs.rmSync(path.dirname(vsix), { recursive: true, force: true });
306
+ return results;
307
+ }
308
+
309
+ module.exports = { cmpVersion, unzip, jbLayout, detectJetbrains, installJetbrains, detectVscodes, installVscode, detectAll, latest, plan, apply, VSIX_ID };
package/lib/worker.js CHANGED
@@ -54,8 +54,11 @@ async function report() {
54
54
  if (Date.now() - (pend.last_report || 0) < (ads.report_seconds || 60) * 1000) return null;
55
55
  const { counts, rest } = toImpressions(pend.ads, impSec);
56
56
  if (!Object.keys(counts).length) return null;
57
- const waitMs = pend.wait_ms || 0;
58
- writeJson(p('pending.json'), { ads: rest, wait_ms: 0, last_report: Date.now() });
57
+ // Остаток неполных показов переносим вместе с его временем ожидания: иначе в следующем отчёте
58
+ // показ, собранный из остатка, сервер отклонит (показов не больше, чем время ожидания).
59
+ const restMs = Object.values(rest).reduce((x, y) => x + y, 0);
60
+ const waitMs = Math.max(0, (pend.wait_ms || 0) - restMs);
61
+ writeJson(p('pending.json'), { ads: rest, wait_ms: Math.min(restMs, pend.wait_ms || 0), last_report: Date.now() });
59
62
  return { counts, waitMs };
60
63
  });
61
64
  if (!taken) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenwait",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Get paid while your AI coding agent thinks. A clearly labelled sponsored line for Claude Code, Codex CLI and Gemini CLI.",
5
5
  "bin": {
6
6
  "tokenwait": "bin/tokenwait.js"