@yegor256/jinka 0.0.2 → 0.0.4
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/README.md +3 -10
- package/bin/jinka.js +62 -28
- package/package.json +6 -1
- package/plugin/skills/accept-pull-request/SKILL.md +48 -12
- package/plugin/skills/check-ci-jobs/SKILL.md +80 -0
- package/plugin/skills/clean-up-backlog/SKILL.md +59 -14
- package/plugin/skills/find-new-bug/SKILL.md +69 -0
- package/plugin/skills/provide-code-review/SKILL.md +66 -0
- package/plugin/skills/triage-an-issue/SKILL.md +86 -12
- package/src/agent.js +32 -0
- package/src/archived.js +0 -5
- package/src/claude.js +28 -32
- package/src/crew.js +67 -0
- package/src/disk.js +35 -0
- package/src/duration.js +0 -5
- package/src/event.js +47 -11
- package/src/freeze.js +43 -0
- package/src/frozen.js +26 -0
- package/src/jinka.js +6 -9
- package/src/keys.js +28 -0
- package/src/legend.js +15 -0
- package/src/log.js +4 -11
- package/src/meters.js +11 -0
- package/src/model.js +25 -0
- package/src/permanent.js +3 -12
- package/src/quota.js +21 -0
- package/src/row.js +0 -5
- package/src/status.js +14 -7
- package/src/tokens.js +26 -0
- package/src/tty.js +33 -28
- package/src/wand.js +6 -11
- package/src/wish.js +0 -5
- package/src/wishes.js +6 -13
package/src/tty.js
CHANGED
|
@@ -1,49 +1,44 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
5
|
+
import sliceAnsi from 'slice-ansi';
|
|
4
6
|
import { Row } from './row.js';
|
|
5
7
|
import { Duration } from './duration.js';
|
|
6
8
|
|
|
7
|
-
/**
|
|
8
|
-
* The screen for terminals, which takes over the whole terminal like top.
|
|
9
|
-
* It switches to the alternate buffer and draws a fixed layout from the
|
|
10
|
-
* top left corner: the status line, one line per wand with the time it
|
|
11
|
-
* has been busy with its current wish, a separator, and the newest
|
|
12
|
-
* protocol lines that fit below it. The layout is redrawn on every change,
|
|
13
|
-
* every second, and on every resize, until the screen is closed and the
|
|
14
|
-
* normal buffer comes back.
|
|
15
|
-
*/
|
|
16
9
|
export class Tty {
|
|
17
|
-
constructor(stream,
|
|
10
|
+
constructor(stream, crew, status, legend) {
|
|
18
11
|
this.stream = stream;
|
|
12
|
+
this.crew = crew;
|
|
19
13
|
this.status = status;
|
|
20
|
-
this.
|
|
14
|
+
this.legend = legend;
|
|
15
|
+
this.lines = new Map();
|
|
21
16
|
this.notes = [];
|
|
22
17
|
this.clock = [];
|
|
23
18
|
}
|
|
24
19
|
row(index) {
|
|
25
20
|
return new Row({ push: ([i, text]) => this.#update(i, text) }, index);
|
|
26
21
|
}
|
|
27
|
-
|
|
28
|
-
return new Row({ push: ([
|
|
22
|
+
notice() {
|
|
23
|
+
return new Row({ push: ([, text]) => this.#note(text) }, 0);
|
|
29
24
|
}
|
|
30
25
|
close() {
|
|
31
26
|
this.clock.splice(0).forEach((c) => {
|
|
32
27
|
clearInterval(c);
|
|
33
28
|
this.stream.write('\x1b[?25h\x1b[?1049l');
|
|
29
|
+
this.stream.write(this.notes.map((n) => `${n}\n`).join(''));
|
|
34
30
|
});
|
|
35
31
|
}
|
|
36
32
|
#update(index, text) {
|
|
37
33
|
const [head, tail = ''] = text.split('\t');
|
|
38
|
-
this.lines
|
|
39
|
-
|
|
40
|
-
tail,
|
|
41
|
-
start: head === this.lines[index].head ? this.lines[index].start : Date.now()
|
|
42
|
-
};
|
|
34
|
+
const line = this.lines.get(index) ?? { head: '', start: Date.now() };
|
|
35
|
+
this.lines.set(index, { head, tail, start: head === line.head ? line.start : Date.now() });
|
|
43
36
|
this.#paint();
|
|
44
37
|
}
|
|
45
|
-
#note(
|
|
46
|
-
this.notes.push(
|
|
38
|
+
#note(text) {
|
|
39
|
+
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`)}`
|
|
41
|
+
);
|
|
47
42
|
this.#paint();
|
|
48
43
|
}
|
|
49
44
|
#paint() {
|
|
@@ -59,17 +54,27 @@ export class Tty {
|
|
|
59
54
|
this.#draw();
|
|
60
55
|
}
|
|
61
56
|
#draw() {
|
|
62
|
-
|
|
63
|
-
const
|
|
57
|
+
const rows = this.stream.rows || 24;
|
|
58
|
+
const lines = this.crew.indexes().map((i) => [i, this.lines.get(i) ?? { head: '', tail: '', start: Date.now() }]);
|
|
59
|
+
const width = Math.max(...lines.map(([, l]) => stripVTControlCharacters(l.head).length));
|
|
60
|
+
const columns = this.stream.columns || 80;
|
|
61
|
+
const notes = this.notes.flatMap((n) => this.#wrap(n, columns));
|
|
62
|
+
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}`),
|
|
66
|
+
'-'.repeat(10),
|
|
67
|
+
...notes.slice(Math.max(0, notes.length - Math.max(0, rows - lines.length - 5)))
|
|
68
|
+
];
|
|
64
69
|
this.stream.write(
|
|
65
70
|
`\x1b[H${
|
|
66
|
-
[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
'-'.repeat(10),
|
|
70
|
-
...this.notes
|
|
71
|
-
].map((l) => `\x1b[2K${l.slice(0, this.stream.columns || 80)}`).join('\n')
|
|
71
|
+
[...top, ...Array(Math.max(1, rows - top.length - 1)).fill(''), this.legend.text()]
|
|
72
|
+
.map((l) => `\x1b[2K${sliceAnsi(l, 0, columns)}\x1b[0m`)
|
|
73
|
+
.join('\n')
|
|
72
74
|
}\x1b[J`
|
|
73
75
|
);
|
|
74
76
|
}
|
|
77
|
+
#wrap(text, width) {
|
|
78
|
+
return stripVTControlCharacters(text).length <= width || width < 3 ? [text] : [sliceAnsi(text, 0, width), ...this.#wrap(` ${sliceAnsi(text, width)}`, width)];
|
|
79
|
+
}
|
|
75
80
|
}
|
package/src/wand.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
+
import { styleText } from 'node:util';
|
|
4
5
|
import { Duration } from './duration.js';
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
-
* One loop that keeps making wishes come true until the signal aborts.
|
|
8
|
-
* It takes the next free wish, shows it on its own screen row, runs the
|
|
9
|
-
* agent on it, and closes the wish when the agent succeeds or frees it
|
|
10
|
-
* for a later retry when the agent fails. Either way, it reports the
|
|
11
|
-
* outcome and the time it took to the protocol of the screen.
|
|
12
|
-
*/
|
|
13
7
|
export class Wand {
|
|
14
8
|
constructor(wishes, agent, row, protocol) {
|
|
15
9
|
this.wishes = wishes;
|
|
@@ -29,17 +23,18 @@ export class Wand {
|
|
|
29
23
|
}
|
|
30
24
|
}
|
|
31
25
|
async #once(signal) {
|
|
32
|
-
this.row.show('idle\
|
|
26
|
+
this.row.show(`${styleText('gray', 'idle', { validateStream: false })}\t${styleText('gray', 'waiting for wishes', { validateStream: false })}`);
|
|
33
27
|
const wish = await this.wishes.next(signal);
|
|
34
28
|
this.row.show(`${wish.name()}\tstarting the agent`);
|
|
35
29
|
const start = Date.now();
|
|
36
|
-
const code = await this.agent.run(wish, { show: (text) => this.row.show(`${wish.name()}\t${text}`) }, signal);
|
|
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(' ');
|
|
37
32
|
if (code === 0) {
|
|
38
33
|
await wish.close();
|
|
39
|
-
this.protocol.show(`${
|
|
34
|
+
this.protocol.show(`${summary === '' ? 'done' : summary} ${suffix}`);
|
|
40
35
|
} else {
|
|
41
36
|
wish.free();
|
|
42
|
-
this.protocol.show(
|
|
37
|
+
this.protocol.show(`failed with exit code ${code}${summary === '' ? '' : `: ${summary}`} ${suffix}`);
|
|
43
38
|
}
|
|
44
39
|
}
|
|
45
40
|
}
|
package/src/wish.js
CHANGED
|
@@ -4,11 +4,6 @@
|
|
|
4
4
|
import { readFile, rm } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* One wish, a Markdown file inside the wishes directory.
|
|
9
|
-
* The wish shares the map of claims with the wishes directory,
|
|
10
|
-
* so it can give its claim back when a wand is done with it.
|
|
11
|
-
*/
|
|
12
7
|
export class Wish {
|
|
13
8
|
constructor(dir, path, claims) {
|
|
14
9
|
this.dir = dir;
|
package/src/wishes.js
CHANGED
|
@@ -2,26 +2,18 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import { readdir } from 'node:fs/promises';
|
|
5
|
-
import { join, relative } from 'node:path';
|
|
5
|
+
import { basename, join, relative } from 'node:path';
|
|
6
6
|
import { setTimeout } from 'node:timers/promises';
|
|
7
7
|
import { Wish } from './wish.js';
|
|
8
8
|
import { Archived } from './archived.js';
|
|
9
9
|
import { Permanent } from './permanent.js';
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* The directory with wishes, shared by all wands.
|
|
13
|
-
* It is the only object with mutable state: a map of claims, where the
|
|
14
|
-
* order of entries tells how recently each wish was tried and the value
|
|
15
|
-
* tells whether a wand holds it now. A wish is claimed synchronously
|
|
16
|
-
* right after the scan, so two wands never get the same file.
|
|
17
|
-
* The wishes found by the first scan are permanent and stay in the
|
|
18
|
-
* directory forever, while the ones that appear later are one-time.
|
|
19
|
-
*/
|
|
20
11
|
export class Wishes {
|
|
21
|
-
constructor(dir, interval, archive = '') {
|
|
12
|
+
constructor(dir, interval, archive = '', pause = 0) {
|
|
22
13
|
this.dir = dir;
|
|
23
14
|
this.interval = interval;
|
|
24
15
|
this.archive = archive;
|
|
16
|
+
this.pause = pause;
|
|
25
17
|
this.claims = new Map();
|
|
26
18
|
this.startup = [];
|
|
27
19
|
}
|
|
@@ -31,6 +23,7 @@ export class Wishes {
|
|
|
31
23
|
.filter((f) => f.isFile() && f.name.endsWith('.md'))
|
|
32
24
|
.map((f) => join(relative(this.dir, f.parentPath), f.name))
|
|
33
25
|
.sort();
|
|
26
|
+
signal.throwIfAborted();
|
|
34
27
|
if (this.startup.length === 0) {
|
|
35
28
|
this.startup.push(new Set(files));
|
|
36
29
|
}
|
|
@@ -43,8 +36,8 @@ export class Wishes {
|
|
|
43
36
|
this.claims.delete(path);
|
|
44
37
|
this.claims.set(path, true);
|
|
45
38
|
const wish = new Wish(this.dir, path, this.claims);
|
|
46
|
-
if (this.startup[0].has(path)) {
|
|
47
|
-
return new Permanent(wish);
|
|
39
|
+
if (this.startup[0].has(path) && /^[A-Z]/.test(basename(path))) {
|
|
40
|
+
return new Permanent(wish, this.pause);
|
|
48
41
|
}
|
|
49
42
|
return this.archive === '' ? wish : new Archived(wish, this.dir, this.archive);
|
|
50
43
|
}
|