@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/bin/jinka.js +51 -17
- package/package.json +2 -2
- package/plugin/skills/accept-pull-request/SKILL.md +15 -25
- package/plugin/skills/check-ci-jobs/SKILL.md +8 -29
- package/plugin/skills/clean-up-backlog/SKILL.md +15 -37
- package/plugin/skills/find-new-bug/SKILL.md +10 -29
- package/plugin/skills/fix-a-bug/SKILL.md +74 -0
- package/plugin/skills/provide-code-review/SKILL.md +15 -29
- package/plugin/skills/triage-an-issue/SKILL.md +7 -31
- package/src/agent.js +2 -12
- package/src/claude.js +18 -47
- package/src/codex.js +32 -0
- package/src/crew.js +3 -3
- package/src/disk.js +7 -1
- package/src/duration.js +15 -0
- package/src/event.js +34 -12
- package/src/freeze.js +4 -1
- package/src/genie.js +61 -0
- package/src/jobs.js +19 -0
- package/src/legend.js +7 -3
- package/src/limited.js +19 -0
- package/src/log.js +4 -1
- package/src/logo.js +8 -0
- package/src/protocol.js +14 -0
- package/src/quota.js +21 -5
- package/src/rollouts.js +53 -0
- package/src/status.js +10 -9
- package/src/summarized.js +30 -0
- package/src/summary.js +26 -0
- package/src/throttled.js +34 -0
- package/src/tokens.js +3 -0
- package/src/tty.js +37 -15
- package/src/wand.js +2 -2
- package/src/wheel.js +20 -0
- package/src/wishes.js +6 -1
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(
|
|
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
|
-
`
|
|
19
|
-
`
|
|
20
|
-
`agent
|
|
21
|
-
`model
|
|
22
|
-
`tokens
|
|
23
|
-
`
|
|
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
|
+
}
|
package/src/throttled.js
ADDED
|
@@ -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
|
|
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[
|
|
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).
|
|
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 -
|
|
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.
|
|
34
|
+
this.protocol.info(`${summary === '' ? 'done' : summary} ${suffix}`);
|
|
35
35
|
} else {
|
|
36
36
|
wish.free();
|
|
37
|
-
this.protocol.
|
|
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));
|