@yegor256/jinka 0.0.7 → 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/bin/jinka.js +66 -48
- package/package.json +1 -1
- package/src/agent.js +4 -7
- package/src/archived.js +22 -30
- package/src/claude.js +14 -24
- package/src/codex.js +17 -27
- package/src/colorless.js +13 -0
- package/src/crew.js +56 -61
- package/src/disk.js +20 -28
- package/src/duration.js +26 -26
- package/src/ellipsized.js +10 -0
- package/src/event.js +76 -82
- package/src/freeze.js +36 -41
- package/src/frozen.js +19 -21
- package/src/genie.js +18 -23
- package/src/jinka.js +7 -13
- package/src/jobs.js +13 -16
- package/src/keys.js +19 -23
- package/src/legend.js +9 -13
- package/src/limited.js +12 -15
- package/src/log.js +8 -17
- package/src/logo.js +3 -5
- package/src/meters.js +3 -8
- package/src/model.js +20 -20
- package/src/permanent.js +9 -21
- package/src/protocol.js +4 -11
- package/src/quota.js +22 -32
- package/src/release.js +30 -0
- package/src/rollouts.js +38 -40
- package/src/row.js +3 -9
- package/src/semaphore.js +16 -0
- package/src/status.js +10 -18
- package/src/summarized.js +15 -26
- package/src/summary.js +11 -19
- package/src/throttled.js +19 -24
- package/src/tokens.js +23 -25
- package/src/tty.js +79 -87
- package/src/wand.js +26 -29
- package/src/wheel.js +5 -9
- package/src/wish.js +10 -23
- package/src/wishes.js +25 -31
package/src/tty.js
CHANGED
|
@@ -3,100 +3,92 @@
|
|
|
3
3
|
|
|
4
4
|
import { stripVTControlCharacters, styleText } from 'node:util';
|
|
5
5
|
import sliceAnsi from 'slice-ansi';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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();
|
|
69
|
-
}
|
|
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();
|
|
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 wrap = (text, width) => (
|
|
21
|
+
stripVTControlCharacters(text).length <= width || width < 3 ? [text] : [sliceAnsi(text, 0, width), ...wrap(` ${sliceAnsi(text, width)}`, width)]
|
|
22
|
+
);
|
|
23
|
+
const draw = () => {
|
|
24
|
+
const rows = stream.rows || 24;
|
|
25
|
+
const wands = crew.indexes().map((i) => [i, lines.get(i) ?? { head: '', tail: '', start: Date.now() }]);
|
|
26
|
+
const width = Math.max(...wands.map(([, l]) => stripVTControlCharacters(l.head).length));
|
|
27
|
+
const columns = stream.columns || 80;
|
|
28
|
+
const wrapped = notes.flatMap((n) => wrap(n, columns));
|
|
29
|
+
const height = Math.max(0, rows - wands.length - 5);
|
|
30
|
+
view.depth = Math.min(view.depth, Math.max(0, wrapped.length - height));
|
|
31
|
+
const [upper, lower] = logo().lines();
|
|
80
32
|
const top = [
|
|
81
|
-
`${upper} ${
|
|
33
|
+
`${upper} ${status.text()}`,
|
|
82
34
|
lower,
|
|
83
|
-
...
|
|
35
|
+
...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
36
|
'-'.repeat(10),
|
|
85
|
-
...
|
|
37
|
+
...wrapped.slice(Math.max(0, wrapped.length - height - view.depth), wrapped.length - view.depth)
|
|
86
38
|
];
|
|
87
|
-
|
|
39
|
+
stream.write(
|
|
88
40
|
`\x1b[H${
|
|
89
|
-
[...top, ...Array(Math.max(1, rows - top.length - 1)).fill(''),
|
|
41
|
+
[...top, ...Array(Math.max(1, rows - top.length - 1)).fill(''), edge(legend.text(), columns)]
|
|
90
42
|
.map((l) => `\x1b[2K${sliceAnsi(l, 0, columns)}\x1b[0m`)
|
|
91
43
|
.join('\n')
|
|
92
44
|
}\x1b[J`
|
|
93
45
|
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
46
|
+
};
|
|
47
|
+
const redraw = () => {
|
|
48
|
+
if (clock.length > 0) {
|
|
49
|
+
draw();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const paint = () => {
|
|
53
|
+
if (clock.length === 0) {
|
|
54
|
+
stream.write('\x1b[?1049h\x1b[?25l\x1b[?1007h');
|
|
55
|
+
clock.push(setInterval(() => draw(), 1000).unref());
|
|
56
|
+
stream.on('resize', () => redraw());
|
|
57
|
+
}
|
|
58
|
+
draw();
|
|
59
|
+
};
|
|
60
|
+
const update = (index, text) => {
|
|
61
|
+
const [head, tail = ''] = text.split('\t');
|
|
62
|
+
const line = lines.get(index) ?? { head: '', start: Date.now() };
|
|
63
|
+
lines.set(index, { head, tail, start: head === line.head ? line.start : Date.now() });
|
|
64
|
+
paint();
|
|
65
|
+
};
|
|
66
|
+
const note = (text) => {
|
|
67
|
+
notes.push(
|
|
68
|
+
`${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`)}`
|
|
69
|
+
);
|
|
70
|
+
if (view.depth > 0) {
|
|
71
|
+
view.depth += wrap(notes.at(-1), stream.columns || 80).length;
|
|
72
|
+
}
|
|
73
|
+
paint();
|
|
74
|
+
};
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
row: (index) => row({ push: ([i, text]) => update(i, text) }, index),
|
|
77
|
+
notice: () => protocol({ push: ([level, text]) => note(level === 'warn' ? `\x1b[38;2;255;135;0m${text}\x1b[39m` : text) }),
|
|
78
|
+
up: () => {
|
|
79
|
+
view.depth += 1;
|
|
80
|
+
redraw();
|
|
81
|
+
},
|
|
82
|
+
down: () => {
|
|
83
|
+
view.depth = Math.max(0, view.depth - 1);
|
|
84
|
+
redraw();
|
|
85
|
+
},
|
|
86
|
+
close: () => {
|
|
87
|
+
clock.splice(0).forEach((c) => {
|
|
88
|
+
clearInterval(c);
|
|
89
|
+
stream.write('\x1b[?1007l\x1b[?25h\x1b[?1049l');
|
|
90
|
+
stream.write(notes.map((n) => `${n}\n`).join(''));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
};
|
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 {
|
|
5
|
+
import { duration } from './duration.js';
|
|
6
|
+
import { ellipsized } from './ellipsized.js';
|
|
6
7
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
31
|
-
const suffix = [`(${[wish.name(),
|
|
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
|
-
|
|
18
|
+
protocol.info(`${summary === '' ? 'done' : summary} ${suffix}`);
|
|
35
19
|
} else {
|
|
36
20
|
wish.free();
|
|
37
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
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') ?
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { wish } from './wish.js';
|
|
8
|
+
import { archived } from './archived.js';
|
|
9
|
+
import { permanent } from './permanent.js';
|
|
10
10
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
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(
|
|
18
|
+
.map((f) => join(relative(dir, f.parentPath), f.name))
|
|
26
19
|
.sort();
|
|
27
20
|
signal.throwIfAborted();
|
|
28
|
-
if (
|
|
29
|
-
|
|
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
|
-
|
|
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) => !
|
|
36
|
-
?? [...
|
|
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(
|
|
39
|
-
return
|
|
31
|
+
await setTimeout(interval, undefined, { signal });
|
|
32
|
+
return next(signal);
|
|
40
33
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
return
|
|
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
|
|
48
|
-
}
|
|
49
|
-
}
|
|
40
|
+
return archive === '' ? w : archived(w, dir, archive);
|
|
41
|
+
};
|
|
42
|
+
return Object.freeze({ next });
|
|
43
|
+
};
|