@yegor256/jinka 0.0.8 → 0.0.9

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/src/permanent.js CHANGED
@@ -1,24 +1,12 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- export class Permanent {
5
- constructor(origin, pause) {
6
- this.origin = origin;
7
- this.pause = pause;
8
- }
9
- name() {
10
- return this.origin.name();
11
- }
12
- async text() {
13
- return this.origin.text();
14
- }
15
- fate() {
16
- return 'which stays there after you exit, so you must not delete it';
17
- }
18
- async close() {
19
- setTimeout(() => this.origin.free(), this.pause).unref();
20
- }
21
- free() {
22
- this.origin.free();
23
- }
24
- }
4
+ export const permanent = (origin, pause) => Object.freeze({
5
+ name: () => origin.name(),
6
+ text: async () => origin.text(),
7
+ fate: () => 'which stays there after you exit, so you must not delete it',
8
+ close: async () => {
9
+ setTimeout(() => origin.free(), pause).unref();
10
+ },
11
+ free: () => origin.free()
12
+ });
package/src/protocol.js CHANGED
@@ -1,14 +1,7 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- export class Protocol {
5
- constructor(lines) {
6
- this.lines = lines;
7
- }
8
- info(text) {
9
- this.lines.push(['info', text]);
10
- }
11
- warn(text) {
12
- this.lines.push(['warn', text]);
13
- }
14
- }
4
+ export const protocol = (lines) => Object.freeze({
5
+ info: (text) => lines.push(['info', text]),
6
+ warn: (text) => lines.push(['warn', text])
7
+ });
package/src/quota.js CHANGED
@@ -1,37 +1,27 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- export class Quota {
5
- constructor() {
6
- this.windows = [];
7
- }
8
- add(line) {
9
- try {
10
- const json = JSON.parse(line);
11
- if (json.type === 'rate_limit_event' && typeof json.rate_limit_info?.unifiedWindows === 'object') {
12
- this.windows.push(Object.entries(json.rate_limit_info.unifiedWindows).filter(([, w]) => typeof w?.utilization === 'number'));
4
+ export const quota = () => {
5
+ const windows = [];
6
+ const live = (now) => (windows.at(-1) ?? [])
7
+ .filter(([, w]) => typeof w.resetsAt !== 'number' || w.resetsAt * 1000 > now)
8
+ .map(([n, w]) => [n.replaceAll('_', '-'), (1 - w.utilization) * 100]);
9
+ const tightest = (now) => live(now).reduce((a, b) => (b[1] < a[1] ? b : a), ['', 100]);
10
+ return Object.freeze({
11
+ add: (line) => {
12
+ try {
13
+ const json = JSON.parse(line);
14
+ if (json.type === 'rate_limit_event' && typeof json.rate_limit_info?.unifiedWindows === 'object') {
15
+ windows.push(Object.entries(json.rate_limit_info.unifiedWindows).filter(([, w]) => typeof w?.utilization === 'number'));
16
+ }
17
+ } catch {
18
+ return;
13
19
  }
14
- } catch {
15
- return;
16
- }
17
- }
18
- text(now) {
19
- return this.#live(now)
20
+ },
21
+ text: (now) => live(now)
20
22
  .map(([n, share]) => `${Math.round(share)}%-${{ 'five-hour': '5H', 'seven-day': 'W', 'thirty-day': 'M', 'seven-day-overage-included': 'W+' }[n] ?? n}`)
21
- .join(',') || '-';
22
- }
23
- left(now) {
24
- return this.#tightest(now)[1];
25
- }
26
- name(now) {
27
- return this.#tightest(now)[0];
28
- }
29
- #tightest(now) {
30
- return this.#live(now).reduce((a, b) => (b[1] < a[1] ? b : a), ['', 100]);
31
- }
32
- #live(now) {
33
- return (this.windows.at(-1) ?? [])
34
- .filter(([, w]) => typeof w.resetsAt !== 'number' || w.resetsAt * 1000 > now)
35
- .map(([n, w]) => [n.replaceAll('_', '-'), (1 - w.utilization) * 100]);
36
- }
37
- }
23
+ .join(',') || '-',
24
+ left: (now) => tightest(now)[1],
25
+ name: (now) => tightest(now)[0]
26
+ });
27
+ };
package/src/release.js ADDED
@@ -0,0 +1,30 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { styleText } from 'node:util';
5
+
6
+ export const release = (installed, url) => {
7
+ const latest = [];
8
+ const higher = (left, right) => {
9
+ const [a, b] = [left, right].map((v) => v.split('-')[0].split('.').map(Number));
10
+ const index = a.findIndex((n, i) => n !== (b[i] ?? 0));
11
+ return index >= 0 && a[index] > (b[index] ?? 0);
12
+ };
13
+ return Object.freeze({
14
+ check: async () => {
15
+ try {
16
+ const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
17
+ if (res.ok) {
18
+ latest.push(String((await res.json()).version));
19
+ }
20
+ } catch {
21
+ return;
22
+ }
23
+ },
24
+ text: () => (
25
+ latest.some((v) => higher(v, installed))
26
+ ? `${installed} ${styleText('red', '(old)', { validateStream: false })}`
27
+ : installed
28
+ )
29
+ });
30
+ };
package/src/rollouts.js CHANGED
@@ -4,50 +4,48 @@
4
4
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
 
7
- export class Rollouts {
8
- constructor(origin, dir) {
9
- this.origin = origin;
10
- this.dir = dir;
11
- this.files = new Map();
12
- this.thread = '';
13
- }
14
- add(line) {
15
- this.origin.add(line);
16
- try {
17
- const json = JSON.parse(line);
18
- if (json.type === 'thread.started' && typeof json.thread_id === 'string') {
19
- this.thread = json.thread_id;
20
- }
21
- if ((json.type === 'turn.completed' || json.type === 'item.completed') && this.thread !== '') {
22
- this.#limits().forEach((limits) => this.origin.add(JSON.stringify({
23
- type: 'rate_limit_event',
24
- rate_limit_info: {
25
- unifiedWindows: Object.fromEntries(
26
- [limits.primary, limits.secondary]
27
- .filter((w) => typeof w?.used_percent === 'number' && typeof w?.window_minutes === 'number')
28
- .map((w) => [{ 300: 'five_hour', 10080: 'seven_day', 43200: 'thirty_day' }[w.window_minutes] ?? `${w.window_minutes}_minute`, { utilization: w.used_percent / 100, resetsAt: w.resets_at }])
29
- )
30
- }
31
- })));
32
- }
33
- } catch {
34
- return;
35
- }
36
- }
37
- #limits() {
38
- if (!this.files.has(this.thread) && existsSync(this.dir)) {
39
- readdirSync(this.dir, { recursive: true })
7
+ export const rollouts = (origin, dir) => {
8
+ const files = new Map();
9
+ let thread = '';
10
+ const limits = () => {
11
+ if (!files.has(thread) && existsSync(dir)) {
12
+ readdirSync(dir, { recursive: true })
40
13
  .map(String)
41
- .filter((path) => path.endsWith(`-${this.thread}.jsonl`))
42
- .forEach((path) => this.files.set(this.thread, path));
14
+ .filter((path) => path.endsWith(`-${thread}.jsonl`))
15
+ .forEach((path) => files.set(thread, path));
43
16
  }
44
- return !this.files.has(this.thread)
17
+ return !files.has(thread)
45
18
  ? []
46
- : readFileSync(join(this.dir, this.files.get(this.thread)), 'utf8')
19
+ : readFileSync(join(dir, files.get(thread)), 'utf8')
47
20
  .split('\n')
48
21
  .filter((text) => text.includes('"rate_limits"'))
49
22
  .map((text) => JSON.parse(text).payload?.rate_limits)
50
- .filter((limits) => typeof limits === 'object' && limits !== null)
23
+ .filter((l) => typeof l === 'object' && l !== null)
51
24
  .slice(-1);
52
- }
53
- }
25
+ };
26
+ return Object.freeze({
27
+ add: (line) => {
28
+ origin.add(line);
29
+ try {
30
+ const json = JSON.parse(line);
31
+ if (json.type === 'thread.started' && typeof json.thread_id === 'string') {
32
+ thread = json.thread_id;
33
+ }
34
+ if ((json.type === 'turn.completed' || json.type === 'item.completed') && thread !== '') {
35
+ limits().forEach((l) => origin.add(JSON.stringify({
36
+ type: 'rate_limit_event',
37
+ rate_limit_info: {
38
+ unifiedWindows: Object.fromEntries(
39
+ [l.primary, l.secondary]
40
+ .filter((w) => typeof w?.used_percent === 'number' && typeof w?.window_minutes === 'number')
41
+ .map((w) => [{ 300: 'five_hour', 10080: 'seven_day', 43200: 'thirty_day' }[w.window_minutes] ?? `${w.window_minutes}_minute`, { utilization: w.used_percent / 100, resetsAt: w.resets_at }])
42
+ )
43
+ }
44
+ })));
45
+ }
46
+ } catch {
47
+ return;
48
+ }
49
+ }
50
+ });
51
+ };
package/src/row.js CHANGED
@@ -1,12 +1,6 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- export class Row {
5
- constructor(lines, index) {
6
- this.lines = lines;
7
- this.index = index;
8
- }
9
- show(text) {
10
- this.lines.push([this.index, text]);
11
- }
12
- }
4
+ export const row = (lines, index) => Object.freeze({
5
+ show: (text) => lines.push([index, text])
6
+ });
package/src/semaphore.js CHANGED
@@ -5,17 +5,12 @@ import { rmSync } from 'node:fs';
5
5
  import { mkdir, writeFile } from 'node:fs/promises';
6
6
  import { dirname } from 'node:path';
7
7
 
8
- export class Semaphore {
9
- constructor(file) {
10
- this.file = file;
11
- }
12
- async acquire() {
13
- await mkdir(dirname(this.file), { recursive: true });
14
- await writeFile(this.file, String(process.pid), { flag: 'wx' }).catch((e) => {
15
- throw e.code === 'EEXIST' ? new Error(`Another jinka is running, because the file ${this.file} already exists`) : e;
8
+ export const semaphore = (file) => Object.freeze({
9
+ acquire: async () => {
10
+ await mkdir(dirname(file), { recursive: true });
11
+ await writeFile(file, String(process.pid), { flag: 'wx' }).catch((e) => {
12
+ throw e.code === 'EEXIST' ? new Error(`Another jinka is running, because the file ${file} already exists`) : e;
16
13
  });
17
- }
18
- release() {
19
- rmSync(this.file, { force: true });
20
- }
21
- }
14
+ },
15
+ release: () => rmSync(file, { force: true })
16
+ });
package/src/status.js CHANGED
@@ -2,26 +2,18 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import { styleText } from 'node:util';
5
- import { Duration } from './duration.js';
5
+ import { duration } from './duration.js';
6
6
 
7
- export class Status {
8
- constructor(start, jobs, agent, model, tokens, quota) {
9
- this.start = start;
10
- this.jobs = jobs;
11
- this.agent = agent;
12
- this.model = model;
13
- this.tokens = tokens;
14
- this.quota = quota;
15
- }
16
- text() {
7
+ export const status = (start, jobs, agent, model, tokens, quota) => Object.freeze({
8
+ text: () => {
17
9
  const now = Date.now();
18
10
  return [
19
- `up:${styleText('bgGray', new Duration(now - this.start).hours(), { validateStream: false })}`,
20
- `done:${styleText('bgGray', this.jobs.text(), { validateStream: false })}`,
21
- `agent:${styleText('bgGray', this.agent, { validateStream: false })}`,
22
- `model:${styleText('bgGray', this.model === '' ? 'default' : this.model, { validateStream: false })}`,
23
- `tokens:${styleText('bgGray', this.tokens.text(), { validateStream: false })}`,
24
- `quota:${styleText('bgGray', this.quota.text(now), { validateStream: false })}`
11
+ `up:${styleText('bgGray', duration(now - start).hours(), { validateStream: false })}`,
12
+ `done:${styleText('bgGray', jobs.text(), { validateStream: false })}`,
13
+ `agent:${styleText('bgGray', agent, { validateStream: false })}`,
14
+ `model:${styleText('bgGray', model === '' ? 'default' : model, { validateStream: false })}`,
15
+ `tokens:${styleText('bgGray', tokens.text(), { validateStream: false })}`,
16
+ `quota:${styleText('bgGray', quota.text(now), { validateStream: false })}`
25
17
  ].join(' ');
26
18
  }
27
- }
19
+ });
package/src/summarized.js CHANGED
@@ -1,30 +1,19 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- export class Summarized {
5
- constructor(screen, summary, stream) {
6
- this.screen = screen;
7
- this.summary = summary;
8
- this.stream = stream;
9
- this.done = [];
10
- }
11
- row(index) {
12
- return this.screen.row(index);
13
- }
14
- notice() {
15
- return this.screen.notice();
16
- }
17
- up() {
18
- this.screen.up();
19
- }
20
- down() {
21
- this.screen.down();
22
- }
23
- close() {
24
- this.screen.close();
25
- if (this.done.length === 0) {
26
- this.done.push(true);
27
- this.stream.write(this.summary.text());
4
+ export const summarized = (screen, summary, stream) => {
5
+ const done = [];
6
+ return Object.freeze({
7
+ row: (index) => screen.row(index),
8
+ notice: () => screen.notice(),
9
+ up: () => screen.up(),
10
+ down: () => screen.down(),
11
+ close: () => {
12
+ screen.close();
13
+ if (done.length === 0) {
14
+ done.push(true);
15
+ stream.write(summary.text());
16
+ }
28
17
  }
29
- }
30
- }
18
+ });
19
+ };
package/src/summary.js CHANGED
@@ -4,23 +4,15 @@
4
4
  import { existsSync, readdirSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { sep } from 'node:path';
7
- import { Duration } from './duration.js';
7
+ import { duration } from './duration.js';
8
8
 
9
- export class Summary {
10
- constructor(start, version, jobs, setup) {
11
- this.start = start;
12
- this.version = version;
13
- this.jobs = jobs;
14
- this.setup = setup;
15
- }
16
- text() {
17
- return [
18
- '---',
19
- `jinka ${this.version} finished in ${new Duration(Date.now() - this.start).hours()}`,
20
- `agent: ${this.setup.agent}, model: ${this.setup.model === '' ? 'default' : this.setup.model}, effort: ${this.setup.effort === '' ? 'default' : this.setup.effort}`,
21
- `sessions finished: ${this.jobs.text()}`,
22
- `wishes remained in ${this.setup.wishes.startsWith(`${homedir()}${sep}`) ? `~${this.setup.wishes.slice(homedir().length)}` : this.setup.wishes}: ${existsSync(this.setup.wishes) ? readdirSync(this.setup.wishes, { recursive: true, withFileTypes: true }).filter((f) => f.isFile() && f.name.endsWith('.md')).length : 0}`,
23
- ''
24
- ].join('\n');
25
- }
26
- }
9
+ export const summary = (start, version, jobs, setup) => Object.freeze({
10
+ text: () => [
11
+ '---',
12
+ `jinka ${version} finished in ${duration(Date.now() - start).hours()}`,
13
+ `agent: ${setup.agent}, model: ${setup.model === '' ? 'default' : setup.model}, effort: ${setup.effort === '' ? 'default' : setup.effort}`,
14
+ `wishes came true: ${jobs.text()}`,
15
+ `wishes remained in ${setup.wishes.startsWith(`${homedir()}${sep}`) ? `~${setup.wishes.slice(homedir().length)}` : setup.wishes}: ${existsSync(setup.wishes) ? readdirSync(setup.wishes, { recursive: true, withFileTypes: true }).filter((f) => f.isFile() && f.name.endsWith('.md')).length : 0}`,
16
+ ''
17
+ ].join('\n')
18
+ });
package/src/throttled.js CHANGED
@@ -2,33 +2,28 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import { setTimeout } from 'node:timers/promises';
5
- import { Duration } from './duration.js';
5
+ import { duration } from './duration.js';
6
6
 
7
- export class Throttled {
8
- constructor(origin, quota, throttle, interval, notice) {
9
- this.origin = origin;
10
- this.quota = quota;
11
- this.throttle = throttle;
12
- this.interval = interval;
13
- this.notice = notice;
14
- }
15
- async next(signal) {
16
- await this.#wait(signal, true);
17
- return this.origin.next(signal);
18
- }
19
- async #wait(signal, first) {
7
+ export const throttled = (origin, quota, throttle, interval, notice) => {
8
+ const wait = async (signal, first) => {
20
9
  signal.throwIfAborted();
21
- const left = this.quota.left(Date.now());
22
- if (left >= this.throttle && first) {
10
+ const left = quota.left(Date.now());
11
+ if (left >= throttle && first) {
23
12
  return;
24
13
  }
25
- const share = `${Math.round(left)}% of the ${this.quota.name(Date.now())} quota is left`;
26
- if (left >= this.throttle) {
27
- this.notice.warn(`now ${share}, not below ${this.throttle}%, new jobs resume`);
14
+ const share = `${Math.round(left)}% of the ${quota.name(Date.now())} quota is left`;
15
+ if (left >= throttle) {
16
+ notice.warn(`now ${share}, not below ${throttle}%, new jobs resume`);
28
17
  return;
29
18
  }
30
- this.notice.warn(`${first ? '' : 'still '}only ${share}, below ${this.throttle}%, new jobs wait, next check in ${new Duration(this.interval).phrase()}`);
31
- await setTimeout(this.interval, undefined, { signal });
32
- await this.#wait(signal, false);
33
- }
34
- }
19
+ notice.warn(`${first ? '' : 'still '}only ${share}, below ${throttle}%, new jobs wait, next check in ${duration(interval).phrase()}`);
20
+ await setTimeout(interval, undefined, { signal });
21
+ await wait(signal, false);
22
+ };
23
+ return Object.freeze({
24
+ next: async (signal) => {
25
+ await wait(signal, true);
26
+ return origin.next(signal);
27
+ }
28
+ });
29
+ };
package/src/tokens.js CHANGED
@@ -1,29 +1,27 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- export class Tokens {
5
- constructor() {
6
- this.messages = new Map();
7
- }
8
- add(line) {
9
- try {
10
- const json = JSON.parse(line);
11
- if (json.type === 'assistant' && json.message?.id !== undefined && json.message?.usage !== undefined) {
12
- this.messages.set(
13
- json.message.id,
14
- ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens']
15
- .reduce((sum, key) => sum + (json.message.usage[key] ?? 0), 0)
16
- );
4
+ export const tokens = () => {
5
+ const messages = new Map();
6
+ return Object.freeze({
7
+ add: (line) => {
8
+ try {
9
+ const json = JSON.parse(line);
10
+ if (json.type === 'assistant' && json.message?.id !== undefined && json.message?.usage !== undefined) {
11
+ messages.set(
12
+ json.message.id,
13
+ ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens']
14
+ .reduce((sum, key) => sum + (json.message.usage[key] ?? 0), 0)
15
+ );
16
+ }
17
+ if (json.type === 'turn.completed' && json.usage !== undefined) {
18
+ messages.set(Symbol('turn'), (json.usage.input_tokens ?? 0) + (json.usage.output_tokens ?? 0));
19
+ }
20
+ } catch {
21
+ return;
17
22
  }
18
- if (json.type === 'turn.completed' && json.usage !== undefined) {
19
- this.messages.set(Symbol('turn'), (json.usage.input_tokens ?? 0) + (json.usage.output_tokens ?? 0));
20
- }
21
- } catch {
22
- return;
23
- }
24
- }
25
- text() {
26
- return new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 })
27
- .format([...this.messages.values()].reduce((sum, count) => sum + count, 0));
28
- }
29
- }
23
+ },
24
+ text: () => new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 })
25
+ .format([...messages.values()].reduce((sum, count) => sum + count, 0))
26
+ });
27
+ };