@yegor256/jinka 0.0.8 → 0.0.10

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/tty.js CHANGED
@@ -3,100 +3,101 @@
3
3
 
4
4
  import { stripVTControlCharacters, styleText } from 'node:util';
5
5
  import sliceAnsi from 'slice-ansi';
6
- import { Row } from './row.js';
7
- import { Protocol } from './protocol.js';
8
- import { Duration } from './duration.js';
9
- import { Logo } from './logo.js';
6
+ import { row } from './row.js';
7
+ import { protocol } from './protocol.js';
8
+ import { duration } from './duration.js';
9
+ import { logo } from './logo.js';
10
10
 
11
- export class Tty {
12
- constructor(stream, crew, status, legend) {
13
- this.stream = stream;
14
- this.crew = crew;
15
- this.status = status;
16
- this.legend = legend;
17
- this.lines = new Map();
18
- this.notes = [];
19
- this.clock = [];
20
- this.view = { depth: 0 };
21
- }
22
- row(index) {
23
- return new Row({ push: ([i, text]) => this.#update(i, text) }, index);
24
- }
25
- notice() {
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();
35
- }
36
- close() {
37
- this.clock.splice(0).forEach((c) => {
38
- clearInterval(c);
39
- this.stream.write('\x1b[?1007l\x1b[?25h\x1b[?1049l');
40
- this.stream.write(this.notes.map((n) => `${n}\n`).join(''));
41
- });
42
- }
43
- #update(index, text) {
44
- const [head, tail = ''] = text.split('\t');
45
- const line = this.lines.get(index) ?? { head: '', start: Date.now() };
46
- this.lines.set(index, { head, tail, start: head === line.head ? line.start : Date.now() });
47
- this.#paint();
48
- }
49
- #note(text) {
50
- this.notes.push(
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`)}`
52
- );
53
- if (this.view.depth > 0) {
54
- this.view.depth += this.#wrap(this.notes.at(-1), this.stream.columns || 80).length;
55
- }
56
- this.#paint();
57
- }
58
- #paint() {
59
- if (this.clock.length === 0) {
60
- this.stream.write('\x1b[?1049h\x1b[?25l\x1b[?1007h');
61
- this.clock.push(setInterval(() => this.#draw(), 1000).unref());
62
- this.stream.on('resize', () => this.#redraw());
63
- }
64
- this.#draw();
65
- }
66
- #redraw() {
67
- if (this.clock.length > 0) {
68
- this.#draw();
11
+ export const tty = (stream, crew, status, legend) => {
12
+ const lines = new Map();
13
+ const notes = [];
14
+ const clock = [];
15
+ const view = { depth: 0 };
16
+ const edge = (text, width) => {
17
+ const [left, right] = text.split('\t');
18
+ return `${left}${' '.repeat(Math.max(1, width - 1 - stripVTControlCharacters(left).length - stripVTControlCharacters(right).length))}${right}`;
19
+ };
20
+ const cut = (text, width) => {
21
+ const plain = stripVTControlCharacters(text);
22
+ const space = plain.lastIndexOf(' ', width);
23
+ return space > 2 && plain.slice(space + 1).split(' ')[0].length <= width - 2 ? [space, space + 1] : [width, width];
24
+ };
25
+ const wrap = (text, width) => {
26
+ if (stripVTControlCharacters(text).length <= width || width < 3) {
27
+ return [text];
69
28
  }
70
- }
71
- #draw() {
72
- const rows = this.stream.rows || 24;
73
- const lines = this.crew.indexes().map((i) => [i, this.lines.get(i) ?? { head: '', tail: '', start: Date.now() }]);
74
- const width = Math.max(...lines.map(([, l]) => stripVTControlCharacters(l.head).length));
75
- const columns = this.stream.columns || 80;
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();
29
+ const [end, start] = cut(text, width);
30
+ return [sliceAnsi(text, 0, end), ...wrap(` ${sliceAnsi(text, start)}`, width)];
31
+ };
32
+ const draw = () => {
33
+ const rows = stream.rows || 24;
34
+ const wands = crew.indexes().map((i) => [i, lines.get(i) ?? { head: '', tail: '', start: Date.now() }]);
35
+ const width = Math.max(...wands.map(([, l]) => stripVTControlCharacters(l.head).length));
36
+ const columns = stream.columns || 80;
37
+ const wrapped = notes.flatMap((n) => wrap(n, columns));
38
+ const height = Math.max(0, rows - wands.length - 5);
39
+ view.depth = Math.min(view.depth, Math.max(0, wrapped.length - height));
40
+ const [upper, lower] = logo().lines();
80
41
  const top = [
81
- `${upper} ${this.status.text()}`,
42
+ `${upper} ${status.text()}`,
82
43
  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}`),
44
+ ...wands.map(([, l]) => `${l.head}${' '.repeat(width - stripVTControlCharacters(l.head).length)} ${styleText(['', 'idle'].includes(stripVTControlCharacters(l.head)) ? 'gray' : 'green', duration(Date.now() - l.start).clock(), { validateStream: false })} ${l.tail}`),
84
45
  '-'.repeat(10),
85
- ...notes.slice(Math.max(0, notes.length - height - this.view.depth), notes.length - this.view.depth)
46
+ ...wrapped.slice(Math.max(0, wrapped.length - height - view.depth), wrapped.length - view.depth)
86
47
  ];
87
- this.stream.write(
48
+ stream.write(
88
49
  `\x1b[H${
89
- [...top, ...Array(Math.max(1, rows - top.length - 1)).fill(''), this.#edge(this.legend.text(), columns)]
50
+ [...top, ...Array(Math.max(1, rows - top.length - 1)).fill(''), edge(legend.text(), columns)]
90
51
  .map((l) => `\x1b[2K${sliceAnsi(l, 0, columns)}\x1b[0m`)
91
52
  .join('\n')
92
53
  }\x1b[J`
93
54
  );
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
- }
99
- #wrap(text, width) {
100
- return stripVTControlCharacters(text).length <= width || width < 3 ? [text] : [sliceAnsi(text, 0, width), ...this.#wrap(` ${sliceAnsi(text, width)}`, width)];
101
- }
102
- }
55
+ };
56
+ const redraw = () => {
57
+ if (clock.length > 0) {
58
+ draw();
59
+ }
60
+ };
61
+ const paint = () => {
62
+ if (clock.length === 0) {
63
+ stream.write('\x1b[?1049h\x1b[?25l\x1b[?1007h');
64
+ clock.push(setInterval(() => draw(), 1000).unref());
65
+ stream.on('resize', () => redraw());
66
+ }
67
+ draw();
68
+ };
69
+ const update = (index, text) => {
70
+ const [head, tail = ''] = text.split('\t');
71
+ const line = lines.get(index) ?? { head: '', start: Date.now() };
72
+ lines.set(index, { head, tail, start: head === line.head ? line.start : Date.now() });
73
+ paint();
74
+ };
75
+ const note = (level, text) => {
76
+ notes.push(
77
+ `${level === 'warn' ? `\x1b[38;2;255;135;0m${new Date().toTimeString().slice(0, 8)}\x1b[39m` : 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`)}`
78
+ );
79
+ if (view.depth > 0) {
80
+ view.depth += wrap(notes.at(-1), stream.columns || 80).length;
81
+ }
82
+ paint();
83
+ };
84
+ return Object.freeze({
85
+ row: (index) => row({ push: ([i, text]) => update(i, text) }, index),
86
+ notice: () => protocol({ push: ([level, text]) => note(level, text) }),
87
+ up: () => {
88
+ view.depth += 1;
89
+ redraw();
90
+ },
91
+ down: () => {
92
+ view.depth = Math.max(0, view.depth - 1);
93
+ redraw();
94
+ },
95
+ close: () => {
96
+ clock.splice(0).forEach((c) => {
97
+ clearInterval(c);
98
+ stream.write('\x1b[?1007l\x1b[?25h\x1b[?1049l');
99
+ stream.write(notes.map((n) => `${n}\n`).join(''));
100
+ });
101
+ }
102
+ });
103
+ };
package/src/wand.js CHANGED
@@ -2,39 +2,36 @@
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
+ import { ellipsized } from './ellipsized.js';
6
7
 
7
- export class Wand {
8
- constructor(wishes, agent, row, protocol) {
9
- this.wishes = wishes;
10
- this.agent = agent;
11
- this.row = row;
12
- this.protocol = protocol;
13
- }
14
- async run(signal) {
15
- try {
16
- while (!signal.aborted) {
17
- await this.#once(signal);
18
- }
19
- } catch (e) {
20
- if (!signal.aborted) {
21
- throw e;
22
- }
23
- }
24
- }
25
- async #once(signal) {
26
- this.row.show(`${styleText('gray', 'idle', { validateStream: false })}\t${styleText('gray', 'waiting for wishes', { validateStream: false })}`);
27
- const wish = await this.wishes.next(signal);
28
- this.row.show(`${wish.name()}\tstarting the agent`);
8
+ export const wand = (wishes, agent, row, protocol) => {
9
+ const once = async (signal) => {
10
+ row.show(`${styleText('gray', 'idle', { validateStream: false })}\t${styleText('gray', 'waiting for wishes', { validateStream: false })}`);
11
+ const wish = await wishes.next(signal);
12
+ row.show(`${ellipsized(wish.name(), 64).text()}\tstarting the agent`);
29
13
  const start = Date.now();
30
- const { code, summary, link = '', tokens, model } = await this.agent.run(wish, { show: (text) => this.row.show(`${wish.name()}\t${text}`) }, signal);
31
- const suffix = [`(${[wish.name(), new Duration(Date.now() - start).minutes(), tokens, model].filter((p) => p !== '').join(' ')})`, link].filter((p) => p !== '').join(' ');
14
+ const { code, summary, link = '', tokens, model } = await agent.run(wish, { show: (text) => row.show(`${ellipsized(wish.name(), 64).text()}\t${text}`) }, signal);
15
+ const suffix = [`(${[wish.name(), duration(Date.now() - start).minutes(), tokens, model].filter((p) => p !== '').join(' ')})`, link].filter((p) => p !== '').join(' ');
32
16
  if (code === 0) {
33
17
  await wish.close();
34
- this.protocol.info(`${summary === '' ? 'done' : summary} ${suffix}`);
18
+ protocol.info(`${summary === '' ? 'done' : summary} ${suffix}`);
35
19
  } else {
36
20
  wish.free();
37
- this.protocol.info(`failed with exit code ${code}${summary === '' ? '' : `: ${summary}`} ${suffix}`);
21
+ protocol.info(`failed with exit code ${code}${summary === '' ? '' : `: ${summary}`} ${suffix}`);
22
+ }
23
+ };
24
+ return Object.freeze({
25
+ run: async (signal) => {
26
+ try {
27
+ while (!signal.aborted) {
28
+ await once(signal);
29
+ }
30
+ } catch (e) {
31
+ if (!signal.aborted) {
32
+ throw e;
33
+ }
34
+ }
38
35
  }
39
- }
40
- }
36
+ });
37
+ };
package/src/wheel.js CHANGED
@@ -1,20 +1,16 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
2
  // SPDX-License-Identifier: MIT
3
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) =>
4
+ export const wheel = (stream, screen) => Object.freeze({
5
+ listen: () => {
6
+ stream.on('data', (chunk) =>
11
7
  chunk
12
8
  .toString()
13
9
  .split('\x1b')
14
10
  .slice(1)
15
11
  .map((s) => s.slice(0, 2))
16
12
  .filter((s) => /^[[O][AB]$/.test(s))
17
- .forEach((s) => (s.endsWith('A') ? this.screen.up() : this.screen.down()))
13
+ .forEach((s) => (s.endsWith('A') ? screen.up() : screen.down()))
18
14
  );
19
15
  }
20
- }
16
+ });
package/src/wish.js CHANGED
@@ -4,26 +4,13 @@
4
4
  import { readFile, rm } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
6
 
7
- export class Wish {
8
- constructor(dir, path, claims) {
9
- this.dir = dir;
10
- this.path = path;
11
- this.claims = claims;
12
- }
13
- name() {
14
- return this.path;
15
- }
16
- async text() {
17
- return readFile(join(this.dir, this.path), 'utf8');
18
- }
19
- fate() {
20
- return 'which will be deleted once you exit with code zero';
21
- }
22
- async close() {
23
- await rm(join(this.dir, this.path));
24
- this.claims.delete(this.path);
25
- }
26
- free() {
27
- this.claims.set(this.path, false);
28
- }
29
- }
7
+ export const wish = (dir, path, claims) => Object.freeze({
8
+ name: () => path,
9
+ text: async () => readFile(join(dir, path), 'utf8'),
10
+ fate: () => 'which will be deleted once you exit with code zero',
11
+ close: async () => {
12
+ await rm(join(dir, path), { force: true });
13
+ claims.delete(path);
14
+ },
15
+ free: () => claims.set(path, false)
16
+ });
package/src/wishes.js CHANGED
@@ -4,46 +4,40 @@
4
4
  import { readdir } from 'node:fs/promises';
5
5
  import { basename, join, relative } from 'node:path';
6
6
  import { setTimeout } from 'node:timers/promises';
7
- import { Wish } from './wish.js';
8
- import { Archived } from './archived.js';
9
- import { Permanent } from './permanent.js';
7
+ import { wish } from './wish.js';
8
+ import { archived } from './archived.js';
9
+ import { permanent } from './permanent.js';
10
10
 
11
- export class Wishes {
12
- constructor(dir, interval, archive = '', pause = 0, notice = { warn: () => {} }) {
13
- this.dir = dir;
14
- this.interval = interval;
15
- this.archive = archive;
16
- this.pause = pause;
17
- this.notice = notice;
18
- this.claims = new Map();
19
- this.startup = [];
20
- }
21
- async next(signal) {
11
+ export const wishes = (dir, interval, archive = '', pause = 0, notice = { warn: () => {} }) => {
12
+ const claims = new Map();
13
+ const startup = [];
14
+ const next = async (signal) => {
22
15
  signal.throwIfAborted();
23
- const files = (await readdir(this.dir, { recursive: true, withFileTypes: true }))
16
+ const files = (await readdir(dir, { recursive: true, withFileTypes: true }))
24
17
  .filter((f) => f.isFile() && f.name.endsWith('.md'))
25
- .map((f) => join(relative(this.dir, f.parentPath), f.name))
18
+ .map((f) => join(relative(dir, f.parentPath), f.name))
26
19
  .sort();
27
20
  signal.throwIfAborted();
28
- if (this.startup.length === 0) {
29
- this.startup.push(new Set(files));
21
+ if (startup.length === 0) {
22
+ startup.push(new Set(files));
30
23
  const count = files.filter((f) => !/^[A-Z]/.test(basename(f))).length;
31
24
  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`);
25
+ notice.warn(`found ${count} pre-existing ${count === 1 ? 'wish' : 'wishes'} in ${dir} that ${count === 1 ? 'is' : 'are'} not permanent`);
33
26
  }
34
27
  }
35
- const path = files.find((f) => !this.claims.has(f))
36
- ?? [...this.claims.keys()].find((c) => !this.claims.get(c) && files.includes(c));
28
+ const path = files.find((f) => !claims.has(f))
29
+ ?? [...claims.keys()].find((c) => !claims.get(c) && files.includes(c));
37
30
  if (path === undefined) {
38
- await setTimeout(this.interval, undefined, { signal });
39
- return this.next(signal);
31
+ await setTimeout(interval, undefined, { signal });
32
+ return next(signal);
40
33
  }
41
- this.claims.delete(path);
42
- this.claims.set(path, true);
43
- const wish = new Wish(this.dir, path, this.claims);
44
- if (this.startup[0].has(path) && /^[A-Z]/.test(basename(path))) {
45
- return new Permanent(wish, this.pause);
34
+ claims.delete(path);
35
+ claims.set(path, true);
36
+ const w = wish(dir, path, claims);
37
+ if (startup[0].has(path) && /^[A-Z]/.test(basename(path))) {
38
+ return permanent(w, pause);
46
39
  }
47
- return this.archive === '' ? wish : new Archived(wish, this.dir, this.archive);
48
- }
49
- }
40
+ return archive === '' ? w : archived(w, dir, archive);
41
+ };
42
+ return Object.freeze({ next });
43
+ };