@yegor256/jinka 0.0.4 → 0.0.6

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/status.js CHANGED
@@ -5,22 +5,23 @@ import { styleText } from 'node:util';
5
5
  import { Duration } from './duration.js';
6
6
 
7
7
  export class Status {
8
- constructor(version, start, agent, model, tokens, quota) {
9
- this.version = version;
8
+ constructor(start, jobs, agent, model, tokens, quota) {
10
9
  this.start = start;
10
+ this.jobs = jobs;
11
11
  this.agent = agent;
12
12
  this.model = model;
13
13
  this.tokens = tokens;
14
14
  this.quota = quota;
15
15
  }
16
16
  text() {
17
+ const now = Date.now();
17
18
  return [
18
- `jinka ${styleText('bgGray', this.version, { validateStream: false })}`,
19
- `up ${styleText('bgGray', new Duration(Date.now() - this.start).hours(), { validateStream: false })}`,
20
- `agent ${styleText('bgGray', this.agent, { validateStream: false })}`,
21
- `model ${styleText('bgGray', this.model === '' ? 'default' : this.model, { validateStream: false })}`,
22
- `tokens ${styleText('bgGray', this.tokens.text(), { validateStream: false })}`,
23
- `5h-quota [${styleText('bgGray', this.quota.text(), { validateStream: false })}]`
24
- ].join(', ');
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 })}`
25
+ ].join(' ');
25
26
  }
26
27
  }
@@ -0,0 +1,30 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
+ // SPDX-License-Identifier: MIT
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());
28
+ }
29
+ }
30
+ }
package/src/summary.js ADDED
@@ -0,0 +1,26 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { existsSync, readdirSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { sep } from 'node:path';
7
+ import { Duration } from './duration.js';
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
+ }
@@ -0,0 +1,34 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { setTimeout } from 'node:timers/promises';
5
+ import { Duration } from './duration.js';
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) {
20
+ signal.throwIfAborted();
21
+ const left = this.quota.left(Date.now());
22
+ if (left >= this.throttle && first) {
23
+ return;
24
+ }
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`);
28
+ return;
29
+ }
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
+ }
package/src/tokens.js CHANGED
@@ -15,6 +15,9 @@ export class Tokens {
15
15
  .reduce((sum, key) => sum + (json.message.usage[key] ?? 0), 0)
16
16
  );
17
17
  }
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
+ }
18
21
  } catch {
19
22
  return;
20
23
  }
package/src/tty.js CHANGED
@@ -1,10 +1,12 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- import { stripVTControlCharacters } from 'node:util';
4
+ import { stripVTControlCharacters, styleText } from 'node:util';
5
5
  import sliceAnsi from 'slice-ansi';
6
6
  import { Row } from './row.js';
7
+ import { Protocol } from './protocol.js';
7
8
  import { Duration } from './duration.js';
9
+ import { Logo } from './logo.js';
8
10
 
9
11
  export class Tty {
10
12
  constructor(stream, crew, status, legend) {
@@ -15,17 +17,26 @@ export class Tty {
15
17
  this.lines = new Map();
16
18
  this.notes = [];
17
19
  this.clock = [];
20
+ this.view = { depth: 0 };
18
21
  }
19
22
  row(index) {
20
23
  return new Row({ push: ([i, text]) => this.#update(i, text) }, index);
21
24
  }
22
25
  notice() {
23
- return new Row({ push: ([, text]) => this.#note(text) }, 0);
26
+ return new Protocol({ push: ([level, text]) => this.#note(level === 'warn' ? `\x1b[38;2;255;135;0m${text}\x1b[39m` : text) });
27
+ }
28
+ up() {
29
+ this.view.depth += 1;
30
+ this.#redraw();
31
+ }
32
+ down() {
33
+ this.view.depth = Math.max(0, this.view.depth - 1);
34
+ this.#redraw();
24
35
  }
25
36
  close() {
26
37
  this.clock.splice(0).forEach((c) => {
27
38
  clearInterval(c);
28
- this.stream.write('\x1b[?25h\x1b[?1049l');
39
+ this.stream.write('\x1b[?1007l\x1b[?25h\x1b[?1049l');
29
40
  this.stream.write(this.notes.map((n) => `${n}\n`).join(''));
30
41
  });
31
42
  }
@@ -37,43 +48,54 @@ export class Tty {
37
48
  }
38
49
  #note(text) {
39
50
  this.notes.push(
40
- `${new Date().toTimeString().slice(0, 8)} ${text.replace(/https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/(?:issues|pull)\/(\d+)/g, (url, repo, number) => `\x1b]8;;${url}\x07\x1b[34m${repo}#${number}\x1b[39m\x1b]8;;\x07`)}`
51
+ `${new Date().toTimeString().slice(0, 8)} ${text.replace(/https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/(?:issues|pull)\/(\d+)/g, (url, repo, number) => `\x1b]8;;${url}\x07\x1b[38;2;106;113;247m${repo}#${number}\x1b[39m\x1b]8;;\x07`)}`
41
52
  );
53
+ if (this.view.depth > 0) {
54
+ this.view.depth += this.#wrap(this.notes.at(-1), this.stream.columns || 80).length;
55
+ }
42
56
  this.#paint();
43
57
  }
44
58
  #paint() {
45
59
  if (this.clock.length === 0) {
46
- this.stream.write('\x1b[?1049h\x1b[?25l');
60
+ this.stream.write('\x1b[?1049h\x1b[?25l\x1b[?1007h');
47
61
  this.clock.push(setInterval(() => this.#draw(), 1000).unref());
48
- this.stream.on('resize', () => {
49
- if (this.clock.length > 0) {
50
- this.#draw();
51
- }
52
- });
62
+ this.stream.on('resize', () => this.#redraw());
53
63
  }
54
64
  this.#draw();
55
65
  }
66
+ #redraw() {
67
+ if (this.clock.length > 0) {
68
+ this.#draw();
69
+ }
70
+ }
56
71
  #draw() {
57
72
  const rows = this.stream.rows || 24;
58
73
  const lines = this.crew.indexes().map((i) => [i, this.lines.get(i) ?? { head: '', tail: '', start: Date.now() }]);
59
74
  const width = Math.max(...lines.map(([, l]) => stripVTControlCharacters(l.head).length));
60
75
  const columns = this.stream.columns || 80;
61
76
  const notes = this.notes.flatMap((n) => this.#wrap(n, columns));
77
+ const height = Math.max(0, rows - lines.length - 5);
78
+ this.view.depth = Math.min(this.view.depth, Math.max(0, notes.length - height));
79
+ const [upper, lower] = new Logo().lines();
62
80
  const top = [
63
- this.status.text(),
64
- '',
65
- ...lines.map(([, l]) => `${l.head}${' '.repeat(width - stripVTControlCharacters(l.head).length)} ${new Duration(Date.now() - l.start).minutes()} ${l.tail}`),
81
+ `${upper} ${this.status.text()}`,
82
+ lower,
83
+ ...lines.map(([, l]) => `${l.head}${' '.repeat(width - stripVTControlCharacters(l.head).length)} ${styleText(['', 'idle'].includes(stripVTControlCharacters(l.head)) ? 'gray' : 'green', new Duration(Date.now() - l.start).clock(), { validateStream: false })} ${l.tail}`),
66
84
  '-'.repeat(10),
67
- ...notes.slice(Math.max(0, notes.length - Math.max(0, rows - lines.length - 5)))
85
+ ...notes.slice(Math.max(0, notes.length - height - this.view.depth), notes.length - this.view.depth)
68
86
  ];
69
87
  this.stream.write(
70
88
  `\x1b[H${
71
- [...top, ...Array(Math.max(1, rows - top.length - 1)).fill(''), this.legend.text()]
89
+ [...top, ...Array(Math.max(1, rows - top.length - 1)).fill(''), this.#edge(this.legend.text(), columns)]
72
90
  .map((l) => `\x1b[2K${sliceAnsi(l, 0, columns)}\x1b[0m`)
73
91
  .join('\n')
74
92
  }\x1b[J`
75
93
  );
76
94
  }
95
+ #edge(text, width) {
96
+ const [left, right] = text.split('\t');
97
+ return `${left}${' '.repeat(Math.max(1, width - 1 - stripVTControlCharacters(left).length - stripVTControlCharacters(right).length))}${right}`;
98
+ }
77
99
  #wrap(text, width) {
78
100
  return stripVTControlCharacters(text).length <= width || width < 3 ? [text] : [sliceAnsi(text, 0, width), ...this.#wrap(` ${sliceAnsi(text, width)}`, width)];
79
101
  }
package/src/wand.js CHANGED
@@ -31,10 +31,10 @@ export class Wand {
31
31
  const suffix = [`(${[wish.name(), new Duration(Date.now() - start).minutes(), tokens, model].filter((p) => p !== '').join(' ')})`, link].filter((p) => p !== '').join(' ');
32
32
  if (code === 0) {
33
33
  await wish.close();
34
- this.protocol.show(`${summary === '' ? 'done' : summary} ${suffix}`);
34
+ this.protocol.info(`${summary === '' ? 'done' : summary} ${suffix}`);
35
35
  } else {
36
36
  wish.free();
37
- this.protocol.show(`failed with exit code ${code}${summary === '' ? '' : `: ${summary}`} ${suffix}`);
37
+ this.protocol.info(`failed with exit code ${code}${summary === '' ? '' : `: ${summary}`} ${suffix}`);
38
38
  }
39
39
  }
40
40
  }
package/src/wheel.js ADDED
@@ -0,0 +1,20 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ export class Wheel {
5
+ constructor(stream, screen) {
6
+ this.stream = stream;
7
+ this.screen = screen;
8
+ }
9
+ listen() {
10
+ this.stream.on('data', (chunk) =>
11
+ chunk
12
+ .toString()
13
+ .split('\x1b')
14
+ .slice(1)
15
+ .map((s) => s.slice(0, 2))
16
+ .filter((s) => /^[[O][AB]$/.test(s))
17
+ .forEach((s) => (s.endsWith('A') ? this.screen.up() : this.screen.down()))
18
+ );
19
+ }
20
+ }
package/src/wishes.js CHANGED
@@ -9,11 +9,12 @@ import { Archived } from './archived.js';
9
9
  import { Permanent } from './permanent.js';
10
10
 
11
11
  export class Wishes {
12
- constructor(dir, interval, archive = '', pause = 0) {
12
+ constructor(dir, interval, archive = '', pause = 0, notice = { warn: () => {} }) {
13
13
  this.dir = dir;
14
14
  this.interval = interval;
15
15
  this.archive = archive;
16
16
  this.pause = pause;
17
+ this.notice = notice;
17
18
  this.claims = new Map();
18
19
  this.startup = [];
19
20
  }
@@ -26,6 +27,10 @@ export class Wishes {
26
27
  signal.throwIfAborted();
27
28
  if (this.startup.length === 0) {
28
29
  this.startup.push(new Set(files));
30
+ const count = files.filter((f) => !/^[A-Z]/.test(basename(f))).length;
31
+ if (count > 0) {
32
+ this.notice.warn(`found ${count} pre-existing ${count === 1 ? 'wish' : 'wishes'} in ${this.dir} that ${count === 1 ? 'is' : 'are'} not permanent`);
33
+ }
29
34
  }
30
35
  const path = files.find((f) => !this.claims.has(f))
31
36
  ?? [...this.claims.keys()].find((c) => !this.claims.get(c) && files.includes(c));