gemcatch 0.1.0 → 0.2.0
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/CHANGELOG.md +35 -1
- package/README.md +19 -7
- package/index.js +123 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-07-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `gemcatch batch <file>` — submit many background tasks from a file in one
|
|
15
|
+
command. One prompt per non-empty line by default (`#` comments and blanks are
|
|
16
|
+
skipped); `--separator <str>` splits the file on a delimiter line so prompts
|
|
17
|
+
can span multiple lines; `-` reads the list from stdin. The whole batch is
|
|
18
|
+
tagged as a unit — `-t/--tag` sets it, otherwise an auto tag `batch-<hex>` is
|
|
19
|
+
generated and printed — so it is collectable with `gemcatch list --tag`.
|
|
20
|
+
Submissions are bounded but concurrent (four at a time, paced by `GEMCATCH_RPM`),
|
|
21
|
+
and a single failed submit is marked `failed` and reported without sinking the
|
|
22
|
+
rest of the batch. Supports `-m/--model`, `-s/--system`, `--dry-run` (parse and
|
|
23
|
+
list, submit nothing), `--json`, and `-w/--watch` (poll the batch to completion,
|
|
24
|
+
then print a combined tally).
|
|
25
|
+
|
|
26
|
+
## [0.1.1] - 2026-07-19
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- REST fallback (`GEMCATCH_FORCE_REST=1`, or when the SDK lacks Interactions)
|
|
31
|
+
no longer prepends the echoed prompt and the model's reasoning to the result.
|
|
32
|
+
A real Interactions response tags each step by type; only `model_output` is
|
|
33
|
+
the answer. The default SDK path was unaffected. Verified live against the API.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Require Node.js 22+. The advertised "20+" was never accurate — `commander@15`
|
|
38
|
+
requires Node `>=22.12`, and Node 20 reached end-of-life. CI now covers 22 and 24.
|
|
39
|
+
- Clarified the free-tier wording in the README: `gemini-3.1-flash-lite` is free
|
|
40
|
+
within the free tier's daily quota, then billed at standard rates.
|
|
41
|
+
|
|
10
42
|
## [0.1.0] - 2026-07-17
|
|
11
43
|
|
|
12
44
|
First release. Wraps the background execution capability added to the Gemini
|
|
@@ -64,5 +96,7 @@ seen a task complete, the text is cached locally and survives that expiry — bu
|
|
|
64
96
|
something has to poll inside that window for it to be seen at all, which is what
|
|
65
97
|
`gemcatch daemon` exists to do.
|
|
66
98
|
|
|
67
|
-
[Unreleased]: https://github.com/Booyaka101/gemcatch/compare/v0.
|
|
99
|
+
[Unreleased]: https://github.com/Booyaka101/gemcatch/compare/v0.2.0...HEAD
|
|
100
|
+
[0.2.0]: https://github.com/Booyaka101/gemcatch/compare/v0.1.1...v0.2.0
|
|
101
|
+
[0.1.1]: https://github.com/Booyaka101/gemcatch/compare/v0.1.0...v0.1.1
|
|
68
102
|
[0.1.0]: https://github.com/Booyaka101/gemcatch/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ The EU AI Act's high-risk obligations phase in from August 2026, whereas...
|
|
|
24
24
|
|
|
25
25
|
## Setup
|
|
26
26
|
|
|
27
|
-
Needs Node.js
|
|
27
|
+
Needs Node.js 22+ and a Gemini API key. **Getting a key needs no billing account and no card.** `gemini-3.1-flash-lite` runs free within the [free tier's](https://ai.google.dev/gemini-api/docs/pricing) daily quota; past that, paid rates apply.
|
|
28
28
|
|
|
29
29
|
1. Get a key at **<https://aistudio.google.com/apikey>**
|
|
30
30
|
2. Put it in your environment:
|
|
@@ -79,6 +79,7 @@ This week in AI: ...
|
|
|
79
79
|
| Command | What it does |
|
|
80
80
|
| --- | --- |
|
|
81
81
|
| `gemcatch research "<prompt>"` | Submits with `background: true`, stores the interaction ID, exits immediately. |
|
|
82
|
+
| `gemcatch batch <file>` | Submits many prompts from a file at once, tagged as one collectable batch. |
|
|
82
83
|
| `gemcatch status <id>` | Polls the API and prints the current state. |
|
|
83
84
|
| `gemcatch get <id>` | Prints the full response if complete, otherwise the current status. |
|
|
84
85
|
| `gemcatch list` | All tasks, newest first: id, age, status, prompt. |
|
|
@@ -95,15 +96,16 @@ Useful flags:
|
|
|
95
96
|
| Flag | On | Does |
|
|
96
97
|
| --- | --- | --- |
|
|
97
98
|
| `--json` | most commands | Machine-readable output. |
|
|
98
|
-
| `-m, --model <id>` | `research` | Override the model. |
|
|
99
|
-
| `-s, --system <text>` | `research` | Set a system instruction. |
|
|
99
|
+
| `-m, --model <id>` | `research`, `batch` | Override the model. |
|
|
100
|
+
| `-s, --system <text>` | `research`, `batch` | Set a system instruction. |
|
|
100
101
|
| `-f, --file <path>` | `research` | Read the prompt from a file. |
|
|
101
|
-
| `-t, --tag <tag>` | `research`, `list` | Label tasks and filter them. |
|
|
102
|
-
| `-w, --watch` | `research` | Submit and wait, in one command. |
|
|
102
|
+
| `-t, --tag <tag>` | `research`, `batch`, `list` | Label tasks and filter them. |
|
|
103
|
+
| `-w, --watch` | `research`, `batch` | Submit and wait, in one command. |
|
|
104
|
+
| `--separator <str>` | `batch` | Split the file on this delimiter line for multi-line prompts. |
|
|
103
105
|
| `-i, --interval <s>` | `watch`, `daemon` | Poll rate. Default 10s for `watch`, 300s for `daemon`. |
|
|
104
106
|
| `--exit-when-idle` | `daemon` | Stop once nothing is left in flight. |
|
|
105
107
|
| `-n, --limit <n>` | `list` | Cap the rows. |
|
|
106
|
-
| `--dry-run` | `prune` | Show what would go; delete nothing. |
|
|
108
|
+
| `--dry-run` | `batch`, `prune` | Show what would go; submit/delete nothing. |
|
|
107
109
|
| `--raw` | `get` | Dump the raw interaction JSON. |
|
|
108
110
|
|
|
109
111
|
IDs are the first 8 characters of a UUID. Any unique prefix works, so `gemcatch get 8f3a` is fine.
|
|
@@ -113,7 +115,17 @@ Statuses come straight from the API: `in_progress`, `requires_action`, `complete
|
|
|
113
115
|
## Recipes
|
|
114
116
|
|
|
115
117
|
```bash
|
|
116
|
-
# Fire off a
|
|
118
|
+
# Fire off a whole file of prompts in one command, then collect later.
|
|
119
|
+
# Every task shares one auto-generated tag (batch-xxxxxx), printed on submit.
|
|
120
|
+
$ gemcatch batch questions.txt # one prompt per line; # and blanks skipped
|
|
121
|
+
$ gemcatch daemon --exit-when-idle # keep polling until they're all in
|
|
122
|
+
$ gemcatch list --tag batch-1a2b3c --status completed
|
|
123
|
+
|
|
124
|
+
# Multi-line prompts: split the file on a delimiter line instead of per-line
|
|
125
|
+
$ gemcatch batch briefs.md --separator ---
|
|
126
|
+
$ gemcatch batch - < questions.txt # or pipe the list in on stdin
|
|
127
|
+
|
|
128
|
+
# The same thing by hand, if you prefer a loop
|
|
117
129
|
$ for q in "topic A" "topic B" "topic C"; do gemcatch research "$q" -t batch1; done
|
|
118
130
|
$ gemcatch sync # one pass now...
|
|
119
131
|
$ gemcatch daemon --exit-when-idle # ...or keep polling until they're all in
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
const { Command, Option } = require('commander');
|
|
6
7
|
const store = require('./db');
|
|
7
8
|
const gemini = require('./gemini');
|
|
@@ -42,6 +43,12 @@ function emit(json, value, human) {
|
|
|
42
43
|
else human();
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// One-line preview of a prompt for the list/batch columns.
|
|
47
|
+
function snippet(prompt, n = 60) {
|
|
48
|
+
const s = (prompt || '').replace(/\s+/g, ' ');
|
|
49
|
+
return s.length > n ? `${s.slice(0, n - 3)}...` : s;
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
function die(err) {
|
|
46
53
|
console.error(`Error: ${err.message}`);
|
|
47
54
|
process.exit(1);
|
|
@@ -168,6 +175,121 @@ program
|
|
|
168
175
|
}
|
|
169
176
|
});
|
|
170
177
|
|
|
178
|
+
// --- batch ----------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
// Turn a prompts file into a list of prompts. Default: one per line, skipping
|
|
181
|
+
// blank lines and `#` comments. With --separator, split the whole file on that
|
|
182
|
+
// delimiter line instead, so a single prompt can span multiple lines.
|
|
183
|
+
function parsePrompts(text, separator) {
|
|
184
|
+
if (separator) {
|
|
185
|
+
const blocks = [];
|
|
186
|
+
let cur = [];
|
|
187
|
+
for (const line of text.split(/\r?\n/)) {
|
|
188
|
+
if (line.trim() === separator) {
|
|
189
|
+
blocks.push(cur.join('\n').trim());
|
|
190
|
+
cur = [];
|
|
191
|
+
} else {
|
|
192
|
+
cur.push(line);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
blocks.push(cur.join('\n').trim());
|
|
196
|
+
return blocks.filter(Boolean);
|
|
197
|
+
}
|
|
198
|
+
return text
|
|
199
|
+
.split(/\r?\n/)
|
|
200
|
+
.map((l) => l.trim())
|
|
201
|
+
.filter((l) => l && !l.startsWith('#'));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Poll just this batch until nothing tagged with it is still in flight, then
|
|
205
|
+
// tally the outcome. Modelled on syncPass (a bounded refresh pass) and
|
|
206
|
+
// watchTask (poll-until-terminal), but scoped to one tag.
|
|
207
|
+
async function watchBatch(tag, intervalMs, json) {
|
|
208
|
+
const inFlight = () => store.listTasks({ tag }).filter((t) => t.interaction_id && !isDone(t.status));
|
|
209
|
+
let pending = inFlight();
|
|
210
|
+
while (pending.length) {
|
|
211
|
+
// A poll that throws keeps the task's old status; the next pass retries it.
|
|
212
|
+
await mapLimit(pending, 4, (t) => refresh(t).catch(() => {}));
|
|
213
|
+
pending = inFlight();
|
|
214
|
+
if (pending.length) await new Promise((r) => setTimeout(r, intervalMs));
|
|
215
|
+
}
|
|
216
|
+
const tasks = store.listTasks({ tag });
|
|
217
|
+
const completed = tasks.filter((t) => isSuccess(t.status)).length;
|
|
218
|
+
const failed = tasks.filter((t) => isDone(t.status) && !isSuccess(t.status)).length;
|
|
219
|
+
emit(json, { tag, completed, failed, total: tasks.length }, () =>
|
|
220
|
+
console.log(dim(`Batch ${tag}: ${completed}/${tasks.length} completed, ${failed} failed.`))
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
program
|
|
225
|
+
.command('batch')
|
|
226
|
+
.argument('<file>', 'prompts file — one per line, or "-" to read stdin')
|
|
227
|
+
.option('-m, --model <id>', 'model to use', gemini.DEFAULT_MODEL)
|
|
228
|
+
.option('-s, --system <text>', 'system instruction')
|
|
229
|
+
.option('-t, --tag <tag>', 'tag the whole batch (default: batch-<hex>)')
|
|
230
|
+
.option('--separator <str>', 'split the file on this delimiter line for multi-line prompts')
|
|
231
|
+
.option('-w, --watch', 'submit all, then poll until the whole batch finishes')
|
|
232
|
+
.option('--dry-run', 'parse and list what would be submitted; submit nothing')
|
|
233
|
+
.option('--json', 'machine-readable output')
|
|
234
|
+
.description('submit many background tasks from a file, tagged as one batch')
|
|
235
|
+
.action(async (file, opts) => {
|
|
236
|
+
try {
|
|
237
|
+
const text = file === '-' ? await readStdin() : fs.readFileSync(file, 'utf8');
|
|
238
|
+
const prompts = parsePrompts(text, opts.separator);
|
|
239
|
+
if (!prompts.length) throw new Error(`no prompts found in ${file === '-' ? 'stdin' : file}`);
|
|
240
|
+
// Auto-tag so the batch is collectable as a unit; a user tag wins.
|
|
241
|
+
const tag = opts.tag || `batch-${crypto.randomUUID().slice(0, 6)}`;
|
|
242
|
+
|
|
243
|
+
if (opts.dryRun) {
|
|
244
|
+
emit(opts.json, { tag, dry_run: true, prompts }, () => {
|
|
245
|
+
console.log(`Batch ${tag}: ${prompts.length} prompt(s) would be submitted:`);
|
|
246
|
+
for (const p of prompts) console.log(` ${snippet(p)}`);
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// One failed submit must not sink the batch: mark that task failed and
|
|
252
|
+
// keep going. mapLimit preserves input order, so the report is stable.
|
|
253
|
+
const results = await mapLimit(prompts, 4, async (prompt) => {
|
|
254
|
+
const id = store.createTask({ prompt, model: opts.model, systemInstruction: opts.system, tag });
|
|
255
|
+
try {
|
|
256
|
+
const r = await gemini.submit(prompt, { model: opts.model, systemInstruction: opts.system });
|
|
257
|
+
store.setInteraction(id, r.interactionId, r.status);
|
|
258
|
+
return { id, interaction_id: r.interactionId, status: r.status, prompt };
|
|
259
|
+
} catch (err) {
|
|
260
|
+
store.setStatus(id, 'failed', { error: err.message });
|
|
261
|
+
return { id, interaction_id: null, status: 'failed', prompt, error: err.message };
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
const submitted = results.filter((r) => !r.error);
|
|
265
|
+
const failed = results.filter((r) => r.error);
|
|
266
|
+
|
|
267
|
+
if (opts.watch) {
|
|
268
|
+
// The submit lines are progress, not the answer, so they go to stderr.
|
|
269
|
+
if (!opts.json) {
|
|
270
|
+
console.error(`Batch ${tag}: submitted ${submitted.length} task(s)` + (failed.length ? `, ${failed.length} failed` : '') + '. Watching...');
|
|
271
|
+
}
|
|
272
|
+
await watchBatch(tag, DEFAULT_POLL_MS, opts.json);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
emit(opts.json, { tag, submitted, failed }, () => {
|
|
277
|
+
console.log(`Batch ${tag}: submitted ${submitted.length} task(s)` + (failed.length ? `, ${failed.length} failed` : '') + '.');
|
|
278
|
+
for (const r of results) {
|
|
279
|
+
const status = r.status || PENDING;
|
|
280
|
+
// Pad before colouring: ANSI codes would break the column width.
|
|
281
|
+
const pad = ' '.repeat(Math.max(0, 16 - status.length));
|
|
282
|
+
console.log(`${r.id} ${colorStatus(status)}${pad} ${snippet(r.prompt)}`);
|
|
283
|
+
}
|
|
284
|
+
console.log(dim('\nCollect them:'));
|
|
285
|
+
console.log(dim(' gemcatch daemon --exit-when-idle'));
|
|
286
|
+
console.log(dim(` gemcatch list --tag ${tag} --status completed`));
|
|
287
|
+
});
|
|
288
|
+
} catch (err) {
|
|
289
|
+
die(err);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
171
293
|
// --- status ---------------------------------------------------------------
|
|
172
294
|
|
|
173
295
|
program
|
|
@@ -246,8 +368,7 @@ program
|
|
|
246
368
|
}
|
|
247
369
|
console.log(dim('ID AGE STATUS PROMPT'));
|
|
248
370
|
for (const t of tasks) {
|
|
249
|
-
const
|
|
250
|
-
const snip = prompt.length > 60 ? `${prompt.slice(0, 57)}...` : prompt;
|
|
371
|
+
const snip = snippet(t.prompt);
|
|
251
372
|
const status = t.status || PENDING;
|
|
252
373
|
// Pad before colouring: ANSI codes would break the column width.
|
|
253
374
|
const pad = ' '.repeat(Math.max(0, 16 - status.length));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemcatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Fire-and-forget CLI for Gemini's Interactions API background execution. Submit long-running research prompts, close your laptop, collect results later.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"CHANGELOG.md"
|
|
17
17
|
],
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=
|
|
19
|
+
"node": ">=22"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"test": "node test-offline.js",
|