@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/README.md +2 -2
- package/bin/jinka.js +69 -51
- package/package.json +1 -1
- package/src/agent.js +6 -8
- package/src/archived.js +22 -30
- package/src/claude.js +15 -24
- package/src/codex.js +17 -26
- 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 +17 -23
- package/src/hello.js +29 -0
- 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 +24 -20
- package/src/permanent.js +9 -21
- package/src/protocol.js +4 -11
- package/src/quota.js +22 -32
- package/src/recorded.js +34 -0
- package/src/release.js +30 -0
- package/src/rollouts.js +38 -40
- package/src/row.js +3 -9
- package/src/semaphore.js +8 -13
- 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 +87 -86
- 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/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
First, you create `~/.jinka/wishes/Make-Me-Happy.md` file:
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
I wish my GitHub
|
|
10
|
+
I wish my GitHub repository to be in good shape: `foo/bar`.
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Then, you install `jinka`:
|
|
@@ -16,7 +16,7 @@ Then, you install `jinka`:
|
|
|
16
16
|
npm install -g @yegor256/jinka
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Finally, you run it
|
|
19
|
+
Finally, you run it:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
jinka
|
package/bin/jinka.js
CHANGED
|
@@ -7,32 +7,37 @@ import { readFile } from 'node:fs/promises';
|
|
|
7
7
|
import { resolve, join, dirname, parse } from 'node:path';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
10
|
+
import { jinka } from '../src/jinka.js';
|
|
11
|
+
import { wishes } from '../src/wishes.js';
|
|
12
|
+
import { agent } from '../src/agent.js';
|
|
13
|
+
import { claude } from '../src/claude.js';
|
|
14
|
+
import { codex } from '../src/codex.js';
|
|
15
|
+
import { genie } from '../src/genie.js';
|
|
16
|
+
import { disk } from '../src/disk.js';
|
|
17
|
+
import { tty } from '../src/tty.js';
|
|
18
|
+
import { colorless } from '../src/colorless.js';
|
|
19
|
+
import { log } from '../src/log.js';
|
|
20
|
+
import { status } from '../src/status.js';
|
|
21
|
+
import { quota } from '../src/quota.js';
|
|
22
|
+
import { rollouts } from '../src/rollouts.js';
|
|
23
|
+
import { recorded } from '../src/recorded.js';
|
|
24
|
+
import { model } from '../src/model.js';
|
|
25
|
+
import { meters } from '../src/meters.js';
|
|
26
|
+
import { freeze } from '../src/freeze.js';
|
|
27
|
+
import { frozen } from '../src/frozen.js';
|
|
28
|
+
import { limited } from '../src/limited.js';
|
|
29
|
+
import { throttled } from '../src/throttled.js';
|
|
30
|
+
import { keys } from '../src/keys.js';
|
|
31
|
+
import { wheel } from '../src/wheel.js';
|
|
32
|
+
import { legend } from '../src/legend.js';
|
|
33
|
+
import { release } from '../src/release.js';
|
|
34
|
+
import { tokens } from '../src/tokens.js';
|
|
35
|
+
import { crew } from '../src/crew.js';
|
|
36
|
+
import { jobs } from '../src/jobs.js';
|
|
37
|
+
import { summary } from '../src/summary.js';
|
|
38
|
+
import { summarized } from '../src/summarized.js';
|
|
39
|
+
import { semaphore } from '../src/semaphore.js';
|
|
40
|
+
import { hello } from '../src/hello.js';
|
|
36
41
|
|
|
37
42
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
38
43
|
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
@@ -62,6 +67,7 @@ const program = new Command()
|
|
|
62
67
|
.name('jinka')
|
|
63
68
|
.description(`${pkg.description} (${pkg.version})`)
|
|
64
69
|
.version(pkg.version)
|
|
70
|
+
.configureHelp({ sortOptions: true })
|
|
65
71
|
.showHelpAfterError('(add --help for additional information)')
|
|
66
72
|
.addOption(new Option('--wishes <dir>', 'directory with wishes, one per Markdown file').default(join(homedir(), '.jinka', 'wishes'), '~/.jinka/wishes'))
|
|
67
73
|
.option('--threads <n>', `how many wishes run at once, up to ${limit}, which the + and - keys change while running`, bounded, 6)
|
|
@@ -77,37 +83,48 @@ const program = new Command()
|
|
|
77
83
|
.addOption(new Option('--effort <level>', 'reasoning effort passed to the agent, such as low, medium, high, xhigh, or max').default('', 'agent default'))
|
|
78
84
|
.option('--throttle <percent>', 'how much of the agent quota, in percent, must be left in every window before a new job starts', percent, 15)
|
|
79
85
|
.addOption(new Option('--pid <file>', 'file that exists while jinka runs, so that a second jinka refuses to start').default(join(homedir(), '.jinka', 'pid.txt'), '~/.jinka/pid.txt'))
|
|
86
|
+
.option('--no-color', 'paint nothing in the terminal, which a non-empty NO_COLOR environment variable also does')
|
|
87
|
+
.option('--no-tui', 'print only the protocol, without the status bars and the list of threads')
|
|
80
88
|
.addOption(new Option('--limit <n>', 'how many jobs start before jinka waits for them to finish and quits').argParser(positive).default(Infinity, 'unlimited'))
|
|
81
89
|
.parse();
|
|
82
90
|
const values = program.opts();
|
|
83
|
-
const
|
|
84
|
-
await
|
|
91
|
+
const s = semaphore(resolve(values.pid));
|
|
92
|
+
await s.acquire().catch((e) => {
|
|
85
93
|
console.error(e.message);
|
|
86
94
|
process.exit(1);
|
|
87
95
|
});
|
|
88
|
-
process.on('exit', () =>
|
|
96
|
+
process.on('exit', () => s.release());
|
|
89
97
|
const dir = resolve(values.wishes);
|
|
90
98
|
const engine = values.engine ?? (parse(values.agent).name === 'codex' ? 'codex' : 'claude');
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
99
|
+
const a = values.agent === '' ? engine : values.agent;
|
|
100
|
+
const t = tokens();
|
|
101
|
+
const q = quota();
|
|
102
|
+
const c = crew(values.threads, limit, { warn: (text) => screen.notice().warn(text) });
|
|
103
|
+
const sessions = join(process.env.CODEX_HOME ?? join(homedir(), '.codex'), 'sessions');
|
|
104
|
+
const e = (engine === 'codex' ? codex : claude)(agent(a), values.model, values.effort, resolve(values.skill));
|
|
105
|
+
const d = disk(dir, resolve(values.logs), resolve(values.memory), c);
|
|
106
|
+
const j = jobs(genie(e, d, meters([t, engine === 'codex' ? rollouts(q, sessions) : q]), engine === 'codex' ? () => recorded(model(), sessions) : model));
|
|
107
|
+
const f = freeze(c, { warn: (text) => screen.notice().warn(text) }, () => quit(0));
|
|
108
|
+
const r = release(pkg.version, 'https://registry.npmjs.org/@yegor256/jinka/latest');
|
|
97
109
|
const start = Date.now();
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
const tui = process.stdout.isTTY && values.tui;
|
|
111
|
+
const screen = summarized(
|
|
112
|
+
tui
|
|
113
|
+
? tty(
|
|
114
|
+
values.color && !process.env.NO_COLOR ? process.stdout : colorless(process.stdout),
|
|
115
|
+
c,
|
|
116
|
+
status(start, j, a, values.model, t, q),
|
|
117
|
+
legend(f, r)
|
|
105
118
|
)
|
|
106
|
-
:
|
|
107
|
-
|
|
119
|
+
: log(process.stdout),
|
|
120
|
+
summary(start, pkg.version, j, { agent: a, model: values.model, effort: values.effort, wishes: dir }),
|
|
108
121
|
process.stdout
|
|
109
122
|
);
|
|
110
123
|
process.on('exit', () => screen.close());
|
|
124
|
+
if (tui) {
|
|
125
|
+
r.check();
|
|
126
|
+
}
|
|
127
|
+
screen.notice().warn(`every agent starts as: ${e.command(d.args())}`);
|
|
111
128
|
const quit = (code) => {
|
|
112
129
|
screen.close();
|
|
113
130
|
process.stdout.write('', () => process.stderr.write('', () => process.exit(code)));
|
|
@@ -120,18 +137,19 @@ const interrupt = () => {
|
|
|
120
137
|
controller.abort();
|
|
121
138
|
};
|
|
122
139
|
process.on('SIGINT', interrupt);
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
keys(process.stdin, f, c, interrupt).listen();
|
|
141
|
+
wheel(process.stdin, screen).listen();
|
|
125
142
|
try {
|
|
126
|
-
await
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
await hello(e, screen.notice()).run(controller.signal);
|
|
144
|
+
await jinka(
|
|
145
|
+
limited(frozen(throttled(wishes(dir, values.interval, values.archive === '' ? '' : resolve(values.archive), values.pause, { warn: (text) => screen.notice().warn(text) }), q, values.throttle, 15 * 60 * 1000, { warn: (text) => screen.notice().warn(text) }), f), values.limit, f),
|
|
146
|
+
j,
|
|
129
147
|
screen,
|
|
130
|
-
|
|
148
|
+
c
|
|
131
149
|
).run(controller.signal);
|
|
132
150
|
quit(130);
|
|
133
151
|
} catch (e) {
|
|
134
152
|
screen.close();
|
|
135
|
-
console.error(e
|
|
153
|
+
console.error(e);
|
|
136
154
|
quit(1);
|
|
137
155
|
}
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -3,13 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import spawn from 'cross-spawn';
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
this.command = command;
|
|
9
|
-
}
|
|
10
|
-
start(args, signal) {
|
|
6
|
+
export const agent = (command) => Object.freeze({
|
|
7
|
+
start: (args, signal) => {
|
|
11
8
|
const windows = process.platform === 'win32';
|
|
12
|
-
const child = spawn(
|
|
9
|
+
const child = spawn(command, args, windows ? { stdio: 'pipe' } : { signal, stdio: 'pipe' });
|
|
13
10
|
if (windows) {
|
|
14
11
|
signal.addEventListener(
|
|
15
12
|
'abort',
|
|
@@ -18,5 +15,6 @@ export class Agent {
|
|
|
18
15
|
);
|
|
19
16
|
}
|
|
20
17
|
return child;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
18
|
+
},
|
|
19
|
+
command: (args) => [command, ...args].map((a) => (/^[\w./\\:=@%+,~-]+$/.test(a) ? a : `'${a.replaceAll("'", "'\\''")}'`)).join(' ')
|
|
20
|
+
});
|
package/src/archived.js
CHANGED
|
@@ -4,34 +4,26 @@
|
|
|
4
4
|
import { copyFile, mkdir, access } from 'node:fs/promises';
|
|
5
5
|
import { join, dirname, extname } from 'node:path';
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return this.origin.name();
|
|
15
|
-
}
|
|
16
|
-
async text() {
|
|
17
|
-
return this.origin.text();
|
|
18
|
-
}
|
|
19
|
-
fate() {
|
|
20
|
-
return this.origin.fate();
|
|
21
|
-
}
|
|
22
|
-
async close() {
|
|
23
|
-
await copyFile(join(this.dir, this.origin.name()), await this.#target());
|
|
24
|
-
await this.origin.close();
|
|
25
|
-
}
|
|
26
|
-
free() {
|
|
27
|
-
this.origin.free();
|
|
28
|
-
}
|
|
29
|
-
async #target() {
|
|
30
|
-
const target = join(this.archive, this.origin.name());
|
|
31
|
-
await mkdir(dirname(target), { recursive: true });
|
|
32
|
-
return access(target).then(
|
|
33
|
-
() => `${target.slice(0, target.length - extname(target).length)}-${Date.now()}${extname(target)}`,
|
|
34
|
-
() => target
|
|
7
|
+
export const archived = (origin, dir, archive) => {
|
|
8
|
+
const target = async () => {
|
|
9
|
+
const path = join(archive, origin.name());
|
|
10
|
+
await mkdir(dirname(path), { recursive: true });
|
|
11
|
+
return access(path).then(
|
|
12
|
+
() => `${path.slice(0, path.length - extname(path).length)}-${Date.now()}${extname(path)}`,
|
|
13
|
+
() => path
|
|
35
14
|
);
|
|
36
|
-
}
|
|
37
|
-
|
|
15
|
+
};
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
name: () => origin.name(),
|
|
18
|
+
text: async () => origin.text(),
|
|
19
|
+
fate: () => origin.fate(),
|
|
20
|
+
close: async () => {
|
|
21
|
+
await access(join(dir, origin.name())).then(
|
|
22
|
+
async () => copyFile(join(dir, origin.name()), await target()),
|
|
23
|
+
() => {}
|
|
24
|
+
);
|
|
25
|
+
await origin.close();
|
|
26
|
+
},
|
|
27
|
+
free: () => origin.free()
|
|
28
|
+
});
|
|
29
|
+
};
|
package/src/claude.js
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
...(this.effort === '' ? [] : ['--effort', this.effort])
|
|
20
|
-
],
|
|
21
|
-
signal
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
lines() {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
4
|
+
export const claude = (agent, model, effort, plugin) => {
|
|
5
|
+
const full = (args) => [
|
|
6
|
+
'-p', '--output-format', 'stream-json', '--verbose',
|
|
7
|
+
'--plugin-dir', plugin,
|
|
8
|
+
...args,
|
|
9
|
+
'--dangerously-skip-permissions',
|
|
10
|
+
...(model === '' ? [] : ['--model', model]),
|
|
11
|
+
...(effort === '' ? [] : ['--effort', effort])
|
|
12
|
+
];
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
start: (args, signal) => agent.start(full(args), signal),
|
|
15
|
+
command: (args) => agent.command(full(args)),
|
|
16
|
+
lines: () => []
|
|
17
|
+
});
|
|
18
|
+
};
|
package/src/codex.js
CHANGED
|
@@ -3,30 +3,21 @@
|
|
|
3
3
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
...(this.effort === '' ? [] : ['--config', `model_reasoning_effort=${this.effort}`]),
|
|
21
|
-
'-'
|
|
22
|
-
],
|
|
23
|
-
signal
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
lines() {
|
|
27
|
-
return [
|
|
28
|
-
`Your skills live in the directory ${join(this.plugin, 'skills')}, one skill per subdirectory, each described in its SKILL.md file.`,
|
|
6
|
+
export const codex = (agent, model, effort, plugin) => {
|
|
7
|
+
const full = (args) => [
|
|
8
|
+
'exec', '--json',
|
|
9
|
+
'--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check',
|
|
10
|
+
...args,
|
|
11
|
+
...(model === '' ? [] : ['--model', model]),
|
|
12
|
+
...(effort === '' ? [] : ['--config', `model_reasoning_effort=${effort}`]),
|
|
13
|
+
'-'
|
|
14
|
+
];
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
start: (args, signal) => agent.start(full(args), signal),
|
|
17
|
+
command: (args) => agent.command(full(args)),
|
|
18
|
+
lines: () => [
|
|
19
|
+
`Your skills live in the directory ${join(plugin, 'skills')}, one skill per subdirectory, each described in its SKILL.md file.`,
|
|
29
20
|
'Before you start, read the descriptions of these skills, and whenever a skill fits your work, read its SKILL.md file and follow it.'
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
}
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
};
|
package/src/colorless.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
export const colorless = (stream) => Object.freeze({
|
|
5
|
+
get columns() {
|
|
6
|
+
return stream.columns;
|
|
7
|
+
},
|
|
8
|
+
get rows() {
|
|
9
|
+
return stream.rows;
|
|
10
|
+
},
|
|
11
|
+
on: (event, listener) => stream.on(event, listener),
|
|
12
|
+
write: (text, done) => stream.write(text.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), ''), done)
|
|
13
|
+
});
|
package/src/crew.js
CHANGED
|
@@ -1,67 +1,62 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
indexes() {
|
|
15
|
-
return [...this.seats.keys()].sort((a, b) => a - b);
|
|
16
|
-
}
|
|
17
|
-
more() {
|
|
18
|
-
if (this.seats.size >= this.limit) {
|
|
19
|
-
this.notice.warn(`more than ${this.limit} threads are not allowed`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const index = Array.from({ length: this.limit }, (_, i) => i).find((i) => !this.seats.has(i));
|
|
23
|
-
this.seats.set(index, { idle: false, controller: new AbortController() });
|
|
24
|
-
this.state.launch(index);
|
|
25
|
-
}
|
|
26
|
-
less() {
|
|
27
|
-
const index = this.indexes().findLast((i) => this.seats.get(i).idle);
|
|
28
|
-
if (this.seats.size === 1) {
|
|
29
|
-
this.notice.warn('the last thread cannot be removed');
|
|
30
|
-
} else if (index === undefined) {
|
|
31
|
-
this.notice.warn('no idle thread can be removed right now');
|
|
32
|
-
} else {
|
|
33
|
-
this.seats.get(index).controller.abort();
|
|
34
|
-
this.seats.delete(index);
|
|
4
|
+
export const crew = (size, limit, notice) => {
|
|
5
|
+
const seats = new Map(Array.from({ length: size }, (_, i) => [i, { idle: false, controller: new AbortController() }]));
|
|
6
|
+
const state = { launch: () => {}, runs: [] };
|
|
7
|
+
const indexes = () => [...seats.keys()].sort((a, b) => a - b);
|
|
8
|
+
const join = async (from) => {
|
|
9
|
+
const to = state.runs.length;
|
|
10
|
+
if (from < to) {
|
|
11
|
+
await Promise.all(state.runs.slice(from, to));
|
|
12
|
+
await join(to);
|
|
35
13
|
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
14
|
+
};
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
size: () => seats.size,
|
|
17
|
+
indexes,
|
|
18
|
+
more: () => {
|
|
19
|
+
if (seats.size >= limit) {
|
|
20
|
+
notice.warn(`more than ${limit} threads are not allowed`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const index = Array.from({ length: limit }, (_, i) => i).find((i) => !seats.has(i));
|
|
24
|
+
seats.set(index, { idle: false, controller: new AbortController() });
|
|
25
|
+
state.launch(index);
|
|
26
|
+
},
|
|
27
|
+
less: () => {
|
|
28
|
+
const index = indexes().findLast((i) => seats.get(i).idle);
|
|
29
|
+
if (seats.size === 1) {
|
|
30
|
+
notice.warn('the last thread cannot be removed');
|
|
31
|
+
} else if (index === undefined) {
|
|
32
|
+
notice.warn('no idle thread can be removed right now');
|
|
33
|
+
} else {
|
|
34
|
+
seats.get(index).controller.abort();
|
|
35
|
+
seats.delete(index);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
run: async (wishes, wand, signal) => {
|
|
39
|
+
state.launch = (index) => {
|
|
40
|
+
const seat = seats.get(index);
|
|
41
|
+
state.runs.push(
|
|
42
|
+
wand(
|
|
43
|
+
index,
|
|
44
|
+
{
|
|
45
|
+
next: async (s) => {
|
|
46
|
+
seat.idle = true;
|
|
47
|
+
try {
|
|
48
|
+
return await wishes.next(s);
|
|
49
|
+
} finally {
|
|
50
|
+
seat.idle = false;
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
await this.#join(0);
|
|
59
|
-
}
|
|
60
|
-
async #join(from) {
|
|
61
|
-
const to = this.state.runs.length;
|
|
62
|
-
if (from < to) {
|
|
63
|
-
await Promise.all(this.state.runs.slice(from, to));
|
|
64
|
-
await this.#join(to);
|
|
53
|
+
},
|
|
54
|
+
AbortSignal.any([signal, seat.controller.signal])
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
indexes().forEach((i) => state.launch(i));
|
|
59
|
+
await join(0);
|
|
65
60
|
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
61
|
+
});
|
|
62
|
+
};
|
package/src/disk.js
CHANGED
|
@@ -5,37 +5,29 @@ import { createWriteStream } from 'node:fs';
|
|
|
5
5
|
import { mkdir } from 'node:fs/promises';
|
|
6
6
|
import { join, dirname, extname } from 'node:path';
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this
|
|
12
|
-
|
|
13
|
-
this
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`if something must happen first, write a new wish that waits for it and finishes the job, and then complete the current wish right away.`, `Your memory lives in the directory ${this.memory}, keep there what the skills tell you to keep.`,
|
|
26
|
-
`Jinka runs ${this.crew.size()} threads right now, so up to ${this.crew.size()} wishes run at once.`
|
|
27
|
-
];
|
|
28
|
-
}
|
|
29
|
-
args() {
|
|
30
|
-
return ['--add-dir', this.wishes, '--add-dir', this.memory];
|
|
31
|
-
}
|
|
32
|
-
async log(wish) {
|
|
8
|
+
export const disk = (wishes, logs, memory, crew) => Object.freeze({
|
|
9
|
+
lines: (wish) => [
|
|
10
|
+
`The wish is written in the file ${join(wishes, wish.name())}, ${wish.fate()}.`,
|
|
11
|
+
'Never delete, move, or rename the file of this wish yourself, even after you split it into smaller wishes, since Jinka takes care of it.',
|
|
12
|
+
`All wishes live in the directory ${wishes}, and you may add new wishes there as Markdown files.`,
|
|
13
|
+
`If this wish is an atomic job that involves no other jobs, make it come true yourself;`,
|
|
14
|
+
`otherwise, do not do it yourself, but split it into smaller wishes`,
|
|
15
|
+
`and write each of them as a separate Markdown file in the directory ${join(wishes, dirname(wish.name()))}.`,
|
|
16
|
+
`Always start the name of every new wish file with a small English letter, never with a capital one.`,
|
|
17
|
+
`In every Git repository you touch, do all your work inside a separate Git worktree, never in the main directory of the repository, so that other wishes running at the same time do not see your changes.`,
|
|
18
|
+
`Never spend time waiting while doing a wish, neither sleeping nor polling for a build, a review, a merge, or anything else;`,
|
|
19
|
+
`if something must happen first, write a new wish that waits for it and finishes the job, and then complete the current wish right away.`,
|
|
20
|
+
`Your memory lives in the directory ${memory}, keep there what the skills tell you to keep.`,
|
|
21
|
+
`Jinka runs ${crew.size()} threads right now, so up to ${crew.size()} wishes run at once.`
|
|
22
|
+
],
|
|
23
|
+
args: () => ['--add-dir', wishes, '--add-dir', memory],
|
|
24
|
+
log: async (wish) => {
|
|
33
25
|
const file = join(
|
|
34
|
-
|
|
26
|
+
logs,
|
|
35
27
|
`${wish.name().slice(0, wish.name().length - extname(wish.name()).length)}-${new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').slice(0, 15)}.jsonl`
|
|
36
28
|
);
|
|
37
29
|
await mkdir(dirname(file), { recursive: true });
|
|
38
|
-
await mkdir(
|
|
30
|
+
await mkdir(memory, { recursive: true });
|
|
39
31
|
return createWriteStream(file);
|
|
40
32
|
}
|
|
41
|
-
}
|
|
33
|
+
});
|
package/src/duration.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
minutes() {
|
|
9
|
-
const seconds = Math.floor(this.millis / 1000);
|
|
4
|
+
export const duration = (millis) => {
|
|
5
|
+
const minutes = () => {
|
|
6
|
+
const seconds = Math.floor(millis / 1000);
|
|
10
7
|
return [Math.floor(seconds / 60), seconds % 60].map((p) => String(p).padStart(2, '0')).join(':');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
};
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
minutes,
|
|
11
|
+
clock: () => {
|
|
12
|
+
const total = Math.floor(millis / 60_000);
|
|
13
|
+
if (total < 1000) {
|
|
14
|
+
return minutes().padStart(6);
|
|
15
|
+
}
|
|
16
|
+
if (total < 6000) {
|
|
17
|
+
return `${String(Math.floor(total / 60)).padStart(2, '0')}:${String(total % 60).padStart(2, '0')}m`;
|
|
18
|
+
}
|
|
19
|
+
return `${Math.floor(total / 60)}h`.padStart(6);
|
|
20
|
+
},
|
|
21
|
+
phrase: () => {
|
|
22
|
+
const seconds = Math.floor(millis / 1000);
|
|
23
|
+
const [count, unit] = seconds < 60 ? [seconds, 'second'] : [Math.floor(seconds / 60), 'minute'];
|
|
24
|
+
return `${count} ${unit}${count === 1 ? '' : 's'}`;
|
|
25
|
+
},
|
|
26
|
+
hours: () => {
|
|
27
|
+
const seconds = Math.floor(millis / 1000);
|
|
28
|
+
return [Math.floor(seconds / 3600), Math.floor(seconds / 60) % 60, seconds % 60].map((p) => String(p).padStart(2, '0')).join(':');
|
|
16
29
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
return `${Math.floor(minutes / 60)}h`.padStart(6);
|
|
21
|
-
}
|
|
22
|
-
phrase() {
|
|
23
|
-
const seconds = Math.floor(this.millis / 1000);
|
|
24
|
-
const [count, unit] = seconds < 60 ? [seconds, 'second'] : [Math.floor(seconds / 60), 'minute'];
|
|
25
|
-
return `${count} ${unit}${count === 1 ? '' : 's'}`;
|
|
26
|
-
}
|
|
27
|
-
hours() {
|
|
28
|
-
const seconds = Math.floor(this.millis / 1000);
|
|
29
|
-
return [Math.floor(seconds / 3600), Math.floor(seconds / 60) % 60, seconds % 60].map((p) => String(p).padStart(2, '0')).join(':');
|
|
30
|
-
}
|
|
31
|
-
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
export const ellipsized = (origin, limit) => Object.freeze({
|
|
5
|
+
text: () => (
|
|
6
|
+
origin.length < limit
|
|
7
|
+
? origin
|
|
8
|
+
: `${origin.slice(0, Math.floor((limit - 2) / 2))}…${origin.slice(-Math.ceil((limit - 2) / 2))}`
|
|
9
|
+
)
|
|
10
|
+
});
|