@yegor256/jinka 0.0.9 → 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 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 repositories were in good shape: `foo/bar` and `foo/bar2`.
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, giving it the path to your directory with wishes:
19
+ Finally, you run it:
20
20
 
21
21
  ```bash
22
22
  jinka
package/bin/jinka.js CHANGED
@@ -20,6 +20,8 @@ import { log } from '../src/log.js';
20
20
  import { status } from '../src/status.js';
21
21
  import { quota } from '../src/quota.js';
22
22
  import { rollouts } from '../src/rollouts.js';
23
+ import { recorded } from '../src/recorded.js';
24
+ import { model } from '../src/model.js';
23
25
  import { meters } from '../src/meters.js';
24
26
  import { freeze } from '../src/freeze.js';
25
27
  import { frozen } from '../src/frozen.js';
@@ -35,6 +37,7 @@ import { jobs } from '../src/jobs.js';
35
37
  import { summary } from '../src/summary.js';
36
38
  import { summarized } from '../src/summarized.js';
37
39
  import { semaphore } from '../src/semaphore.js';
40
+ import { hello } from '../src/hello.js';
38
41
 
39
42
  const root = join(dirname(fileURLToPath(import.meta.url)), '..');
40
43
  const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
@@ -97,7 +100,10 @@ const a = values.agent === '' ? engine : values.agent;
97
100
  const t = tokens();
98
101
  const q = quota();
99
102
  const c = crew(values.threads, limit, { warn: (text) => screen.notice().warn(text) });
100
- const j = jobs(genie((engine === 'codex' ? codex : claude)(agent(a), values.model, values.effort, resolve(values.skill)), disk(dir, resolve(values.logs), resolve(values.memory), c), meters([t, engine === 'codex' ? rollouts(q, join(process.env.CODEX_HOME ?? join(homedir(), '.codex'), 'sessions')) : q])));
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));
101
107
  const f = freeze(c, { warn: (text) => screen.notice().warn(text) }, () => quit(0));
102
108
  const r = release(pkg.version, 'https://registry.npmjs.org/@yegor256/jinka/latest');
103
109
  const start = Date.now();
@@ -118,6 +124,7 @@ process.on('exit', () => screen.close());
118
124
  if (tui) {
119
125
  r.check();
120
126
  }
127
+ screen.notice().warn(`every agent starts as: ${e.command(d.args())}`);
121
128
  const quit = (code) => {
122
129
  screen.close();
123
130
  process.stdout.write('', () => process.stderr.write('', () => process.exit(code)));
@@ -133,6 +140,7 @@ process.on('SIGINT', interrupt);
133
140
  keys(process.stdin, f, c, interrupt).listen();
134
141
  wheel(process.stdin, screen).listen();
135
142
  try {
143
+ await hello(e, screen.notice()).run(controller.signal);
136
144
  await jinka(
137
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),
138
146
  j,
package/package.json CHANGED
@@ -35,5 +35,5 @@
35
35
  "test": "c8 --check-coverage --lines 80 mocha"
36
36
  },
37
37
  "type": "module",
38
- "version": "0.0.9"
38
+ "version": "0.0.10"
39
39
  }
package/src/agent.js CHANGED
@@ -15,5 +15,6 @@ export const agent = (command) => Object.freeze({
15
15
  );
16
16
  }
17
17
  return child;
18
- }
18
+ },
19
+ command: (args) => [command, ...args].map((a) => (/^[\w./\\:=@%+,~-]+$/.test(a) ? a : `'${a.replaceAll("'", "'\\''")}'`)).join(' ')
19
20
  });
package/src/claude.js CHANGED
@@ -1,17 +1,18 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- export const claude = (agent, model, effort, plugin) => Object.freeze({
5
- start: (args, signal) => agent.start(
6
- [
7
- '-p', '--output-format', 'stream-json', '--verbose',
8
- '--plugin-dir', plugin,
9
- ...args,
10
- '--dangerously-skip-permissions',
11
- ...(model === '' ? [] : ['--model', model]),
12
- ...(effort === '' ? [] : ['--effort', effort])
13
- ],
14
- signal
15
- ),
16
- lines: () => []
17
- });
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,20 +3,21 @@
3
3
 
4
4
  import { join } from 'node:path';
5
5
 
6
- export const codex = (agent, model, effort, plugin) => Object.freeze({
7
- start: (args, signal) => agent.start(
8
- [
9
- 'exec', '--json',
10
- '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check',
11
- ...args,
12
- ...(model === '' ? [] : ['--model', model]),
13
- ...(effort === '' ? [] : ['--config', `model_reasoning_effort=${effort}`]),
14
- '-'
15
- ],
16
- signal
17
- ),
18
- lines: () => [
19
- `Your skills live in the directory ${join(plugin, 'skills')}, one skill per subdirectory, each described in its SKILL.md file.`,
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.'
21
- ]
22
- });
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.`,
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.'
21
+ ]
22
+ });
23
+ };
package/src/genie.js CHANGED
@@ -4,9 +4,8 @@
4
4
  import { createInterface } from 'node:readline';
5
5
  import { event } from './event.js';
6
6
  import { tokens } from './tokens.js';
7
- import { model } from './model.js';
8
7
 
9
- export const genie = (engine, disk, meter) => Object.freeze({
8
+ export const genie = (engine, disk, meter, model) => Object.freeze({
10
9
  run: async (wish, row, signal) => {
11
10
  const prompt = [
12
11
  'Make this wish come true, working on your own without asking any questions:',
package/src/hello.js ADDED
@@ -0,0 +1,29 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { createInterface } from 'node:readline';
5
+ import { event } from './event.js';
6
+
7
+ export const hello = (engine, notice) => Object.freeze({
8
+ run: (signal) => new Promise((resolve, reject) => {
9
+ const child = engine.start([], signal);
10
+ const answer = [''];
11
+ child.stdin.on('error', () => {});
12
+ child.stdin.end('Tell me, who you are, in a very short single sentence.');
13
+ child.stderr.resume();
14
+ createInterface({ input: child.stdout }).on('line', (line) => {
15
+ answer[0] = event(line, 0).summary() || answer[0];
16
+ });
17
+ child.on('error', (e) => (signal.aborted ? resolve() : reject(new Error(`Cannot start the agent "${child.spawnfile}": ${e.message}`))));
18
+ child.on('close', (code) => {
19
+ if (signal.aborted) {
20
+ resolve();
21
+ } else if (code === 0) {
22
+ notice.info(`the agent says: ${answer[0]}`);
23
+ resolve();
24
+ } else {
25
+ reject(new Error(`The agent "${child.spawnfile}" exited with code ${code} while telling who it is: ${answer[0]}`));
26
+ }
27
+ });
28
+ })
29
+ });
package/src/model.js CHANGED
@@ -15,7 +15,11 @@ export const model = () => {
15
15
  }
16
16
  },
17
17
  text: () => {
18
- const parts = (names.at(-1) ?? '').replace(/^claude-/, '').replace(/-\d{8}$/, '').split('-').filter((p) => p !== '');
18
+ const name = names.at(-1) ?? '';
19
+ if (!name.startsWith('claude-')) {
20
+ return name;
21
+ }
22
+ const parts = name.replace(/^claude-/, '').replace(/-\d{8}$/, '').split('-').filter((p) => p !== '');
19
23
  return [
20
24
  ...parts.filter((p) => !/^\d+$/.test(p)).map((p) => `${p[0].toUpperCase()}${p.slice(1)}`),
21
25
  parts.filter((p) => /^\d+$/.test(p)).join('.')
@@ -0,0 +1,34 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ export const recorded = (origin, dir) => {
8
+ let thread = '';
9
+ return Object.freeze({
10
+ add: (line) => {
11
+ origin.add(line);
12
+ try {
13
+ const json = JSON.parse(line);
14
+ if (json.type === 'thread.started' && typeof json.thread_id === 'string') {
15
+ thread = json.thread_id;
16
+ }
17
+ if (json.type === 'turn.completed' && thread !== '' && existsSync(dir)) {
18
+ readdirSync(dir, { recursive: true })
19
+ .map(String)
20
+ .filter((path) => path.endsWith(`-${thread}.jsonl`))
21
+ .flatMap((path) => readFileSync(join(dir, path), 'utf8').split('\n'))
22
+ .filter((text) => text.includes('"turn_context"'))
23
+ .map((text) => JSON.parse(text).payload?.model)
24
+ .filter((name) => typeof name === 'string')
25
+ .slice(-1)
26
+ .forEach((name) => origin.add(JSON.stringify({ type: 'system', subtype: 'init', model: name })));
27
+ }
28
+ } catch {
29
+ return;
30
+ }
31
+ },
32
+ text: () => origin.text()
33
+ });
34
+ };
package/src/tty.js CHANGED
@@ -17,9 +17,18 @@ export const tty = (stream, crew, status, legend) => {
17
17
  const [left, right] = text.split('\t');
18
18
  return `${left}${' '.repeat(Math.max(1, width - 1 - stripVTControlCharacters(left).length - stripVTControlCharacters(right).length))}${right}`;
19
19
  };
20
- const wrap = (text, width) => (
21
- stripVTControlCharacters(text).length <= width || width < 3 ? [text] : [sliceAnsi(text, 0, width), ...wrap(` ${sliceAnsi(text, width)}`, width)]
22
- );
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];
28
+ }
29
+ const [end, start] = cut(text, width);
30
+ return [sliceAnsi(text, 0, end), ...wrap(` ${sliceAnsi(text, start)}`, width)];
31
+ };
23
32
  const draw = () => {
24
33
  const rows = stream.rows || 24;
25
34
  const wands = crew.indexes().map((i) => [i, lines.get(i) ?? { head: '', tail: '', start: Date.now() }]);
@@ -63,9 +72,9 @@ export const tty = (stream, crew, status, legend) => {
63
72
  lines.set(index, { head, tail, start: head === line.head ? line.start : Date.now() });
64
73
  paint();
65
74
  };
66
- const note = (text) => {
75
+ const note = (level, text) => {
67
76
  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`)}`
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`)}`
69
78
  );
70
79
  if (view.depth > 0) {
71
80
  view.depth += wrap(notes.at(-1), stream.columns || 80).length;
@@ -74,7 +83,7 @@ export const tty = (stream, crew, status, legend) => {
74
83
  };
75
84
  return Object.freeze({
76
85
  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) }),
86
+ notice: () => protocol({ push: ([level, text]) => note(level, text) }),
78
87
  up: () => {
79
88
  view.depth += 1;
80
89
  redraw();