gemcatch 0.4.0 → 0.5.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 +67 -1
- package/README.md +97 -9
- package/db.js +44 -2
- package/gemini.js +22 -0
- package/index.js +447 -68
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Collaborative planning.** Until now the only agent flow was a blind one-shot
|
|
15
|
+
bet: gemcatch showed a cost band, asked y/N, and fired. The cost band was
|
|
16
|
+
everything you knew before spending, and there was no way to check the agent
|
|
17
|
+
had read the prompt the way you meant it. A 20-line `batch` against
|
|
18
|
+
`deep-research-max` is a $60–$140 command placed sight unseen. The Deep
|
|
19
|
+
Research docs have always documented a second mode for exactly this, and
|
|
20
|
+
gemcatch could not reach it. Now it can:
|
|
21
|
+
|
|
22
|
+
- `--plan` on `research` and `batch` submits with
|
|
23
|
+
`agent_config.collaborative_planning: true`, so **the agent returns a
|
|
24
|
+
research plan instead of a report**. Still `background: true`, still stored,
|
|
25
|
+
still collected by the daemon. The row is stored with `kind='plan'`.
|
|
26
|
+
- `gemcatch get <id>` on a plan prints the plan and then the literal next
|
|
27
|
+
command, `Approve with: gemcatch approve 8f3a1c04 · Refine with: gemcatch
|
|
28
|
+
refine 8f3a1c04 "..."`, on stderr, so `get <plan> > plan.md` still captures
|
|
29
|
+
just the plan.
|
|
30
|
+
- `gemcatch refine <id> "<instruction>"` sends the instruction back with
|
|
31
|
+
`previous_interaction_id` and planning still on, and stores the revised plan
|
|
32
|
+
linked to the one it came from, inheriting its agent and tag.
|
|
33
|
+
- `gemcatch approve <id>` is the turn that commits: `previous_interaction_id`
|
|
34
|
+
with `collaborative_planning: false`, stored as `kind='report'` linked to the
|
|
35
|
+
plan. It shows the cost band and asks, honours `--yes` when stdin is not a
|
|
36
|
+
TTY, and honours `--dry-run`.
|
|
37
|
+
|
|
38
|
+
**Planning is not cheaper.** The docs give one band per task and do not price a
|
|
39
|
+
planning turn separately, so a planning turn is quoted at the same band and the
|
|
40
|
+
line says so: *"the docs price per task and do not price a planning turn
|
|
41
|
+
separately"*. What you get for it is a look at the plan before you commit to
|
|
42
|
+
the research run, not a discount.
|
|
43
|
+
- `gemcatch list` renders a chain indented under its root, in submission order,
|
|
44
|
+
and grows a `KIND` column when a listing contains plans or reports (a store
|
|
45
|
+
with neither keeps the layout it had). `gemcatch export` follows a chain to its
|
|
46
|
+
report and leaves the intermediate plans out unless `--include-plans` is
|
|
47
|
+
passed; the Markdown sections and the JSON rows now name the turn.
|
|
48
|
+
- `gemcatch stats` now tallies plan and report turns, and totals the estimated
|
|
49
|
+
spend across the agent turns that were actually billed, from the same
|
|
50
|
+
published bands the guard quotes before each one. A chain bills per turn, so a
|
|
51
|
+
release that turns one submission into three owes the user a running total. It
|
|
52
|
+
is the documented bands applied to what was submitted, not a reading of your
|
|
53
|
+
bill, and it errs toward telling you rather than flattering you: only runs
|
|
54
|
+
that reached the server are priced (a submit that failed before it left the
|
|
55
|
+
machine is counted as an attempt but costs nothing), and an agent with no
|
|
56
|
+
published band totals to `unknown`, never to `$0.00`. A store with no agent
|
|
57
|
+
runs prints neither line.
|
|
58
|
+
- Additive schema migration: `collaborative_planning`, `previous_interaction_id`,
|
|
59
|
+
`kind` (defaulting to `'task'`) and `parent_id`. A 0.4.0 `tasks.db` upgrades in
|
|
60
|
+
place, keeps every row, and behaves exactly as it did.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- `approve` and `refine` fail before sending anything when the target is not a
|
|
65
|
+
plan, or is a plan that has not completed. A plan whose interaction the free
|
|
66
|
+
tier has already dropped names the retention window (1 day free, 55 days paid)
|
|
67
|
+
instead of sending a `previous_interaction_id` the server would reject. A plan
|
|
68
|
+
that expires *after* it completed locally gets the same explanation rather
|
|
69
|
+
than a bare 404 for an id the user never typed.
|
|
70
|
+
- `--plan` without `--agent` (or with `--model`) is a clean error rather than a
|
|
71
|
+
flag that quietly does nothing: collaborative planning is an agent feature.
|
|
72
|
+
- Approving the same plan twice submits a second run rather than silently
|
|
73
|
+
reusing the first, and `list` shows both under their plan.
|
|
74
|
+
|
|
10
75
|
## [0.4.0] - 2026-08-08
|
|
11
76
|
|
|
12
77
|
### Added
|
|
@@ -204,7 +269,8 @@ seen a task complete, the text is cached locally and survives that expiry — bu
|
|
|
204
269
|
something has to poll inside that window for it to be seen at all, which is what
|
|
205
270
|
`gemcatch daemon` exists to do.
|
|
206
271
|
|
|
207
|
-
[Unreleased]: https://github.com/Booyaka101/gemcatch/compare/v0.
|
|
272
|
+
[Unreleased]: https://github.com/Booyaka101/gemcatch/compare/v0.5.0...HEAD
|
|
273
|
+
[0.5.0]: https://github.com/Booyaka101/gemcatch/compare/v0.4.0...v0.5.0
|
|
208
274
|
[0.4.0]: https://github.com/Booyaka101/gemcatch/compare/v0.3.0...v0.4.0
|
|
209
275
|
[0.3.0]: https://github.com/Booyaka101/gemcatch/compare/v0.2.0...v0.3.0
|
|
210
276
|
[0.2.0]: https://github.com/Booyaka101/gemcatch/compare/v0.1.1...v0.2.0
|
package/README.md
CHANGED
|
@@ -81,8 +81,10 @@ This week in AI: ...
|
|
|
81
81
|
| `gemcatch research "<prompt>"` | Submits with `background: true`, stores the interaction ID, exits immediately. |
|
|
82
82
|
| `gemcatch batch <file>` | Submits many prompts from a file at once, tagged as one collectable batch. |
|
|
83
83
|
| `gemcatch status <id>` | Polls the API and prints the current state. |
|
|
84
|
-
| `gemcatch get <id>` | Prints the full response if complete, otherwise the current status. |
|
|
85
|
-
| `gemcatch
|
|
84
|
+
| `gemcatch get <id>` | Prints the full response if complete, otherwise the current status. On a [plan](#see-the-plan-before-you-pay-for-the-run), the plan plus the approve/refine commands. |
|
|
85
|
+
| `gemcatch refine <id> "<instruction>"` | Sends an instruction back to a plan and returns a revised plan. |
|
|
86
|
+
| `gemcatch approve <id>` | Approves a plan and submits the research run it describes. |
|
|
87
|
+
| `gemcatch list` | All tasks, newest first: id, age, status, prompt. Plan chains are indented under their root. |
|
|
86
88
|
| `gemcatch export` | Concatenates finished results, each under its prompt, to stdout or a file (Markdown or JSON). |
|
|
87
89
|
| `gemcatch digest` | Feeds a tag's completed results through one Gemini call into a single summary. |
|
|
88
90
|
| `gemcatch watch <id>` | Polls until the task finishes, then prints the result. |
|
|
@@ -91,7 +93,7 @@ This week in AI: ...
|
|
|
91
93
|
| `gemcatch cancel <id>` | Asks the API to stop an in-flight task. |
|
|
92
94
|
| `gemcatch rm <ids...>` | Forgets tasks locally. `--remote` deletes them server-side too. |
|
|
93
95
|
| `gemcatch prune` | Drops finished tasks older than `--days` (default 30). |
|
|
94
|
-
| `gemcatch stats` | Where the store lives
|
|
96
|
+
| `gemcatch stats` | Where the store lives, what's in it, and what the agent runs have plausibly cost. |
|
|
95
97
|
|
|
96
98
|
Useful flags:
|
|
97
99
|
|
|
@@ -100,7 +102,8 @@ Useful flags:
|
|
|
100
102
|
| `--json` | most commands | Machine-readable output. |
|
|
101
103
|
| `-m, --model <id>` | `research`, `batch` | Override the model. |
|
|
102
104
|
| `-a, --agent <id>` | `research`, `batch` | Submit to a [research agent](#research-agents) instead of a model. Mutually exclusive with `--model`. |
|
|
103
|
-
| `--
|
|
105
|
+
| `--plan` | `research`, `batch` | Ask the agent for a [research plan](#see-the-plan-before-you-pay-for-the-run) first, to refine and approve. Needs `--agent`. |
|
|
106
|
+
| `--yes` | `research`, `batch`, `refine`, `approve` | Confirm the agent cost without asking. Required for `--agent` when stdin is not a TTY. |
|
|
104
107
|
| `-s, --system <text>` | `research`, `batch` | Set a system instruction. |
|
|
105
108
|
| `-f, --file <path>` | `research` | Read the prompt from a file. |
|
|
106
109
|
| `-t, --tag <tag>` | `research`, `batch`, `list` | Label tasks and filter them. |
|
|
@@ -112,7 +115,8 @@ Useful flags:
|
|
|
112
115
|
| `-n, --limit <n>` | `list` | Cap the rows (non-negative; `0` shows none). |
|
|
113
116
|
| `--format <md\|json>` | `export` | Output format. Default `md`. |
|
|
114
117
|
| `-o, --out <file>` | `export` | Write to a file instead of stdout. |
|
|
115
|
-
| `--
|
|
118
|
+
| `--include-plans` | `export` | Also emit a chain's plan turns, not just its report. |
|
|
119
|
+
| `--dry-run` | `research`, `batch`, `refine`, `approve`, `prune` | Show what would go — including the projected agent spend; submit/delete nothing. |
|
|
116
120
|
| `--raw` | `get` | Dump the raw interaction JSON. |
|
|
117
121
|
|
|
118
122
|
IDs are the first 8 characters of a UUID. Any unique prefix works, so `gemcatch get 8f3a` is fine.
|
|
@@ -187,14 +191,97 @@ The report lands like any other result — final answer only, none of the agent'
|
|
|
187
191
|
|
|
188
192
|
An agent run can also come back `incomplete` — that is what a `max_total_tokens` budget cap produces when the run "safely pauses" — which `gemcatch` treats as terminal, exactly like the API does: the daemon retires it and moves on.
|
|
189
193
|
|
|
194
|
+
### See the plan before you pay for the run
|
|
195
|
+
|
|
196
|
+
A cost band tells you what a run will cost. It tells you nothing about whether the agent understood the question. Add `--plan` and it doesn't start researching: with `agent_config.collaborative_planning: true`, *"the agent returns a research plan instead of a full report"*. You read it, push back on it, and approve it when it's right.
|
|
197
|
+
|
|
198
|
+
```console
|
|
199
|
+
$ gemcatch research "map the EU AI Act high-risk obligations against the UK approach" --agent deep-research --plan -t euuk
|
|
200
|
+
Agent deep-research-preview-04-2026 (planning turn) — estimated $1.00–$3.00 for this task (preview rates, subject to change; the docs price per task and do not price a planning turn separately).
|
|
201
|
+
Submit? [y/N] y
|
|
202
|
+
Plan task d014e21b submitted. Run: gemcatch get d014e21b when ready.
|
|
203
|
+
|
|
204
|
+
$ gemcatch get d014e21b
|
|
205
|
+
Research plan
|
|
206
|
+
|
|
207
|
+
1. Scope the EU AI Act high-risk regime: Annex III use cases, Article 6 classification,
|
|
208
|
+
and the Chapter III obligations (risk management, data governance, logging, human
|
|
209
|
+
oversight, conformity assessment) with their August 2026 / August 2027 dates.
|
|
210
|
+
2. Scope the UK approach: the five cross-sector principles, the regulator-led model
|
|
211
|
+
(ICO, FCA, MHRA, Ofcom), and what is guidance rather than statute.
|
|
212
|
+
3. Build an obligation-by-obligation comparison table: EU requirement, nearest UK
|
|
213
|
+
equivalent, whether it is binding, and who enforces it.
|
|
214
|
+
4. Flag the gaps in both directions and the compliance implications for a firm
|
|
215
|
+
operating in both.
|
|
216
|
+
Approve with: gemcatch approve d014e21b · Refine with: gemcatch refine d014e21b "..."
|
|
217
|
+
|
|
218
|
+
$ gemcatch refine d014e21b "cut the history, and add enforcement penalties on both sides"
|
|
219
|
+
Plan task 2140e699 submitted (refines d014e21b). Run: gemcatch get 2140e699 when ready.
|
|
220
|
+
|
|
221
|
+
$ gemcatch approve 2140e699
|
|
222
|
+
Agent deep-research-preview-04-2026 — estimated $1.00–$3.00 for this task (preview rates, subject to change).
|
|
223
|
+
Submit? [y/N] y
|
|
224
|
+
Task 96e8209b submitted (approves plan 2140e699).
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Every turn is an ordinary background task: stored, polled, and collected by the daemon before the 1-day expiry, same as everything else. `refine` chains as many times as you like; each revision inherits the agent and tag of the plan it came from.
|
|
228
|
+
|
|
229
|
+
**A planning turn is a task, and it is billed as one.** The docs publish one band per task and price no planning turn separately, so `gemcatch` quotes the same band for it and says exactly that on the line. What `--plan` buys you is a look at the plan before you commit to the research run, not a discount. Budget for the plan, each refinement, and the run.
|
|
230
|
+
|
|
231
|
+
Since a chain bills per turn, `gemcatch stats` keeps a running total across all of them:
|
|
232
|
+
|
|
233
|
+
```console
|
|
234
|
+
$ gemcatch stats
|
|
235
|
+
Store: ~/.gemcatch/tasks.db
|
|
236
|
+
Tasks: 3
|
|
237
|
+
completed 3
|
|
238
|
+
Agent runs:
|
|
239
|
+
deep-research-preview-04-2026 3
|
|
240
|
+
Plan chains: 2 plan, 1 report
|
|
241
|
+
Estimated spend: $3.00–$9.00 across 3 billed task(s) (preview rates, subject to change).
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
That's the chain above: one plan, one refinement, one run, three tasks at the same band. It's the published bands applied to what you actually submitted, not a reading of your bill, and it errs toward telling you rather than flattering you:
|
|
245
|
+
|
|
246
|
+
- Only runs that **reached the server** are priced. A submit that failed before it left the machine (bad key, rejected agent id) is still counted under "Agent runs" as an attempt, but it costs nothing and isn't billed.
|
|
247
|
+
- An agent with **no published band** totals to `unknown`, never to `$0.00`. Quoting zero for a run that costs real money is the one thing a spend guard must not do.
|
|
248
|
+
|
|
249
|
+
`list` shows the chain as one thing, and `export` follows it to the report:
|
|
250
|
+
|
|
251
|
+
```console
|
|
252
|
+
$ gemcatch list
|
|
253
|
+
ID AGE STATUS KIND AGENT PROMPT
|
|
254
|
+
d014e21b 22s completed plan deep-research map the EU AI Act high-risk obligations against the UK ap...
|
|
255
|
+
2140e699 12s completed plan deep-research └─ cut the history, and add enforcement penalties on both sides
|
|
256
|
+
96e8209b 7s completed report deep-research └─ map the EU AI Act high-risk obligations against the UK ap...
|
|
257
|
+
|
|
258
|
+
$ gemcatch export --tag euuk -o report.md
|
|
259
|
+
Wrote 1 result(s) to report.md.
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The plans are working notes on the way to the report, so `export` leaves them out; pass `--include-plans` if you want the whole chain in the document. The report is filed under the question that started the chain rather than the one-line approval actually sent to the API, so an exported document reads as research.
|
|
263
|
+
|
|
264
|
+
A few things that will bite otherwise:
|
|
265
|
+
|
|
266
|
+
- `--plan` needs `--agent`. Collaborative planning is an agent feature, so `--plan --model ...` (or `--plan` on its own) is an error, not a no-op.
|
|
267
|
+
- `approve` only works on a plan that has completed. Anything else fails before a request goes out.
|
|
268
|
+
- On the free tier the plan's interaction is dropped after a day. Once that happens the chain cannot be continued. `approve` says so and names the window instead of sending a `previous_interaction_id` the server will reject. Run the daemon, or approve the same day.
|
|
269
|
+
- Approving twice submits twice. There is no dedupe, and both runs show up under the plan in `list`.
|
|
270
|
+
- `--plan` and `--watch` work together: `gemcatch research "..." --agent deep-research --plan -w` waits for the plan and then prints it.
|
|
271
|
+
|
|
190
272
|
The agent recipe, end to end:
|
|
191
273
|
|
|
192
274
|
```bash
|
|
193
|
-
$ gemcatch batch questions.txt --agent deep-research --yes #
|
|
194
|
-
$ gemcatch daemon --exit-when-idle
|
|
195
|
-
$ gemcatch
|
|
275
|
+
$ gemcatch batch questions.txt --agent deep-research --plan --yes # a plan per prompt, N × band quoted
|
|
276
|
+
$ gemcatch daemon --exit-when-idle # catch the plans
|
|
277
|
+
$ gemcatch list --tag batch-1a2b3c # read them, approve the good ones
|
|
278
|
+
$ gemcatch approve 8f3a1c04 --yes
|
|
279
|
+
$ gemcatch daemon --exit-when-idle # catch the reports before the 1-day expiry
|
|
280
|
+
$ gemcatch export --tag batch-1a2b3c -o reports.md # every report, with its sources
|
|
196
281
|
```
|
|
197
282
|
|
|
283
|
+
Drop `--plan` and the first two lines become the 0.4.0 one-shot flow, which still works exactly as it did.
|
|
284
|
+
|
|
198
285
|
## How it works
|
|
199
286
|
|
|
200
287
|
Tasks live in SQLite at `~/.gemcatch/tasks.db` (override with `GEMCATCH_HOME`):
|
|
@@ -202,7 +289,8 @@ Tasks live in SQLite at `~/.gemcatch/tasks.db` (override with `GEMCATCH_HOME`):
|
|
|
202
289
|
```sql
|
|
203
290
|
CREATE TABLE tasks (id TEXT PRIMARY KEY, prompt TEXT, interaction_id TEXT,
|
|
204
291
|
status TEXT DEFAULT 'pending', result TEXT, created_at INTEGER);
|
|
205
|
-
-- plus model, system_instruction, tag, error, usage, updated_at, agent, citations
|
|
292
|
+
-- plus model, system_instruction, tag, error, usage, updated_at, agent, citations,
|
|
293
|
+
-- collaborative_planning, previous_interaction_id, kind, parent_id
|
|
206
294
|
```
|
|
207
295
|
|
|
208
296
|
`research` calls `interactions.create({model, input, background: true})` via [`@google/genai`](https://www.npmjs.com/package/@google/genai) and keeps the returned `id`. The polling commands call `interactions.get(id)` and write the status back. Once a task completes, the text is cached in the `result` column — `gemcatch get` then answers from disk without touching the network.
|
package/db.js
CHANGED
|
@@ -31,6 +31,17 @@ const MIGRATIONS = [
|
|
|
31
31
|
// the JSON array of sources an agent run returned alongside its report.
|
|
32
32
|
['agent', 'TEXT'],
|
|
33
33
|
['citations', 'TEXT'],
|
|
34
|
+
// 0.5.0: collaborative planning. `collaborative_planning` is the agent_config
|
|
35
|
+
// flag the row was submitted with (1 plan turn, 0 report turn, NULL for every
|
|
36
|
+
// run that sent no agent_config at all, including every pre-0.5.0 row);
|
|
37
|
+
// `previous_interaction_id` is the interaction this turn continues;
|
|
38
|
+
// `kind` is 'task' | 'plan' | 'report'; `parent_id` is the local task this
|
|
39
|
+
// turn continues. The DEFAULT backfills 'task' for older rows, so a 0.4.0
|
|
40
|
+
// store keeps behaving exactly as it did.
|
|
41
|
+
['collaborative_planning', 'INTEGER'],
|
|
42
|
+
['previous_interaction_id', 'TEXT'],
|
|
43
|
+
['kind', "TEXT DEFAULT 'task'"],
|
|
44
|
+
['parent_id', 'TEXT'],
|
|
34
45
|
];
|
|
35
46
|
|
|
36
47
|
let _db = null;
|
|
@@ -64,8 +75,10 @@ function createTask(fields) {
|
|
|
64
75
|
const now = Date.now();
|
|
65
76
|
db()
|
|
66
77
|
.prepare(
|
|
67
|
-
'INSERT INTO tasks (id, prompt, status, created_at, updated_at, model, system_instruction, tag, agent
|
|
68
|
-
'
|
|
78
|
+
'INSERT INTO tasks (id, prompt, status, created_at, updated_at, model, system_instruction, tag, agent, ' +
|
|
79
|
+
'kind, parent_id, collaborative_planning, previous_interaction_id) ' +
|
|
80
|
+
'VALUES (@id, @prompt, @status, @now, @now, @model, @system_instruction, @tag, @agent, ' +
|
|
81
|
+
'@kind, @parent_id, @collaborative_planning, @previous_interaction_id)'
|
|
69
82
|
)
|
|
70
83
|
.run({
|
|
71
84
|
id,
|
|
@@ -76,6 +89,12 @@ function createTask(fields) {
|
|
|
76
89
|
system_instruction: t.systemInstruction || null,
|
|
77
90
|
tag: t.tag || null,
|
|
78
91
|
agent: t.agent || null,
|
|
92
|
+
kind: t.kind || 'task',
|
|
93
|
+
parent_id: t.parentId || null,
|
|
94
|
+
// Presence, not truthiness: `false` is the report turn's real flag and
|
|
95
|
+
// must be stored as 0, while a run that sends no agent_config stores NULL.
|
|
96
|
+
collaborative_planning: t.collaborativePlanning === undefined ? null : Number(!!t.collaborativePlanning),
|
|
97
|
+
previous_interaction_id: t.previousInteractionId || null,
|
|
79
98
|
});
|
|
80
99
|
return id;
|
|
81
100
|
}
|
|
@@ -181,6 +200,27 @@ function agentCounts() {
|
|
|
181
200
|
.all();
|
|
182
201
|
}
|
|
183
202
|
|
|
203
|
+
// Agent runs that actually reached the server, keyed by agent. A submit that
|
|
204
|
+
// never got an interaction_id (a bad key, a rejected agent id, a network
|
|
205
|
+
// failure) was never billed, so it must not appear in a spend total -- unlike
|
|
206
|
+
// agentCounts(), which tallies every attempt.
|
|
207
|
+
function billedAgentCounts() {
|
|
208
|
+
return db()
|
|
209
|
+
.prepare(
|
|
210
|
+
'SELECT agent, COUNT(*) AS n FROM tasks WHERE agent IS NOT NULL AND interaction_id IS NOT NULL GROUP BY agent'
|
|
211
|
+
)
|
|
212
|
+
.all();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Plan/report totals for `stats`. Ordinary tasks are not a row here; they are
|
|
216
|
+
// already accounted for in counts(), and a store that has never planned reports
|
|
217
|
+
// nothing at all.
|
|
218
|
+
function kindCounts() {
|
|
219
|
+
return db()
|
|
220
|
+
.prepare("SELECT kind, COUNT(*) AS n FROM tasks WHERE kind IS NOT NULL AND kind != 'task' GROUP BY kind")
|
|
221
|
+
.all();
|
|
222
|
+
}
|
|
223
|
+
|
|
184
224
|
function close() {
|
|
185
225
|
if (_db) _db.close();
|
|
186
226
|
_db = null;
|
|
@@ -200,5 +240,7 @@ module.exports = {
|
|
|
200
240
|
prunableTasks,
|
|
201
241
|
counts,
|
|
202
242
|
agentCounts,
|
|
243
|
+
billedAgentCounts,
|
|
244
|
+
kindCounts,
|
|
203
245
|
close,
|
|
204
246
|
};
|
package/gemini.js
CHANGED
|
@@ -23,6 +23,12 @@ const AGENT_ALIASES = Object.freeze({
|
|
|
23
23
|
'deep-research-max': 'deep-research-max-preview-04-2026',
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
// `agent_config.type` for the config block a collaborative-planning turn sends.
|
|
27
|
+
// Both documented Deep Research agents use the same value, and an unknown
|
|
28
|
+
// pass-through id is assumed to be one too -- collaborative planning is a Deep
|
|
29
|
+
// Research feature, so there is nothing else it could be.
|
|
30
|
+
const AGENT_CONFIG_TYPE = 'deep-research';
|
|
31
|
+
|
|
26
32
|
// Documented per-task price bands, in dollars, keyed by the RESOLVED id.
|
|
27
33
|
// The docs' own hedge applies -- "These figures are estimates based on
|
|
28
34
|
// preview rates and are subject to change" -- so the spend guard quotes
|
|
@@ -360,6 +366,21 @@ async function submit(prompt, opts) {
|
|
|
360
366
|
? { agent: o.agent, input: prompt, background: true }
|
|
361
367
|
: { model: o.model || DEFAULT_MODEL, input: prompt, background: true };
|
|
362
368
|
if (o.systemInstruction) body.system_instruction = o.systemInstruction;
|
|
369
|
+
// collaborative_planning is an `agent_config` field, NOT a top-level one, and
|
|
370
|
+
// the docs send the whole block (type + thinking_summaries) with it. Sent only
|
|
371
|
+
// when a plan turn is involved -- agent_config is optional otherwise, so an
|
|
372
|
+
// ordinary run keeps making exactly the request it always made. Presence, not
|
|
373
|
+
// truthiness: `false` is the approval turn's real value and must reach the API.
|
|
374
|
+
if (o.collaborativePlanning !== undefined) {
|
|
375
|
+
body.agent_config = {
|
|
376
|
+
type: AGENT_CONFIG_TYPE,
|
|
377
|
+
thinking_summaries: 'auto',
|
|
378
|
+
collaborative_planning: !!o.collaborativePlanning,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
// Continues an earlier interaction server-side: the plan is already in that
|
|
382
|
+
// conversation, so this turn sends only what changed.
|
|
383
|
+
if (o.previousInteractionId) body.previous_interaction_id = o.previousInteractionId;
|
|
363
384
|
const r = await call(() => {
|
|
364
385
|
const api = sdkInteractions();
|
|
365
386
|
return api
|
|
@@ -414,6 +435,7 @@ module.exports = {
|
|
|
414
435
|
MAX_RETRIES,
|
|
415
436
|
AGENT_ALIASES,
|
|
416
437
|
AGENT_PRICE_BANDS,
|
|
438
|
+
AGENT_CONFIG_TYPE,
|
|
417
439
|
resolveAgent,
|
|
418
440
|
submit,
|
|
419
441
|
poll,
|
package/index.js
CHANGED
|
@@ -126,6 +126,12 @@ function parseCitations(raw) {
|
|
|
126
126
|
// The bands are quoted with the docs' own hedge ("estimates based on preview
|
|
127
127
|
// rates and subject to change"), never as authoritative.
|
|
128
128
|
|
|
129
|
+
// A planning turn is a task and is billed as one. The docs publish ONE band per
|
|
130
|
+
// task and price no planning turn separately, so it is quoted at the same band
|
|
131
|
+
// and the line says so outright. Planning buys you a look at the plan before you
|
|
132
|
+
// commit to the research run; it does not buy you a discount.
|
|
133
|
+
const PLAN_NOTE = 'the docs price per task and do not price a planning turn separately';
|
|
134
|
+
|
|
129
135
|
function bandText(agentId, count) {
|
|
130
136
|
const band = gemini.AGENT_PRICE_BANDS[agentId];
|
|
131
137
|
if (!band) return 'no published price band for this agent';
|
|
@@ -135,9 +141,51 @@ function bandText(agentId, count) {
|
|
|
135
141
|
: `estimated ${money(band[0])}–${money(band[1])} for this task`;
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
function spendLine(agentId, count) {
|
|
144
|
+
function spendLine(agentId, count, planning) {
|
|
139
145
|
const head = count > 1 ? `${count} prompts × ${agentId}` : `Agent ${agentId}`;
|
|
140
|
-
return `${head} — ${bandText(agentId, count)}`;
|
|
146
|
+
return `${head}${planning ? ' (planning turn)' : ''} — ${bandText(agentId, count)}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The parenthetical after the band. `hedge` is the docs' "preview rates" caveat,
|
|
150
|
+
// which the confirmation carries; --dry-run already reads as a projection.
|
|
151
|
+
function spendNote(planning, hedge) {
|
|
152
|
+
const parts = [];
|
|
153
|
+
if (hedge) parts.push('preview rates, subject to change');
|
|
154
|
+
if (planning) parts.push(PLAN_NOTE);
|
|
155
|
+
return parts.length ? ` (${parts.join('; ')})` : '';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// What the agent runs in the store have plausibly cost, from the same per-task
|
|
159
|
+
// bands the guard quotes before each one. A plan chain bills per turn, so this
|
|
160
|
+
// counts plans and refinements alongside reports -- that is the number worth
|
|
161
|
+
// knowing. Agents with no published band are counted separately rather than
|
|
162
|
+
// silently priced at zero. Null when nothing has been billed at all.
|
|
163
|
+
function estimatedSpend(agentRows) {
|
|
164
|
+
let low = 0;
|
|
165
|
+
let high = 0;
|
|
166
|
+
let tasks = 0;
|
|
167
|
+
let unpriced = 0;
|
|
168
|
+
for (const a of agentRows) {
|
|
169
|
+
const band = gemini.AGENT_PRICE_BANDS[a.agent];
|
|
170
|
+
if (!band) {
|
|
171
|
+
unpriced += a.n;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
low += band[0] * a.n;
|
|
175
|
+
high += band[1] * a.n;
|
|
176
|
+
tasks += a.n;
|
|
177
|
+
}
|
|
178
|
+
if (!tasks && !unpriced) return null;
|
|
179
|
+
// Nothing priced means the total is unknown, NOT zero. Reporting $0.00 for
|
|
180
|
+
// runs that cost real money is the exact dishonesty this guard exists to
|
|
181
|
+
// avoid, so low/high are null and the caller says "unknown" instead.
|
|
182
|
+
if (!tasks) return { low: null, high: null, tasks: 0, unpriced };
|
|
183
|
+
return { low, high, tasks, unpriced };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// One sentence for a --dry-run: the band, the honesty note, and that nothing went.
|
|
187
|
+
function dryRunSpend(agentId, count, planning) {
|
|
188
|
+
return `${spendLine(agentId, count, planning)}${spendNote(planning, false)}. Nothing submitted (--dry-run).`;
|
|
141
189
|
}
|
|
142
190
|
|
|
143
191
|
function askYesNo(question) {
|
|
@@ -154,8 +202,8 @@ function askYesNo(question) {
|
|
|
154
202
|
// Returns only when the submission is confirmed; otherwise it exits (declined)
|
|
155
203
|
// or throws (no way to ask). Runs BEFORE any row is written, so a declined or
|
|
156
204
|
// refused submission leaves the tasks table untouched.
|
|
157
|
-
async function confirmSpend(agentId, count, opts) {
|
|
158
|
-
console.error(`${spendLine(agentId, count)}
|
|
205
|
+
async function confirmSpend(agentId, count, opts, planning) {
|
|
206
|
+
console.error(`${spendLine(agentId, count, planning)}${spendNote(planning, true)}.`);
|
|
159
207
|
if (opts.yes) return;
|
|
160
208
|
// GEMCATCH_ASSUME_TTY lets the offline suite drive the interactive branch
|
|
161
209
|
// through a pipe; real non-TTY callers (cron, CI, scripts) must say --yes.
|
|
@@ -177,7 +225,18 @@ async function confirmSpend(agentId, count, opts) {
|
|
|
177
225
|
// only when the user actually typed it -- commander fills in the default
|
|
178
226
|
// otherwise, and the default must not poison every agent run.
|
|
179
227
|
function resolveAgentOpts(opts, cmd) {
|
|
180
|
-
if (!opts.agent)
|
|
228
|
+
if (!opts.agent) {
|
|
229
|
+
// Collaborative planning is an agent_config field on an agent run. A model
|
|
230
|
+
// run has no plan turn at all, so --plan without --agent is a mistake worth
|
|
231
|
+
// naming rather than a flag that quietly does nothing.
|
|
232
|
+
if (opts.plan) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
'--plan is a research-agent feature: collaborative planning applies to an agent, not a model, ' +
|
|
235
|
+
'and a model run has no plan turn.\n Try: --agent deep-research --plan'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
181
240
|
if (cmd.getOptionValueSource('model') === 'cli') {
|
|
182
241
|
throw new Error(
|
|
183
242
|
'--model and --agent are mutually exclusive: an agent run is submitted with `agent` ' +
|
|
@@ -187,6 +246,170 @@ function resolveAgentOpts(opts, cmd) {
|
|
|
187
246
|
return gemini.resolveAgent(opts.agent);
|
|
188
247
|
}
|
|
189
248
|
|
|
249
|
+
// --- plan chains ----------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
// `collaborative_planning: true` makes the agent return a research plan instead
|
|
252
|
+
// of a report. That plan is a decision point, not a deliverable: you read it,
|
|
253
|
+
// optionally `refine` it, and `approve` it to spend on the research run itself.
|
|
254
|
+
// Each turn is its own task row, linked to the one it continues by parent_id
|
|
255
|
+
// locally and by previous_interaction_id on the wire.
|
|
256
|
+
|
|
257
|
+
// What the approval turn sends as `input`. The plan is already in the
|
|
258
|
+
// conversation via previous_interaction_id, so this turn only has to say yes --
|
|
259
|
+
// the docs' own example sends a one-line confirmation, not the question again.
|
|
260
|
+
const APPROVE_INPUT = 'Plan looks good, proceed with the research.';
|
|
261
|
+
|
|
262
|
+
// The reason refresh() records when a poll 404s. Named because `approve` reads
|
|
263
|
+
// it back to tell an expired plan apart from any other terminal one.
|
|
264
|
+
const EXPIRED_ERROR = 'interaction not found (expired or deleted)';
|
|
265
|
+
|
|
266
|
+
const RETENTION_NOTE =
|
|
267
|
+
'The Interactions API retains interactions for 1 day on the free tier (55 days on paid).';
|
|
268
|
+
|
|
269
|
+
// A finished plan is a decision point, so the next command is spelled out under
|
|
270
|
+
// it. Guidance, so it goes to stderr like the rest -- `gemcatch get <plan> >
|
|
271
|
+
// plan.md` still captures only the plan.
|
|
272
|
+
function planFooter(task) {
|
|
273
|
+
return `Approve with: gemcatch approve ${task.id} · Refine with: gemcatch refine ${task.id} "..."`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The result payload for `get`/`watch`. The plan-chain fields ride along only on
|
|
277
|
+
// a plan row, so a model run's --json shape is exactly what it always was.
|
|
278
|
+
function resultPayload(task, status, result, citations) {
|
|
279
|
+
const p = { id: task.id, status, result, citations: citations || null };
|
|
280
|
+
if (task.kind === 'plan') {
|
|
281
|
+
p.kind = 'plan';
|
|
282
|
+
p.approve = `gemcatch approve ${task.id}`;
|
|
283
|
+
p.refine = `gemcatch refine ${task.id} "<instruction>"`;
|
|
284
|
+
}
|
|
285
|
+
return p;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Both continuation commands need the same thing: a plan row that completed and
|
|
289
|
+
// whose interaction the server can still resolve. Anything else exits here,
|
|
290
|
+
// before a row is written or a request is sent.
|
|
291
|
+
function needPlan(id, verb) {
|
|
292
|
+
const task = needTask(id);
|
|
293
|
+
if (task.kind !== 'plan') {
|
|
294
|
+
die(
|
|
295
|
+
new Error(
|
|
296
|
+
`Task ${task.id} is not a plan (kind: ${task.kind || 'task'}), so there is nothing to ${verb}.\n` +
|
|
297
|
+
' Plans come from: gemcatch research "<prompt>" --agent deep-research --plan'
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (task.status === 'incomplete' && task.error === EXPIRED_ERROR) {
|
|
302
|
+
die(
|
|
303
|
+
new Error(
|
|
304
|
+
`Plan ${task.id}'s interaction was dropped server-side, so it can no longer be continued.\n` +
|
|
305
|
+
` ${RETENTION_NOTE}\n` +
|
|
306
|
+
` Submit a fresh plan: gemcatch research "<prompt>" --agent ${task.agent || '<agent>'} --plan`
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
// A plan that finished any way other than `completed` is never coming back, so
|
|
311
|
+
// "wait for it" would be wrong advice. Only a plan still in flight gets
|
|
312
|
+
// pointed at `watch`.
|
|
313
|
+
if (isDone(task.status) && !isSuccess(task.status)) {
|
|
314
|
+
die(
|
|
315
|
+
new Error(
|
|
316
|
+
`Plan ${task.id} ended ${task.status}, so there is nothing to ${verb}.\n` +
|
|
317
|
+
` Submit a fresh plan: gemcatch research "<prompt>" --agent ${task.agent || '<agent>'} --plan`
|
|
318
|
+
)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (!isSuccess(task.status)) {
|
|
322
|
+
die(
|
|
323
|
+
new Error(
|
|
324
|
+
`Plan ${task.id} has not completed yet (status: ${task.status}), so there is nothing to ${verb}.\n` +
|
|
325
|
+
` Wait for it: gemcatch watch ${task.id}`
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (!task.interaction_id) die(new Error(`Plan ${task.id} was never submitted.`));
|
|
330
|
+
return task;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// A plan can complete locally and still expire server-side before you approve
|
|
334
|
+
// it, in which case the continuation 404s on an id the user never typed. Name
|
|
335
|
+
// the retention window instead of passing that through raw.
|
|
336
|
+
function expiredHint(err, plan) {
|
|
337
|
+
if (!err || err.httpStatus !== 404) return err;
|
|
338
|
+
const e = new Error(
|
|
339
|
+
`the plan's interaction (${plan.interaction_id}) is gone server-side, so it cannot be continued.\n` +
|
|
340
|
+
` ${RETENTION_NOTE}\n` +
|
|
341
|
+
` Submit a fresh plan: gemcatch research "<prompt>" --agent ${plan.agent || '<agent>'} --plan`
|
|
342
|
+
);
|
|
343
|
+
e.code = 'API_ERROR';
|
|
344
|
+
e.httpStatus = 404;
|
|
345
|
+
return e;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// A report row is displayed under the question that started the chain rather
|
|
349
|
+
// than the "plan looks good" line actually sent -- that is what keeps `list` and
|
|
350
|
+
// `export` reading as research instead of as protocol chatter.
|
|
351
|
+
function rootPrompt(task) {
|
|
352
|
+
let cur = task;
|
|
353
|
+
const seen = new Set([task.id]);
|
|
354
|
+
while (cur.parent_id && !seen.has(cur.parent_id)) {
|
|
355
|
+
seen.add(cur.parent_id);
|
|
356
|
+
const parent = store.getTask(cur.parent_id);
|
|
357
|
+
if (!parent) break;
|
|
358
|
+
cur = parent;
|
|
359
|
+
}
|
|
360
|
+
return cur.prompt;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// `refine` (another plan) and `approve` (the report) are the same submission --
|
|
364
|
+
// same agent, same tag, linked to the plan by previous_interaction_id --
|
|
365
|
+
// differing only in the collaborative_planning flag they send, the kind they
|
|
366
|
+
// store and the line they print. So they share one path, and the spend guard is
|
|
367
|
+
// on it exactly once.
|
|
368
|
+
async function continuePlan(plan, opts, turn) {
|
|
369
|
+
let id;
|
|
370
|
+
try {
|
|
371
|
+
if (opts.dryRun) {
|
|
372
|
+
emit(
|
|
373
|
+
opts.json,
|
|
374
|
+
{
|
|
375
|
+
dry_run: true,
|
|
376
|
+
agent: plan.agent,
|
|
377
|
+
kind: turn.kind,
|
|
378
|
+
parent_id: plan.id,
|
|
379
|
+
previous_interaction_id: plan.interaction_id,
|
|
380
|
+
input: turn.input,
|
|
381
|
+
},
|
|
382
|
+
() => console.log(dryRunSpend(plan.agent, 1, turn.planning))
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
await confirmSpend(plan.agent, 1, opts, turn.planning);
|
|
387
|
+
id = store.createTask({
|
|
388
|
+
prompt: turn.prompt,
|
|
389
|
+
agent: plan.agent,
|
|
390
|
+
tag: plan.tag,
|
|
391
|
+
kind: turn.kind,
|
|
392
|
+
parentId: plan.id,
|
|
393
|
+
collaborativePlanning: turn.planning,
|
|
394
|
+
previousInteractionId: plan.interaction_id,
|
|
395
|
+
});
|
|
396
|
+
const r = await gemini.submit(turn.input, {
|
|
397
|
+
agent: plan.agent,
|
|
398
|
+
collaborativePlanning: turn.planning,
|
|
399
|
+
previousInteractionId: plan.interaction_id,
|
|
400
|
+
});
|
|
401
|
+
store.setInteraction(id, r.interactionId, r.status);
|
|
402
|
+
emit(
|
|
403
|
+
opts.json,
|
|
404
|
+
{ id, interaction_id: r.interactionId, status: r.status, kind: turn.kind, parent_id: plan.id },
|
|
405
|
+
() => console.log(turn.line(id))
|
|
406
|
+
);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
markSubmitFailure(id, err);
|
|
409
|
+
die(expiredHint(err, plan));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
190
413
|
// --- input ----------------------------------------------------------------
|
|
191
414
|
|
|
192
415
|
function readStdin() {
|
|
@@ -230,7 +453,7 @@ async function refresh(task) {
|
|
|
230
453
|
// the end of time. Any other error (5xx, network) is transient and is
|
|
231
454
|
// re-thrown for the caller to retry on its next pass.
|
|
232
455
|
if (err && err.httpStatus === 404) {
|
|
233
|
-
store.setStatus(task.id, 'incomplete', { error:
|
|
456
|
+
store.setStatus(task.id, 'incomplete', { error: EXPIRED_ERROR });
|
|
234
457
|
return { status: 'incomplete', text: null, usage: null, raw: null };
|
|
235
458
|
}
|
|
236
459
|
throw err;
|
|
@@ -249,6 +472,18 @@ async function refresh(task) {
|
|
|
249
472
|
return r;
|
|
250
473
|
}
|
|
251
474
|
|
|
475
|
+
// Only a failed *submit* should mark a task failed. Once it has an
|
|
476
|
+
// interaction_id it is live on the server, and a later watch/poll error must
|
|
477
|
+
// never overwrite it to failed -- that would drop it from the active set and the
|
|
478
|
+
// daemon would abandon a task whose result is still coming. Leave it active; the
|
|
479
|
+
// daemon (or a later `get`) collects it. Every command that submits calls this
|
|
480
|
+
// on its way out, so the rule cannot drift between them.
|
|
481
|
+
function markSubmitFailure(id, err) {
|
|
482
|
+
if (!id) return;
|
|
483
|
+
const t = store.getTask(id);
|
|
484
|
+
if (!t || !t.interaction_id) store.setStatus(id, 'failed', { error: err.message });
|
|
485
|
+
}
|
|
486
|
+
|
|
252
487
|
// Bounds how many polls are open at once. The *rate* limit is enforced in
|
|
253
488
|
// gemini.js (GEMCATCH_RPM), which is the part that keeps a wide fan-out inside the
|
|
254
489
|
// free tier's requests-per-minute allowance.
|
|
@@ -280,6 +515,7 @@ program
|
|
|
280
515
|
.option('-f, --file <path>', 'read the prompt from a file')
|
|
281
516
|
.option('-m, --model <id>', 'model to use', gemini.DEFAULT_MODEL)
|
|
282
517
|
.option('-a, --agent <id>', 'submit to a research agent instead of a model (e.g. deep-research)')
|
|
518
|
+
.option('--plan', 'ask the agent for a research plan first, to refine and approve (needs --agent)')
|
|
283
519
|
.option('-s, --system <text>', 'system instruction')
|
|
284
520
|
.option('-t, --tag <tag>', 'label for filtering with `gemcatch list --tag`')
|
|
285
521
|
.option('-w, --watch', 'wait for the result instead of exiting')
|
|
@@ -292,44 +528,50 @@ program
|
|
|
292
528
|
try {
|
|
293
529
|
const agent = resolveAgentOpts(opts, cmd);
|
|
294
530
|
const prompt = await resolvePrompt(promptArg, opts);
|
|
531
|
+
// undefined, not false: an ordinary run must keep sending no agent_config
|
|
532
|
+
// at all, exactly as it did before collaborative planning existed.
|
|
533
|
+
const planning = opts.plan ? true : undefined;
|
|
295
534
|
if (opts.dryRun) {
|
|
296
|
-
emit(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
535
|
+
emit(
|
|
536
|
+
opts.json,
|
|
537
|
+
{ dry_run: true, agent: agent || null, model: agent ? null : opts.model, plan: !!opts.plan, prompt },
|
|
538
|
+
() => {
|
|
539
|
+
if (agent) console.log(dryRunSpend(agent, 1, opts.plan));
|
|
540
|
+
else console.log(`Would submit to ${opts.model}: ${snippet(prompt)}. Nothing submitted (--dry-run).`);
|
|
541
|
+
}
|
|
542
|
+
);
|
|
300
543
|
return;
|
|
301
544
|
}
|
|
302
|
-
if (agent) await confirmSpend(agent, 1, opts);
|
|
545
|
+
if (agent) await confirmSpend(agent, 1, opts, opts.plan);
|
|
303
546
|
id = store.createTask({
|
|
304
547
|
prompt,
|
|
305
548
|
model: agent ? null : opts.model,
|
|
306
549
|
agent,
|
|
307
550
|
systemInstruction: opts.system,
|
|
308
551
|
tag: opts.tag,
|
|
552
|
+
kind: opts.plan ? 'plan' : 'task',
|
|
553
|
+
collaborativePlanning: planning,
|
|
554
|
+
});
|
|
555
|
+
const r = await gemini.submit(prompt, {
|
|
556
|
+
model: opts.model,
|
|
557
|
+
agent,
|
|
558
|
+
systemInstruction: opts.system,
|
|
559
|
+
collaborativePlanning: planning,
|
|
309
560
|
});
|
|
310
|
-
const r = await gemini.submit(prompt, { model: opts.model, agent, systemInstruction: opts.system });
|
|
311
561
|
store.setInteraction(id, r.interactionId, r.status);
|
|
312
562
|
if (opts.watch) {
|
|
313
563
|
// Under --watch the submit line is progress, not the answer, so it
|
|
314
564
|
// goes to stderr -- `gemcatch research -w "..." > out.txt` then captures
|
|
315
565
|
// only the result.
|
|
316
|
-
if (!opts.json) console.error(edim(
|
|
566
|
+
if (!opts.json) console.error(edim(`${opts.plan ? 'Plan task' : 'Task'} ${id} submitted.`));
|
|
317
567
|
await watchTask(store.getTask(id), DEFAULT_POLL_MS, opts.json);
|
|
318
568
|
return;
|
|
319
569
|
}
|
|
320
|
-
emit(opts.json, { id, interaction_id: r.interactionId, status: r.status }, () =>
|
|
321
|
-
console.log(
|
|
570
|
+
emit(opts.json, { id, interaction_id: r.interactionId, status: r.status, kind: opts.plan ? 'plan' : 'task' }, () =>
|
|
571
|
+
console.log(`${opts.plan ? 'Plan task' : 'Task'} ${id} submitted. Run: gemcatch get ${id} when ready.`)
|
|
322
572
|
);
|
|
323
573
|
} catch (err) {
|
|
324
|
-
|
|
325
|
-
// interaction_id it is live on the server, and a later watch/poll error
|
|
326
|
-
// must never overwrite it to failed -- that would drop it from the active
|
|
327
|
-
// set and the daemon would abandon a task whose result is still coming.
|
|
328
|
-
// Leave it active; the daemon (or a later `get`) collects it.
|
|
329
|
-
if (id) {
|
|
330
|
-
const t = store.getTask(id);
|
|
331
|
-
if (!t || !t.interaction_id) store.setStatus(id, 'failed', { error: err.message });
|
|
332
|
-
}
|
|
574
|
+
markSubmitFailure(id, err);
|
|
333
575
|
die(err);
|
|
334
576
|
}
|
|
335
577
|
});
|
|
@@ -412,6 +654,7 @@ program
|
|
|
412
654
|
.argument('<file>', 'prompts file — one per line, or "-" to read stdin')
|
|
413
655
|
.option('-m, --model <id>', 'model to use', gemini.DEFAULT_MODEL)
|
|
414
656
|
.option('-a, --agent <id>', 'submit every prompt to a research agent instead of a model')
|
|
657
|
+
.option('--plan', 'ask the agent for a research plan per prompt, to refine and approve (needs --agent)')
|
|
415
658
|
.option('-s, --system <text>', 'system instruction')
|
|
416
659
|
.option('-t, --tag <tag>', 'tag the whole batch (default: batch-<hex>)')
|
|
417
660
|
.option('--separator <str>', 'split the file on this delimiter line for multi-line prompts')
|
|
@@ -434,11 +677,14 @@ program
|
|
|
434
677
|
// Auto-tag so the batch is collectable as a unit; a user tag wins.
|
|
435
678
|
const tag = opts.tag || `batch-${crypto.randomUUID().slice(0, 6)}`;
|
|
436
679
|
|
|
680
|
+
// undefined, not false: an ordinary run must keep sending no agent_config.
|
|
681
|
+
const planning = opts.plan ? true : undefined;
|
|
682
|
+
|
|
437
683
|
if (opts.dryRun) {
|
|
438
|
-
emit(opts.json, { tag, dry_run: true, agent: agent || null, prompts }, () => {
|
|
684
|
+
emit(opts.json, { tag, dry_run: true, agent: agent || null, plan: !!opts.plan, prompts }, () => {
|
|
439
685
|
if (agent) {
|
|
440
686
|
// The whole point of the guard: N × the per-task band, up front.
|
|
441
|
-
console.log(
|
|
687
|
+
console.log(dryRunSpend(agent, prompts.length, opts.plan));
|
|
442
688
|
} else {
|
|
443
689
|
console.log(`Batch ${tag}: ${prompts.length} prompt(s) would be submitted:`);
|
|
444
690
|
for (const p of prompts) console.log(` ${snippet(p)}`);
|
|
@@ -449,14 +695,27 @@ program
|
|
|
449
695
|
|
|
450
696
|
// An agent batch multiplies a per-task dollar band by the whole file, so
|
|
451
697
|
// it is confirmed as one total before a single row is written.
|
|
452
|
-
if (agent) await confirmSpend(agent, prompts.length, opts);
|
|
698
|
+
if (agent) await confirmSpend(agent, prompts.length, opts, opts.plan);
|
|
453
699
|
|
|
454
700
|
// One failed submit must not sink the batch: mark that task failed and
|
|
455
701
|
// keep going. mapLimit preserves input order, so the report is stable.
|
|
456
702
|
const results = await mapLimit(prompts, 4, async (prompt) => {
|
|
457
|
-
const id = store.createTask({
|
|
703
|
+
const id = store.createTask({
|
|
704
|
+
prompt,
|
|
705
|
+
model: agent ? null : opts.model,
|
|
706
|
+
agent,
|
|
707
|
+
systemInstruction: opts.system,
|
|
708
|
+
tag,
|
|
709
|
+
kind: opts.plan ? 'plan' : 'task',
|
|
710
|
+
collaborativePlanning: planning,
|
|
711
|
+
});
|
|
458
712
|
try {
|
|
459
|
-
const r = await gemini.submit(prompt, {
|
|
713
|
+
const r = await gemini.submit(prompt, {
|
|
714
|
+
model: opts.model,
|
|
715
|
+
agent,
|
|
716
|
+
systemInstruction: opts.system,
|
|
717
|
+
collaborativePlanning: planning,
|
|
718
|
+
});
|
|
460
719
|
store.setInteraction(id, r.interactionId, r.status);
|
|
461
720
|
return { id, interaction_id: r.interactionId, status: r.status, prompt };
|
|
462
721
|
} catch (err) {
|
|
@@ -484,9 +743,16 @@ program
|
|
|
484
743
|
const pad = ' '.repeat(Math.max(0, 16 - status.length));
|
|
485
744
|
console.log(`${r.id} ${colorStatus(status)}${pad} ${snippet(r.prompt)}`);
|
|
486
745
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
746
|
+
if (opts.plan) {
|
|
747
|
+
console.log(dim('\nCollect the plans, then approve the ones worth running:'));
|
|
748
|
+
console.log(dim(' gemcatch daemon --exit-when-idle'));
|
|
749
|
+
console.log(dim(` gemcatch list --tag ${tag}`));
|
|
750
|
+
console.log(dim(' gemcatch get <id> # prints the plan and the approve command'));
|
|
751
|
+
} else {
|
|
752
|
+
console.log(dim('\nCollect them:'));
|
|
753
|
+
console.log(dim(' gemcatch daemon --exit-when-idle'));
|
|
754
|
+
console.log(dim(` gemcatch list --tag ${tag} --status completed`));
|
|
755
|
+
}
|
|
490
756
|
});
|
|
491
757
|
} catch (err) {
|
|
492
758
|
die(err);
|
|
@@ -530,17 +796,19 @@ program
|
|
|
530
796
|
// -- re-polling it would 404 after 24h, the very thing we cache to avoid.
|
|
531
797
|
if (isSuccess(task.status) && task.result != null && !opts.raw) {
|
|
532
798
|
const cits = parseCitations(task.citations);
|
|
533
|
-
emit(opts.json,
|
|
534
|
-
console.log(withSources(task.result, cits))
|
|
535
|
-
|
|
799
|
+
emit(opts.json, resultPayload(task, task.status, task.result, cits), () => {
|
|
800
|
+
console.log(withSources(task.result, cits));
|
|
801
|
+
if (task.kind === 'plan') console.error(planFooter(task));
|
|
802
|
+
});
|
|
536
803
|
return;
|
|
537
804
|
}
|
|
538
805
|
const r = await refresh(task);
|
|
539
806
|
if (opts.raw) return console.log(JSON.stringify(r.raw, null, 2));
|
|
540
807
|
if (isSuccess(r.status)) {
|
|
541
|
-
emit(opts.json,
|
|
542
|
-
console.log(withSources(r.text, r.citations))
|
|
543
|
-
|
|
808
|
+
emit(opts.json, resultPayload(task, r.status, r.text, r.citations), () => {
|
|
809
|
+
console.log(withSources(r.text, r.citations));
|
|
810
|
+
if (task.kind === 'plan') console.error(planFooter(task));
|
|
811
|
+
});
|
|
544
812
|
} else if (isDone(r.status)) {
|
|
545
813
|
emit(opts.json, { id: task.id, status: r.status, error: r.text || null }, () =>
|
|
546
814
|
console.log(`Task ${task.id}: ${colorStatus(r.status)}${r.text ? `\n${r.text}` : ''}`)
|
|
@@ -556,8 +824,80 @@ program
|
|
|
556
824
|
}
|
|
557
825
|
});
|
|
558
826
|
|
|
827
|
+
// --- refine / approve -----------------------------------------------------
|
|
828
|
+
|
|
829
|
+
program
|
|
830
|
+
.command('refine')
|
|
831
|
+
.argument('<id>', 'plan task id')
|
|
832
|
+
.argument('<instruction>', 'what the plan should do differently')
|
|
833
|
+
.option('--yes', 'confirm the agent cost without asking (required when stdin is not a TTY)')
|
|
834
|
+
.option('--dry-run', 'show what it would cost; submit nothing')
|
|
835
|
+
.option('--json', 'machine-readable output')
|
|
836
|
+
.description('send an instruction back to a plan and get a revised plan')
|
|
837
|
+
.action(async (id, instruction, opts) => {
|
|
838
|
+
const text = (instruction || '').trim();
|
|
839
|
+
if (!text) {
|
|
840
|
+
return die(new Error('provide an instruction, e.g. gemcatch refine 8f3a1c04 "focus on enforcement dates"'));
|
|
841
|
+
}
|
|
842
|
+
const plan = needPlan(id, 'refine');
|
|
843
|
+
await continuePlan(plan, opts, {
|
|
844
|
+
planning: true,
|
|
845
|
+
kind: 'plan',
|
|
846
|
+
input: text,
|
|
847
|
+
prompt: text,
|
|
848
|
+
line: (newId) => `Plan task ${newId} submitted (refines ${plan.id}). Run: gemcatch get ${newId} when ready.`,
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
program
|
|
853
|
+
.command('approve')
|
|
854
|
+
.argument('<id>', 'plan task id')
|
|
855
|
+
.option('--yes', 'confirm the agent cost without asking (required when stdin is not a TTY)')
|
|
856
|
+
.option('--dry-run', 'show what it would cost; submit nothing')
|
|
857
|
+
.option('--json', 'machine-readable output')
|
|
858
|
+
.description('approve a plan and submit the research run it describes')
|
|
859
|
+
.action(async (id, opts) => {
|
|
860
|
+
const plan = needPlan(id, 'approve');
|
|
861
|
+
await continuePlan(plan, opts, {
|
|
862
|
+
planning: false,
|
|
863
|
+
kind: 'report',
|
|
864
|
+
input: APPROVE_INPUT,
|
|
865
|
+
prompt: rootPrompt(plan),
|
|
866
|
+
line: (newId) => `Task ${newId} submitted (approves plan ${plan.id}).`,
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
|
|
559
870
|
// --- list -----------------------------------------------------------------
|
|
560
871
|
|
|
872
|
+
// A plan chain is one piece of work, so it is listed as one: the root in its
|
|
873
|
+
// normal newest-first position, its continuations indented under it in the order
|
|
874
|
+
// they were submitted. A row whose parent is filtered out of this listing (by
|
|
875
|
+
// --status, --tag or -n) is rendered as its own root rather than dropped.
|
|
876
|
+
function chainOrder(tasks) {
|
|
877
|
+
const present = new Set(tasks.map((t) => t.id));
|
|
878
|
+
const kids = new Map();
|
|
879
|
+
for (const t of tasks) {
|
|
880
|
+
if (!t.parent_id || !present.has(t.parent_id)) continue;
|
|
881
|
+
if (!kids.has(t.parent_id)) kids.set(t.parent_id, []);
|
|
882
|
+
kids.get(t.parent_id).push(t);
|
|
883
|
+
}
|
|
884
|
+
for (const list of kids.values()) list.sort((a, b) => a.created_at - b.created_at);
|
|
885
|
+
const out = [];
|
|
886
|
+
const seen = new Set();
|
|
887
|
+
const walk = (t, depth) => {
|
|
888
|
+
if (seen.has(t.id)) return;
|
|
889
|
+
seen.add(t.id);
|
|
890
|
+
out.push({ task: t, depth });
|
|
891
|
+
for (const c of kids.get(t.id) || []) walk(c, depth + 1);
|
|
892
|
+
};
|
|
893
|
+
for (const t of tasks) if (!t.parent_id || !present.has(t.parent_id)) walk(t, 0);
|
|
894
|
+
// A listing must never lose a row. Nothing the CLI writes can put a cycle in
|
|
895
|
+
// parent_id, but a row that is unreachable from any root would otherwise
|
|
896
|
+
// vanish silently, so anything left over is rendered flat.
|
|
897
|
+
for (const t of tasks) walk(t, 0);
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
900
|
+
|
|
561
901
|
program
|
|
562
902
|
.command('list')
|
|
563
903
|
.alias('ls')
|
|
@@ -578,21 +918,27 @@ program
|
|
|
578
918
|
console.log('No tasks yet. Submit one: gemcatch research "your question"');
|
|
579
919
|
return;
|
|
580
920
|
}
|
|
581
|
-
// The AGENT
|
|
582
|
-
// a pure-model store keeps the compact four-column layout it always
|
|
583
|
-
// Agent ids are shown compact -- the "-preview-MM-YYYY" suffix is
|
|
584
|
-
// noise in a table (the full id is in --json and in stats).
|
|
921
|
+
// The AGENT and KIND columns only appear when something in the listing uses
|
|
922
|
+
// them, so a pure-model store keeps the compact four-column layout it always
|
|
923
|
+
// had. Agent ids are shown compact -- the "-preview-MM-YYYY" suffix is
|
|
924
|
+
// version noise in a table (the full id is in --json and in stats).
|
|
585
925
|
const showAgent = tasks.some((t) => t.agent);
|
|
926
|
+
const showKind = tasks.some((t) => t.kind && t.kind !== 'task');
|
|
586
927
|
const shortAgent = (a) => (a ? a.replace(/-preview-\d{2}-\d{4}$/, '') : '-');
|
|
587
|
-
console.log(
|
|
588
|
-
|
|
589
|
-
|
|
928
|
+
console.log(
|
|
929
|
+
dim(`ID AGE STATUS ${showKind ? 'KIND ' : ''}${showAgent ? 'AGENT ' : ''}PROMPT`)
|
|
930
|
+
);
|
|
931
|
+
for (const { task: t, depth } of chainOrder(tasks)) {
|
|
590
932
|
const status = t.status || PENDING;
|
|
591
933
|
// Pad before colouring: ANSI codes would break the column width.
|
|
592
934
|
const pad = ' '.repeat(Math.max(0, 16 - status.length));
|
|
935
|
+
const kindCol = showKind ? `${(t.kind || 'task').padEnd(7)} ` : '';
|
|
593
936
|
const agentCol = showAgent ? `${shortAgent(t.agent).padEnd(18)} ` : '';
|
|
937
|
+
// Indent the prompt, not the id: the fixed-width columns stay aligned and
|
|
938
|
+
// the chain still reads as one thing.
|
|
939
|
+
const branch = depth ? `${' '.repeat(depth - 1)}└─ ` : '';
|
|
594
940
|
console.log(
|
|
595
|
-
`${t.id} ${age(t.created_at).padEnd(4)} ${colorStatus(status)}${pad} ${agentCol}${
|
|
941
|
+
`${t.id} ${age(t.created_at).padEnd(4)} ${colorStatus(status)}${pad} ${kindCol}${agentCol}${branch}${snippet(t.prompt)}`
|
|
596
942
|
);
|
|
597
943
|
}
|
|
598
944
|
});
|
|
@@ -609,6 +955,7 @@ program
|
|
|
609
955
|
.addOption(new Option('--status <status>', 'only this status').choices(ALL_STATUSES).default('completed'))
|
|
610
956
|
.addOption(new Option('--format <fmt>', 'output format').choices(['md', 'json']).default('md'))
|
|
611
957
|
.option('-o, --out <file>', 'write to a file instead of stdout')
|
|
958
|
+
.option('--include-plans', 'also export the plan turns of a chain, not just its report')
|
|
612
959
|
.description('concatenate finished results, each under its prompt, to stdout or a file')
|
|
613
960
|
.action((opts) => {
|
|
614
961
|
const tasks = store.listTasks({ tag: opts.tag, status: opts.status });
|
|
@@ -617,11 +964,19 @@ program
|
|
|
617
964
|
tasks.reverse();
|
|
618
965
|
// Only rows that actually carry a result are worth exporting: a status
|
|
619
966
|
// filter other than `completed` can match tasks that never stored text.
|
|
620
|
-
|
|
967
|
+
// A chain's plan turns are working notes on the way to its report, so an
|
|
968
|
+
// export of a tag follows the chain to the report and leaves them out
|
|
969
|
+
// unless they were asked for.
|
|
970
|
+
const rows = tasks.filter((t) => t.result != null && (opts.includePlans || t.kind !== 'plan'));
|
|
621
971
|
if (!rows.length) {
|
|
622
972
|
// Nothing to write isn't an error, but say why so an empty -o file (or an
|
|
623
973
|
// empty pipe) isn't a mystery. The note goes to stderr, never the output.
|
|
624
974
|
console.error(`No ${opts.status} results to export${opts.tag ? ` for tag '${opts.tag}'` : ''}.`);
|
|
975
|
+
// A chain with no approved run yet has plans and nothing else, which would
|
|
976
|
+
// otherwise read as "there is nothing here".
|
|
977
|
+
if (!opts.includePlans && tasks.some((t) => t.result != null && t.kind === 'plan')) {
|
|
978
|
+
console.error(' Only plan turns matched. Approve one (gemcatch approve <id>), or pass --include-plans.');
|
|
979
|
+
}
|
|
625
980
|
return;
|
|
626
981
|
}
|
|
627
982
|
|
|
@@ -632,6 +987,7 @@ program
|
|
|
632
987
|
id: t.id,
|
|
633
988
|
tag: t.tag,
|
|
634
989
|
status: t.status,
|
|
990
|
+
kind: t.kind || 'task',
|
|
635
991
|
prompt: t.prompt,
|
|
636
992
|
result: t.result,
|
|
637
993
|
created_at: t.created_at,
|
|
@@ -645,7 +1001,8 @@ program
|
|
|
645
1001
|
const when = new Date(t.created_at).toISOString().replace('T', ' ').slice(0, 16);
|
|
646
1002
|
const head = (t.prompt || '(no prompt)').replace(/\s+/g, ' ').trim();
|
|
647
1003
|
const body = t.result && t.result.trim() ? t.result : '_(empty result)_';
|
|
648
|
-
|
|
1004
|
+
const kind = t.kind && t.kind !== 'task' ? ` · ${t.kind}` : '';
|
|
1005
|
+
return `## ${head}\n\n\`${t.id}\` · ${t.status}${kind} · ${when} UTC\n\n${body}`;
|
|
649
1006
|
})
|
|
650
1007
|
.join('\n\n---\n\n');
|
|
651
1008
|
}
|
|
@@ -699,11 +1056,7 @@ program
|
|
|
699
1056
|
if (!opts.json) console.error(edim(`Digesting ${done.length} result(s) tagged ${opts.tag} -> task ${id}.`));
|
|
700
1057
|
await watchTask(store.getTask(id), DEFAULT_POLL_MS, opts.json);
|
|
701
1058
|
} catch (err) {
|
|
702
|
-
|
|
703
|
-
if (id) {
|
|
704
|
-
const t = store.getTask(id);
|
|
705
|
-
if (!t || !t.interaction_id) store.setStatus(id, 'failed', { error: err.message });
|
|
706
|
-
}
|
|
1059
|
+
markSubmitFailure(id, err);
|
|
707
1060
|
die(err);
|
|
708
1061
|
}
|
|
709
1062
|
});
|
|
@@ -861,9 +1214,10 @@ async function watchTask(task, intervalMs, json) {
|
|
|
861
1214
|
last = r.status;
|
|
862
1215
|
}
|
|
863
1216
|
if (isSuccess(r.status)) {
|
|
864
|
-
emit(json,
|
|
865
|
-
console.log(withSources(r.text, r.citations))
|
|
866
|
-
|
|
1217
|
+
emit(json, resultPayload(task, r.status, r.text, r.citations), () => {
|
|
1218
|
+
console.log(withSources(r.text, r.citations));
|
|
1219
|
+
if (task.kind === 'plan') console.error(planFooter(task));
|
|
1220
|
+
});
|
|
867
1221
|
return;
|
|
868
1222
|
}
|
|
869
1223
|
if (isDone(r.status)) {
|
|
@@ -891,9 +1245,10 @@ program
|
|
|
891
1245
|
// empty-text completion is served instead of re-polled (and lost at 24h).
|
|
892
1246
|
if (isSuccess(task.status) && task.result != null) {
|
|
893
1247
|
const cits = parseCitations(task.citations);
|
|
894
|
-
emit(opts.json,
|
|
895
|
-
console.log(withSources(task.result, cits))
|
|
896
|
-
|
|
1248
|
+
emit(opts.json, resultPayload(task, task.status, task.result, cits), () => {
|
|
1249
|
+
console.log(withSources(task.result, cits));
|
|
1250
|
+
if (task.kind === 'plan') console.error(planFooter(task));
|
|
1251
|
+
});
|
|
897
1252
|
return;
|
|
898
1253
|
}
|
|
899
1254
|
if (opts.interval != null && (!Number.isFinite(opts.interval) || opts.interval <= 0)) {
|
|
@@ -983,20 +1338,44 @@ program
|
|
|
983
1338
|
program
|
|
984
1339
|
.command('stats')
|
|
985
1340
|
.option('--json', 'machine-readable output')
|
|
986
|
-
.description('where the store lives
|
|
1341
|
+
.description('where the store lives, what is in it, and what the agent runs have plausibly cost')
|
|
987
1342
|
.action((opts) => {
|
|
988
1343
|
const rows = store.counts();
|
|
989
1344
|
const agents = store.agentCounts();
|
|
1345
|
+
const kinds = store.kindCounts();
|
|
990
1346
|
const total = rows.reduce((n, r) => n + r.n, 0);
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1347
|
+
// Priced from the runs that actually reached the server, not from every
|
|
1348
|
+
// attempt: a submit that failed before it left the machine cost nothing.
|
|
1349
|
+
const spend = estimatedSpend(store.billedAgentCounts());
|
|
1350
|
+
emit(
|
|
1351
|
+
opts.json,
|
|
1352
|
+
{ db: store.DB_PATH, total, by_status: rows, by_agent: agents, by_kind: kinds, estimated_spend: spend },
|
|
1353
|
+
() => {
|
|
1354
|
+
console.log(`Store: ${store.DB_PATH}`);
|
|
1355
|
+
console.log(`Tasks: ${total}`);
|
|
1356
|
+
for (const r of rows) console.log(` ${colorStatus(r.status).padEnd(useColor ? 26 : 17)} ${r.n}`);
|
|
1357
|
+
if (agents.length) {
|
|
1358
|
+
console.log('Agent runs:');
|
|
1359
|
+
for (const a of agents) console.log(` ${a.agent.padEnd(34)} ${a.n}`);
|
|
1360
|
+
}
|
|
1361
|
+
if (kinds.length) {
|
|
1362
|
+
console.log(`Plan chains: ${kinds.map((k) => `${k.n} ${k.kind}`).join(', ')}`);
|
|
1363
|
+
}
|
|
1364
|
+
if (spend && !spend.tasks) {
|
|
1365
|
+
console.log(
|
|
1366
|
+
`Estimated spend: unknown for ${spend.unpriced} agent task(s) on an agent with no published price band.`
|
|
1367
|
+
);
|
|
1368
|
+
} else if (spend) {
|
|
1369
|
+
const rest = spend.unpriced
|
|
1370
|
+
? `, plus ${spend.unpriced} on an agent with no published band`
|
|
1371
|
+
: '';
|
|
1372
|
+
console.log(
|
|
1373
|
+
`Estimated spend: $${spend.low.toFixed(2)}–$${spend.high.toFixed(2)} across ${spend.tasks} billed task(s)` +
|
|
1374
|
+
` (preview rates, subject to change)${rest}.`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
998
1377
|
}
|
|
999
|
-
|
|
1378
|
+
);
|
|
1000
1379
|
});
|
|
1001
1380
|
|
|
1002
1381
|
// Close the store on the way out so a one-shot command doesn't leave the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemcatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"commander": "^15.0.0"
|
|
52
52
|
},
|
|
53
53
|
"allowScripts": {
|
|
54
|
-
"@google/genai@2.
|
|
54
|
+
"@google/genai@2.16.0": true,
|
|
55
55
|
"protobufjs@7.6.5": true
|
|
56
56
|
}
|
|
57
57
|
}
|