strom-research 1.3.0 → 1.4.1
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 +1 -1
- package/assets/lang/cs.json +2 -1
- package/assets/lang/de.json +2 -1
- package/assets/method/reading.md +8 -1
- package/assets/plugins/gates/README.md +1 -1
- package/dist/agents/files.js +146 -100
- package/dist/agents/global.js +82 -10
- package/dist/agents/launch.js +11 -4
- package/dist/agents/profiles.js +21 -6
- package/dist/cli/registry.js +1 -1
- package/dist/cli/ui.js +2 -1
- package/dist/commands/guide.js +19 -7
- package/dist/commands/meta.js +6 -2
- package/dist/commands/session.js +7 -5
- package/dist/commands/setup.js +5 -1
- package/dist/commands/start.js +4 -3
- package/dist/core/config.js +1 -1
- package/dist/core/install.js +3 -2
- package/dist/core/shortcut.js +1 -4
- package/dist/core/tree.js +1 -1
- package/dist/core/which.js +18 -0
- package/dist/runners/codex.js +14 -5
- package/dist/runners/grok.js +108 -0
- package/dist/runners/index.js +2 -0
- package/dist/runners/opencode.js +8 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Strom Research
|
|
2
2
|
|
|
3
3
|
Research your family's history with an AI agent. `strom` turns an AI agent —
|
|
4
|
-
Claude Code, OpenAI Codex, Google Antigravity or
|
|
4
|
+
Claude Code, OpenAI Codex, Google Antigravity, OpenCode or xAI's Grok — into a careful
|
|
5
5
|
genealogical researcher: the agent searches parish registers and archives, you
|
|
6
6
|
decide, and only what a record proves goes into your family tree. The result is
|
|
7
7
|
a GEDCOM file for the [Strom app](https://stromapp.info) or any other program.
|
package/assets/lang/cs.json
CHANGED
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"ui.run.stop.time": "časový limit",
|
|
172
172
|
"ui.run.stop.empty": "žádné otevřené úkoly",
|
|
173
173
|
"ui.run.stop.problems": "doklady mají problém – strom check",
|
|
174
|
-
"ui.run.stop.denied": "agent nesměl spouštět strom –
|
|
174
|
+
"ui.run.stop.denied": "agent nesměl spouštět strom – nepustila ho jeho oprávnění nebo sandbox; nic dalšího se nespustilo",
|
|
175
175
|
"ui.run.stop.limit": "limit předplatného{resets}",
|
|
176
176
|
"ui.run.stop.resets": " (obnoví se {at})",
|
|
177
177
|
"ui.run.stop.auth": "agent není přihlášený – spusťte jednou {command} a přihlaste se",
|
|
@@ -209,6 +209,7 @@
|
|
|
209
209
|
"ui.doc.yes": "ano",
|
|
210
210
|
"ui.doc.default": "výchozí",
|
|
211
211
|
"ui.doc.noagent": "není nainstalovaný žádný AI agent",
|
|
212
|
+
"ui.doc.agent.other": "žádný, kterého strom zná – strom teď ovládá jiný agent nebo program a výzkum dělá přes strom",
|
|
212
213
|
"ui.doc.knows.yes": "ano, v jakékoli složce",
|
|
213
214
|
"ui.doc.knows.no": "zatím ne: {agents}",
|
|
214
215
|
"ui.doc.app.yes": "používáte ji",
|
package/assets/lang/de.json
CHANGED
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"ui.run.stop.time": "Zeitlimit",
|
|
172
172
|
"ui.run.stop.empty": "keine offenen Aufgaben",
|
|
173
173
|
"ui.run.stop.problems": "die Belege haben Probleme – strom check",
|
|
174
|
-
"ui.run.stop.denied": "der Agent durfte strom nicht starten – seine Berechtigungen
|
|
174
|
+
"ui.run.stop.denied": "der Agent durfte strom nicht starten – seine Berechtigungen oder seine Sandbox ließen es nicht zu; nichts weiter wurde gestartet",
|
|
175
175
|
"ui.run.stop.limit": "Limit des Abonnements{resets}",
|
|
176
176
|
"ui.run.stop.resets": " (erneuert sich {at})",
|
|
177
177
|
"ui.run.stop.auth": "der Agent ist nicht angemeldet – starten Sie einmal {command} und melden Sie sich an",
|
|
@@ -209,6 +209,7 @@
|
|
|
209
209
|
"ui.doc.yes": "ja",
|
|
210
210
|
"ui.doc.default": "Standard",
|
|
211
211
|
"ui.doc.noagent": "kein KI-Agent installiert",
|
|
212
|
+
"ui.doc.agent.other": "keiner, den strom kennt – strom wird gerade von einem anderen Agenten oder Programm bedient, das die Forschung über strom macht",
|
|
212
213
|
"ui.doc.knows.yes": "ja, in jedem Ordner",
|
|
213
214
|
"ui.doc.knows.no": "noch nicht: {agents}",
|
|
214
215
|
"ui.doc.app.yes": "Sie nutzen sie",
|
package/assets/method/reading.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Method: reading scans
|
|
2
2
|
|
|
3
|
+
**Old handwriting is read by your strongest model** — you, or a subagent or
|
|
4
|
+
reader on that same model; never a faster, cheaper or "quick" pass. A misread
|
|
5
|
+
name sends the research to the wrong village. **A name or place the next
|
|
6
|
+
search depends on** (a surname, a birthplace, a parish) is read again on a
|
|
7
|
+
crop at full resolution before it is recorded; what stays unsure is recorded
|
|
8
|
+
as unsure and said so.
|
|
9
|
+
|
|
3
10
|
An image you open stays in your context and is paid for again on every turn.
|
|
4
11
|
Look at as few pixels as the question needs, and write down what you saw at once.
|
|
5
12
|
|
|
@@ -22,7 +29,7 @@ Look at as few pixels as the question needs, and write down what you saw at once
|
|
|
22
29
|
It waits for its readers (often ten minutes or more): run it in the
|
|
23
30
|
foreground and let it finish — your session ends with your turn, and
|
|
24
31
|
whatever is left running in the background ends with it.
|
|
25
|
-
(Your own subagents can read too, in batches of at most twelve — but what they
|
|
32
|
+
(Your own subagents can read too — on your own model, never a faster one — in batches of at most twelve — but what they
|
|
26
33
|
report is lost with your session unless you write it down: record each
|
|
27
34
|
report as it comes back, before you send the next readers.) Only the entries
|
|
28
35
|
that will be cited need your own eyes, at full resolution.
|
|
@@ -61,7 +61,7 @@ Environment variables, besides the user's own:
|
|
|
61
61
|
| `STROM_GATE` | the gate's name |
|
|
62
62
|
| `STROM_TREE` | the family tree's folder |
|
|
63
63
|
| `STROM_LANG` | the research language (cs, de, en…) |
|
|
64
|
-
| `STROM_AGENT` | the agent: claude, codex, antigravity, opencode |
|
|
64
|
+
| `STROM_AGENT` | the agent: claude, codex, antigravity, opencode, grok |
|
|
65
65
|
| `STROM_MODEL` | the main model, when one is set |
|
|
66
66
|
| `STROM_SESSIONS` | sessions this run has had |
|
|
67
67
|
| `STROM_COST_USD` | what this run has cost so far (as the agent reported it) |
|
package/dist/agents/files.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Instruction and permission files for the agents that work in a tree.
|
|
2
|
-
// AGENTS.md is the single source every agent reads (Codex, Antigravity
|
|
3
|
-
// OpenCode natively; CLAUDE.md imports it) and holds nothing
|
|
4
|
-
// each agent's own file adds what only that agent needs.
|
|
5
|
-
// (and OpenCode's opencode.json
|
|
6
|
-
//
|
|
2
|
+
// AGENTS.md is the single source every agent reads (Codex, Antigravity,
|
|
3
|
+
// OpenCode and Grok natively; CLAUDE.md imports it) and holds nothing
|
|
4
|
+
// agent-specific; each agent's own file adds what only that agent needs.
|
|
5
|
+
// .claude/settings.json (and OpenCode's opencode.json, Grok's .grok/config.toml)
|
|
6
|
+
// allow `strom` and deny direct access to the evidence — the first line of
|
|
7
|
+
// defence; the seal is the one that always holds.
|
|
7
8
|
import fs from "node:fs";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { langName } from "../core/lang.js";
|
|
@@ -25,7 +26,10 @@ the researcher; \`strom\` is your only way to read and change the research.
|
|
|
25
26
|
1. Start with \`strom\` — it says where things stand and what to do next.
|
|
26
27
|
\`strom guide\` explains the work; \`strom help <command>\` any command.
|
|
27
28
|
2. Work in sessions: \`strom session start\` gives you the brief for the next
|
|
28
|
-
task; finish with \`strom session close --summary "…" --next "…"\`.
|
|
29
|
+
task; finish with \`strom session close --summary "…" --next "…"\`. The
|
|
30
|
+
next step is strom's queue (\`strom\` names it, \`strom plan\` lists it):
|
|
31
|
+
what the user wants instead becomes a task first — never search outside a
|
|
32
|
+
session and its task.
|
|
29
33
|
3. Never create, edit or delete anything in \`data/\`, \`strom.json\` or \`.git\`,
|
|
30
34
|
never read the files in \`data/\` (strom shows them better and shorter), and
|
|
31
35
|
never run git yourself. strom detects changes and then refuses to write.
|
|
@@ -42,7 +46,7 @@ the researcher; \`strom\` is your only way to read and change the research.
|
|
|
42
46
|
already short, listings take \`--limit\` and \`--page\`, and piped commands may
|
|
43
47
|
be refused by your permissions.
|
|
44
48
|
8. Other agents may work on this tree too (Claude Code, Codex, Antigravity,
|
|
45
|
-
OpenCode — the user's choice). \`strom\` shows who is working now; never take a task
|
|
49
|
+
OpenCode, Grok — the user's choice). \`strom\` shows who is working now; never take a task
|
|
46
50
|
another one has started.
|
|
47
51
|
|
|
48
52
|
## Working with the user
|
|
@@ -130,76 +134,100 @@ export function permissionPath(abs) {
|
|
|
130
134
|
return win ? `//${win[1].toLowerCase()}/${win[2]}` : `/${p}`;
|
|
131
135
|
}
|
|
132
136
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* rules
|
|
137
|
+
* The tree's rules for Claude Code or Grok Build. Grok reads Claude Code's rules too, but an absolute
|
|
138
|
+
* path in Claude Code's words ("//Users/x") is plain text to it: its own file gets the path as it is.
|
|
139
|
+
* File rules are Edit(…) only: Claude Code checks every file-editing tool against them (Write(…) rules
|
|
140
|
+
* match nothing and are reported as a mistake); Grok's Edit covers Write as well.
|
|
136
141
|
*/
|
|
137
|
-
|
|
142
|
+
function treeRules(tree, agent) {
|
|
138
143
|
const settings = new Settings(tree.env, {});
|
|
139
144
|
const shared = settings.shared()?.value;
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
// A rule for each of Claude Code's shells: Bash, and PowerShell (on Windows) — a Bash rule does not cover it.
|
|
147
|
-
const shell = (cmd) => [`Bash(${cmd})`, `PowerShell(${cmd})`];
|
|
145
|
+
const claude = agent === "claude";
|
|
146
|
+
const at = (abs) => (claude ? permissionPath(abs) : abs.replace(/\\/g, "/"));
|
|
147
|
+
const keys = at(configDir(tree.env));
|
|
148
|
+
// Connectors of this tree whose images come through the user's browser: browser tools (Claude in Chrome), for their sites only.
|
|
149
|
+
const sites = claude ? [...new Set(treeBrowserConnectors(tree, shared).flatMap((c) => c.manifest.hosts.map(chromeDomain)))] : [];
|
|
150
|
+
const downloads = at(settings.downloads());
|
|
151
|
+
// A rule for each of Claude Code's shells: Bash, and PowerShell (on Windows) — a Bash rule does not cover it. Grok has one.
|
|
152
|
+
const shell = (cmd) => (claude ? [`Bash(${cmd})`, `PowerShell(${cmd})`] : [`Bash(${cmd})`]);
|
|
148
153
|
return {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
154
|
+
allow: [
|
|
155
|
+
...shell("strom:*"),
|
|
156
|
+
"Read(inputs/**)",
|
|
157
|
+
"Read(output/**)",
|
|
158
|
+
"Read(notes/**)",
|
|
159
|
+
"Read(.strom/views/**)",
|
|
160
|
+
// Files the user drops for the research; scans are seen through views only (strom media view).
|
|
161
|
+
...(shared ? [`Read(${at(path.join(shared, "inbox"))}/**)`] : []),
|
|
162
|
+
"Edit(notes/**)",
|
|
163
|
+
// A connector it builds for an archive the research needs (strom connector new); strom runs it.
|
|
164
|
+
...(shared ? ["Read", "Edit"].map((t) => `${t}(${at(path.join(shared, "plugins", "connectors"))}/**)`) : []),
|
|
165
|
+
"WebSearch",
|
|
166
|
+
"WebFetch",
|
|
167
|
+
...(sites.length ? [...CHROME_ALLOW, ...sites] : []),
|
|
168
|
+
],
|
|
169
|
+
deny: [
|
|
170
|
+
"Read(data/**)",
|
|
171
|
+
// The media store: images are looked at through views, so strom knows what was seen.
|
|
172
|
+
...(shared ? [`Read(${at(path.join(shared, "media"))}/**)`] : []),
|
|
173
|
+
"Edit(data/**)",
|
|
174
|
+
"Edit(strom.json)",
|
|
175
|
+
"Edit(.git/**)",
|
|
176
|
+
...shell("git:*"),
|
|
177
|
+
// A password is typed by the user in their own terminal, and so is installing a plugin; the seal is strom's.
|
|
178
|
+
// (Consents the agent may ask for — strom allow …: strom asks the person in a window.)
|
|
179
|
+
...shell("strom login:*"),
|
|
180
|
+
...shell("strom connector add:*"),
|
|
181
|
+
...shell("strom connector remove:*"),
|
|
182
|
+
...shell("strom seal:*"),
|
|
183
|
+
// The limiter's memory (pace, refusals).
|
|
184
|
+
...(shared ? [`Edit(${at(path.join(shared, "net"))}/**)`] : []),
|
|
185
|
+
// A session ends with its turn: nothing wakes it up later (a live run waited for a wake-up that never came).
|
|
186
|
+
...(claude ? ["ScheduleWakeup", "CronCreate"] : []),
|
|
187
|
+
// The seal keys: an agent that could read them could forge the seal.
|
|
188
|
+
`Read(${keys}/**)`,
|
|
189
|
+
`Edit(${keys}/**)`,
|
|
190
|
+
// What the browser downloads is strom's to take over; the rest of the folder is the user's.
|
|
191
|
+
`Read(${downloads}/**)`,
|
|
192
|
+
...(claude ? CHROME_DENY : []),
|
|
193
|
+
// With full permissions only the deny rules count: what the allow list kept away is kept away here.
|
|
194
|
+
...(settings.agentPermissions() === "full" ? BYPASS_DENY : []),
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Permissions for Claude Code in this tree (paths of this computer). */
|
|
199
|
+
export function claudeSettings(tree) {
|
|
200
|
+
const lead = new Settings(tree.env, {}).models("claude", tree.config).lead;
|
|
201
|
+
return {
|
|
202
|
+
permissions: treeRules(tree, "claude"),
|
|
194
203
|
// The user's model: the desktop app cannot be given one when it opens (the CLI is, with --model).
|
|
195
204
|
...(lead ? { model: lead } : {}),
|
|
196
205
|
};
|
|
197
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Grok Build's rules for this tree (.grok/config.toml, read once the folder is trusted — strom starts it
|
|
209
|
+
* so). It reads .claude/settings.json as well; this file says the same with paths it understands.
|
|
210
|
+
*/
|
|
211
|
+
export function grokConfig(tree) {
|
|
212
|
+
const { allow, deny } = treeRules(tree, "grok");
|
|
213
|
+
// A TOML basic string is written like a JSON one.
|
|
214
|
+
const list = (xs) => `[\n${xs.map((x) => ` ${JSON.stringify(x)},`).join("\n")}\n]`;
|
|
215
|
+
return `# strom: generated (strom agents sync) — Grok Build's rules for this family tree; changed by strom only\n[permission]\nallow = ${list(allow)}\ndeny = ${list(deny)}\n`;
|
|
216
|
+
}
|
|
217
|
+
/** Grok Build reads CLAUDE.md too (for Claude Code's projects): what in it is not for Grok. */
|
|
218
|
+
const GROK_RULES = `# Grok Build in this family tree
|
|
219
|
+
|
|
220
|
+
CLAUDE.md in this folder is written for Claude Code: its way of delegating (the
|
|
221
|
+
Agent tool, the models opus, sonnet and haiku) is not yours. Follow AGENTS.md,
|
|
222
|
+
and read scans yourself as it says under "Reading scans".
|
|
223
|
+
`;
|
|
198
224
|
/** Denied as well when the user lets the agent do everything else (agent.permissions full). */
|
|
199
225
|
export const BYPASS_DENY = [
|
|
200
226
|
// its own permissions and instructions
|
|
201
227
|
"Edit(.claude/**)",
|
|
202
228
|
...["AGENTS.md", "CLAUDE.md"].map((f) => `Edit(${f})`),
|
|
229
|
+
// (Grok Build's own rules and instructions)
|
|
230
|
+
"Edit(.grok/**)",
|
|
203
231
|
// downloads round strom's limiter
|
|
204
232
|
"Bash(curl:*)",
|
|
205
233
|
"Bash(wget:*)",
|
|
@@ -218,47 +246,63 @@ export function opencodeConfig(tree) {
|
|
|
218
246
|
const abs = (p) => p.replace(/\\/g, "/");
|
|
219
247
|
const full = settings.agentPermissions() === "full";
|
|
220
248
|
const users = ["strom login *", "strom seal *", "strom connector add *", "strom connector remove *"];
|
|
249
|
+
// The user's model ("provider/model"): its conversation takes no --model, it reads it here.
|
|
250
|
+
const lead = settings.models("opencode", tree.config).lead;
|
|
251
|
+
const permission = {
|
|
252
|
+
bash: {
|
|
253
|
+
"*": "ask",
|
|
254
|
+
"strom *": "allow",
|
|
255
|
+
...Object.fromEntries(users.map((c) => [c, "deny"])),
|
|
256
|
+
"git *": "deny",
|
|
257
|
+
...(full ? { "curl *": "deny", "wget *": "deny" } : {}),
|
|
258
|
+
},
|
|
259
|
+
read: { "*": "allow", "data/*": "deny", ".git/*": "deny" },
|
|
260
|
+
edit: {
|
|
261
|
+
"*": "ask",
|
|
262
|
+
"notes/*": "allow",
|
|
263
|
+
"data/*": "deny",
|
|
264
|
+
".git/*": "deny",
|
|
265
|
+
"strom.json": "deny",
|
|
266
|
+
// its own rules and instructions
|
|
267
|
+
"opencode.json": "deny",
|
|
268
|
+
...Object.fromEntries(["AGENTS.md", "CLAUDE.md", ".claude/*"].map((f) => [f, "deny"])),
|
|
269
|
+
},
|
|
270
|
+
external_directory: {
|
|
271
|
+
"*": "ask",
|
|
272
|
+
...(shared
|
|
273
|
+
? {
|
|
274
|
+
[`${abs(path.join(shared, "inbox"))}/*`]: "allow",
|
|
275
|
+
[`${abs(path.join(shared, "plugins", "connectors"))}/*`]: "allow",
|
|
276
|
+
// images are looked at through views, so strom knows what was seen; the limiter's memory is strom's
|
|
277
|
+
[`${abs(path.join(shared, "media"))}/*`]: "deny",
|
|
278
|
+
[`${abs(path.join(shared, "net"))}/*`]: "deny",
|
|
279
|
+
}
|
|
280
|
+
: {}),
|
|
281
|
+
// the seal keys, and what the browser downloads (strom's to take over)
|
|
282
|
+
[`${abs(configDir(tree.env))}/*`]: "deny",
|
|
283
|
+
[`${abs(settings.downloads())}/*`]: "deny",
|
|
284
|
+
},
|
|
285
|
+
webfetch: "allow",
|
|
286
|
+
websearch: "allow",
|
|
287
|
+
};
|
|
288
|
+
// What would ask is refused when nobody watches: OpenCode 2 ends a whole headless run at the first question
|
|
289
|
+
// it cannot ask (found live), where a refusal only fails that call.
|
|
290
|
+
const refused = (v) => v === "ask" ? "deny" : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, refused(x)])) : v;
|
|
221
291
|
return {
|
|
222
292
|
$schema: "https://opencode.ai/config.json",
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"
|
|
229
|
-
|
|
230
|
-
},
|
|
231
|
-
read: { "*": "allow", "data/*": "deny", ".git/*": "deny" },
|
|
232
|
-
edit: {
|
|
233
|
-
"*": "ask",
|
|
234
|
-
"notes/*": "allow",
|
|
235
|
-
"data/*": "deny",
|
|
236
|
-
".git/*": "deny",
|
|
237
|
-
"strom.json": "deny",
|
|
238
|
-
// its own rules and instructions
|
|
239
|
-
"opencode.json": "deny",
|
|
240
|
-
...Object.fromEntries(["AGENTS.md", "CLAUDE.md", ".claude/*"].map((f) => [f, "deny"])),
|
|
241
|
-
},
|
|
242
|
-
external_directory: {
|
|
243
|
-
"*": "ask",
|
|
244
|
-
...(shared
|
|
245
|
-
? {
|
|
246
|
-
[`${abs(path.join(shared, "inbox"))}/*`]: "allow",
|
|
247
|
-
[`${abs(path.join(shared, "plugins", "connectors"))}/*`]: "allow",
|
|
248
|
-
// images are looked at through views, so strom knows what was seen; the limiter's memory is strom's
|
|
249
|
-
[`${abs(path.join(shared, "media"))}/*`]: "deny",
|
|
250
|
-
[`${abs(path.join(shared, "net"))}/*`]: "deny",
|
|
251
|
-
}
|
|
252
|
-
: {}),
|
|
253
|
-
// the seal keys, and what the browser downloads (strom's to take over)
|
|
254
|
-
[`${abs(configDir(tree.env))}/*`]: "deny",
|
|
255
|
-
[`${abs(settings.downloads())}/*`]: "deny",
|
|
293
|
+
...(lead ? { model: lead } : {}),
|
|
294
|
+
permission,
|
|
295
|
+
agent: {
|
|
296
|
+
[OPENCODE_RUN_AGENT]: {
|
|
297
|
+
mode: "primary",
|
|
298
|
+
description: "strom run: the agent working alone, nobody to ask — what the rules do not allow is refused",
|
|
299
|
+
permission: refused(permission),
|
|
256
300
|
},
|
|
257
|
-
webfetch: "allow",
|
|
258
|
-
websearch: "allow",
|
|
259
301
|
},
|
|
260
302
|
};
|
|
261
303
|
}
|
|
304
|
+
/** OpenCode's agent for a run nobody watches under ask and auto (opencode run --agent). */
|
|
305
|
+
export const OPENCODE_RUN_AGENT = "strom-run";
|
|
262
306
|
/** Files strom wrote for Gemini CLI (gone: Google ended it for personal accounts) — taken away when they are strom's. */
|
|
263
307
|
const OBSOLETE = [
|
|
264
308
|
["GEMINI.md", (t) => t.startsWith("@./AGENTS.md") && t.includes(MARKER) && t.slice(t.indexOf(MARKER) + MARKER.length).trim() === ""],
|
|
@@ -277,7 +321,7 @@ function withUserPart(file, generated) {
|
|
|
277
321
|
}
|
|
278
322
|
return generated;
|
|
279
323
|
}
|
|
280
|
-
export const AGENT_FILES = ["AGENTS.md", "CLAUDE.md", path.join(".claude", "settings.json"), "opencode.json"];
|
|
324
|
+
export const AGENT_FILES = ["AGENTS.md", "CLAUDE.md", path.join(".claude", "settings.json"), "opencode.json", path.join(".grok", "config.toml"), path.join(".grok", "rules", "strom.md")];
|
|
281
325
|
export function syncAgentFiles(tree) {
|
|
282
326
|
const written = [];
|
|
283
327
|
const write = (rel, content) => {
|
|
@@ -300,6 +344,8 @@ export function syncAgentFiles(tree) {
|
|
|
300
344
|
write("CLAUDE.md", claudeMd(tree));
|
|
301
345
|
write(path.join(".claude", "settings.json"), JSON.stringify(claudeSettings(tree), null, 2) + "\n");
|
|
302
346
|
write("opencode.json", JSON.stringify(opencodeConfig(tree), null, 2) + "\n");
|
|
347
|
+
write(path.join(".grok", "config.toml"), grokConfig(tree));
|
|
348
|
+
write(path.join(".grok", "rules", "strom.md"), GROK_RULES);
|
|
303
349
|
for (const [rel, ours] of OBSOLETE) {
|
|
304
350
|
const file = path.join(tree.root, rel);
|
|
305
351
|
let text;
|
package/dist/agents/global.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// is the user's — a password, a consent — strom itself keeps for the user).
|
|
9
9
|
// OpenCode gets a file of its own named in its global config (a global
|
|
10
10
|
// AGENTS.md of strom's would hide the user's ~/.claude/CLAUDE.md, which
|
|
11
|
-
// OpenCode reads when it has none) and `strom *` allowed there.
|
|
11
|
+
// OpenCode reads when it has none) and `strom *` allowed there. Grok Build
|
|
12
|
+
// gets a skill of its own, like Claude Code, and strom allowed in its
|
|
13
|
+
// config.toml (a marked block; the user's own text around it is kept).
|
|
12
14
|
import fs from "node:fs";
|
|
13
15
|
import path from "node:path";
|
|
14
16
|
import { userHome } from "../core/paths.js";
|
|
@@ -24,8 +26,12 @@ registers, archives, evidence, a family tree the Strom app can import.
|
|
|
24
26
|
Use it whenever the user wants to research their ancestors or family, or
|
|
25
27
|
mentions strom, a family tree or the Strom app.
|
|
26
28
|
|
|
27
|
-
1. Run \`strom\` first
|
|
28
|
-
(\`strom --json\` for the same as data
|
|
29
|
+
1. Run \`strom\` first, and before each step: it says where the research
|
|
30
|
+
stands and what to do next (\`strom --json\` for the same as data). Read
|
|
31
|
+
\`strom guide\` before the first step — the rules and the way to work —
|
|
32
|
+
and work only in strom's sessions on its tasks.
|
|
33
|
+
Run each strom command on its own — no pipes, no \`;\` or \`&&\` chains: you
|
|
34
|
+
may run strom without asking, but not what is chained to it.
|
|
29
35
|
2. Not set up yet: ask the user where to keep the research (suggest
|
|
30
36
|
~/Documents/Strom) and which language they want, then run
|
|
31
37
|
\`strom setup --home "<folder>" --lang <code> --yes\`, then
|
|
@@ -43,10 +49,13 @@ mentions strom, a family tree or the Strom app.
|
|
|
43
49
|
6. When strom needs the user's consent it opens a window on their screen —
|
|
44
50
|
tell them to answer it; you cannot. Exit code 4 means the user must run
|
|
45
51
|
the given command themselves.
|
|
46
|
-
7.
|
|
52
|
+
7. Old handwriting is read by your strongest model — never a faster or
|
|
53
|
+
cheaper model, subagent or pass; a name or place the next search depends
|
|
54
|
+
on is read again on an enlarged crop before it is recorded.
|
|
55
|
+
8. Scans come through strom. An archive the research needs has no
|
|
47
56
|
downloader (connector) yet: build one yourself, unasked
|
|
48
57
|
(\`strom connector new\`), and tell the user in a sentence.
|
|
49
|
-
|
|
58
|
+
9. The Strom app (https://stromapp.info) is strom's companion: a free family
|
|
50
59
|
tree app, no account, the data stay on the user's computer. The research's
|
|
51
60
|
result (\`output/tree-strom.ged\`) opens in it. When the user wants to see
|
|
52
61
|
the tree (or \`strom\` says to offer it — once), suggest it gently — best installed as an app from the browser,
|
|
@@ -66,7 +75,46 @@ function allowRules(agent) {
|
|
|
66
75
|
if (agent === "antigravity")
|
|
67
76
|
return ["command(strom)"];
|
|
68
77
|
const commands = ["strom", ...(installation().launchers ?? [])];
|
|
69
|
-
|
|
78
|
+
// Grok Build: one shell tool (its rules know no PowerShell)
|
|
79
|
+
return commands.flatMap((c) => (agent === "grok" ? [`Bash(${c}:*)`] : [`Bash(${c}:*)`, `PowerShell(${c}:*)`]));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Grok Build's config.toml, strom's lines marked: a [permission] table of strom's when there is none,
|
|
83
|
+
* else strom's allow list in the user's table — when it has no allow list of its own (strom does not
|
|
84
|
+
* rewrite the user's list: Grok then asks before it runs strom, as the user set it).
|
|
85
|
+
*/
|
|
86
|
+
const TOML_BEGIN = "# strom: begin (strom agents install; removed by strom agents uninstall)";
|
|
87
|
+
const TOML_END = "# strom: end";
|
|
88
|
+
function grokWithout(text) {
|
|
89
|
+
const lines = text.split("\n");
|
|
90
|
+
const out = [];
|
|
91
|
+
let inside = false;
|
|
92
|
+
for (const l of lines) {
|
|
93
|
+
if (l.trim() === TOML_BEGIN)
|
|
94
|
+
inside = true;
|
|
95
|
+
else if (l.trim() === TOML_END)
|
|
96
|
+
inside = false;
|
|
97
|
+
else if (!inside)
|
|
98
|
+
out.push(l);
|
|
99
|
+
}
|
|
100
|
+
return out.join("\n");
|
|
101
|
+
}
|
|
102
|
+
function grokWith(text) {
|
|
103
|
+
const rest = grokWithout(text);
|
|
104
|
+
const allow = `allow = [${allowRules("grok").map((r) => JSON.stringify(r)).join(", ")}]`;
|
|
105
|
+
const lines = rest.split("\n");
|
|
106
|
+
const header = lines.findIndex((l) => /^\s*\[permission\]\s*(#.*)?$/.test(l));
|
|
107
|
+
if (header < 0) {
|
|
108
|
+
const body = rest.replace(/\s+$/, "");
|
|
109
|
+
return `${body ? `${body}\n\n` : ""}${TOML_BEGIN}\n[permission]\n${allow}\n${TOML_END}\n`;
|
|
110
|
+
}
|
|
111
|
+
// the user's table: up to the next table
|
|
112
|
+
let end = lines.findIndex((l, i) => i > header && /^\s*\[/.test(l));
|
|
113
|
+
if (end < 0)
|
|
114
|
+
end = lines.length;
|
|
115
|
+
if (lines.slice(header + 1, end).some((l) => /^\s*allow\s*=/.test(l)))
|
|
116
|
+
return undefined;
|
|
117
|
+
return [...lines.slice(0, header + 1), TOML_BEGIN, allow, TOML_END, ...lines.slice(header + 1)].join("\n");
|
|
70
118
|
}
|
|
71
119
|
/** A rule of strom's in Claude Code's or Antigravity's settings, whichever installation wrote it. */
|
|
72
120
|
function isStromRule(agent, rule) {
|
|
@@ -74,6 +122,10 @@ function isStromRule(agent, rule) {
|
|
|
74
122
|
return rule === "command(strom)";
|
|
75
123
|
return /^(?:Bash|PowerShell)\((?:.*[\\/])?strom(?:\.exe|\.cmd)?:\*\)$/.test(rule);
|
|
76
124
|
}
|
|
125
|
+
/** Grok Build's folder (GROK_HOME, else ~/.grok). */
|
|
126
|
+
function grokDir(env) {
|
|
127
|
+
return env.GROK_HOME ?? path.join(userHome(env), ".grok");
|
|
128
|
+
}
|
|
77
129
|
/** OpenCode: the rule that lets it run strom without asking. */
|
|
78
130
|
const OPENCODE_STROM = "strom *";
|
|
79
131
|
/** OpenCode's global config folder (it follows XDG on every system). */
|
|
@@ -101,6 +153,8 @@ export function globalTargets(env) {
|
|
|
101
153
|
{ agent: "antigravity", file: path.join(home, ".gemini", "antigravity-cli", "settings.json"), kind: "allow" },
|
|
102
154
|
{ agent: "opencode", file: path.join(opencodeDir(env), "strom.md"), kind: "own" },
|
|
103
155
|
{ agent: "opencode", file: path.join(opencodeDir(env), "opencode.json"), kind: "allow" },
|
|
156
|
+
{ agent: "grok", file: path.join(grokDir(env), "skills", "strom", "SKILL.md"), kind: "own" },
|
|
157
|
+
{ agent: "grok", file: path.join(grokDir(env), "config.toml"), kind: "allow" },
|
|
104
158
|
];
|
|
105
159
|
}
|
|
106
160
|
const SKILL = `---
|
|
@@ -209,7 +263,13 @@ function removeAllow(t, s) {
|
|
|
209
263
|
export function installGlobal(t) {
|
|
210
264
|
const cur = read(t.file);
|
|
211
265
|
let next;
|
|
212
|
-
if (t.kind === "allow") {
|
|
266
|
+
if (t.kind === "allow" && t.agent === "grok") {
|
|
267
|
+
const with_ = grokWith(cur ?? "");
|
|
268
|
+
if (with_ === undefined)
|
|
269
|
+
return false;
|
|
270
|
+
next = with_;
|
|
271
|
+
}
|
|
272
|
+
else if (t.kind === "allow") {
|
|
213
273
|
const s = settingsOf(cur);
|
|
214
274
|
if (!s || hasAllow(t, s))
|
|
215
275
|
return false;
|
|
@@ -217,7 +277,7 @@ export function installGlobal(t) {
|
|
|
217
277
|
next = JSON.stringify(s, null, 2) + "\n";
|
|
218
278
|
}
|
|
219
279
|
else if (t.kind === "own")
|
|
220
|
-
next = t.agent === "claude" ? SKILL : GLOBAL_TEXT;
|
|
280
|
+
next = t.agent === "claude" || t.agent === "grok" ? SKILL : GLOBAL_TEXT;
|
|
221
281
|
else {
|
|
222
282
|
const rest = cur ? withoutBlock(cur).replace(/\s+$/, "") : "";
|
|
223
283
|
next = `${rest ? `${rest}\n\n` : ""}${BEGIN}\n${GLOBAL_TEXT}${END}\n`;
|
|
@@ -233,6 +293,16 @@ export function uninstallGlobal(t) {
|
|
|
233
293
|
const cur = read(t.file);
|
|
234
294
|
if (cur === undefined)
|
|
235
295
|
return false;
|
|
296
|
+
if (t.kind === "allow" && t.agent === "grok") {
|
|
297
|
+
if (!cur.includes(TOML_BEGIN))
|
|
298
|
+
return false;
|
|
299
|
+
const rest = grokWithout(cur);
|
|
300
|
+
if (rest.trim())
|
|
301
|
+
writeFileAtomic(t.file, rest.replace(/\n{3,}/g, "\n\n").replace(/\s*$/, "\n"));
|
|
302
|
+
else
|
|
303
|
+
fs.rmSync(t.file);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
236
306
|
if (t.kind === "allow") {
|
|
237
307
|
const s = settingsOf(cur);
|
|
238
308
|
// Any of strom's rules, also those another installation of strom wrote.
|
|
@@ -244,8 +314,8 @@ export function uninstallGlobal(t) {
|
|
|
244
314
|
return true;
|
|
245
315
|
}
|
|
246
316
|
if (t.kind === "own") {
|
|
247
|
-
//
|
|
248
|
-
if (t.agent === "claude")
|
|
317
|
+
// A skill is a folder of its own; OpenCode's instructions one file among the user's.
|
|
318
|
+
if (t.agent === "claude" || t.agent === "grok")
|
|
249
319
|
fs.rmSync(path.dirname(t.file), { recursive: true, force: true });
|
|
250
320
|
else
|
|
251
321
|
fs.rmSync(t.file, { force: true });
|
|
@@ -272,6 +342,8 @@ export function isInstalled(t) {
|
|
|
272
342
|
const cur = read(t.file);
|
|
273
343
|
if (cur === undefined)
|
|
274
344
|
return false;
|
|
345
|
+
if (t.kind === "allow" && t.agent === "grok")
|
|
346
|
+
return cur.includes(TOML_BEGIN) && grokWith(cur) === cur;
|
|
275
347
|
if (t.kind === "allow")
|
|
276
348
|
return hasAllow(t, settingsOf(cur) ?? {});
|
|
277
349
|
return t.kind === "own" || cur.includes(BEGIN);
|
package/dist/agents/launch.js
CHANGED
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
// --dangerously-skip-permissions; `strom` is allowed in its settings (strom agents install).
|
|
12
12
|
// OpenCode: ask and auto — the tree's rules (opencode.json), anything else it
|
|
13
13
|
// asks (it has no review of its own); full — --auto, everything the rules do not deny.
|
|
14
|
+
// Grok Build: always --trust (the tree folder: else it reads neither AGENTS.md nor
|
|
15
|
+
// the tree's rules); ask — its own mode; auto — --permission-mode auto (its own
|
|
16
|
+
// review); full — --always-approve (only the deny rules count).
|
|
14
17
|
//
|
|
15
18
|
// Claude Code always gets the tree's allow and deny lists (--settings); Codex
|
|
16
|
-
// works in its sandbox on the tree folder with
|
|
17
|
-
// network on (strom fetches).
|
|
19
|
+
// works in its sandbox on the tree folder with its .git (strom commits) and the
|
|
20
|
+
// shared folder writable and the network on (strom fetches).
|
|
18
21
|
import { claudeArgs } from "../runners/claude.js";
|
|
22
|
+
import { codexWritable } from "../runners/codex.js";
|
|
19
23
|
/** The command line of a conversation with this agent. */
|
|
20
24
|
export function conversationArgs(agent, o) {
|
|
21
25
|
if (agent === "claude")
|
|
@@ -23,14 +27,17 @@ export function conversationArgs(agent, o) {
|
|
|
23
27
|
if (agent === "codex") {
|
|
24
28
|
const args = o.level === "full"
|
|
25
29
|
? ["--dangerously-bypass-approvals-and-sandbox"]
|
|
26
|
-
: [...(o.level === "auto" ? ["--approve-for-me"] : ["--sandbox", "workspace-write", "--ask-for-approval", "on-request"]), "-c", "sandbox_workspace_write.network_access=true", ...(o.
|
|
30
|
+
: [...(o.level === "auto" ? ["--approve-for-me"] : ["--sandbox", "workspace-write", "--ask-for-approval", "on-request"]), "-c", "sandbox_workspace_write.network_access=true", ...codexWritable(o.root, o.shared)];
|
|
27
31
|
return [...args, "--search", ...(o.model ? ["--model", o.model] : []), o.kickoff];
|
|
28
32
|
}
|
|
29
33
|
if (agent === "antigravity") {
|
|
30
34
|
const mode = o.level === "full" ? ["--dangerously-skip-permissions"] : o.level === "auto" ? ["--mode", "accept-edits"] : [];
|
|
31
35
|
return [...mode, ...(o.shared ? ["--add-dir", o.shared] : []), ...(o.model ? ["--model", o.model] : []), "--prompt-interactive", o.kickoff];
|
|
32
36
|
}
|
|
37
|
+
// (its TUI takes no --model: the model is in the tree's opencode.json)
|
|
33
38
|
if (agent === "opencode")
|
|
34
|
-
return [...(o.level === "full" ? ["--auto"] : []),
|
|
39
|
+
return [...(o.level === "full" ? ["--auto"] : []), "--prompt", o.kickoff];
|
|
40
|
+
if (agent === "grok")
|
|
41
|
+
return ["--trust", ...(o.level === "full" ? ["--always-approve"] : o.level === "auto" ? ["--permission-mode", "auto"] : []), ...(o.model ? ["--model", o.model] : []), o.kickoff];
|
|
35
42
|
throw new Error(`no conversation launcher for agent "${agent}"`);
|
|
36
43
|
}
|
package/dist/agents/profiles.js
CHANGED
|
@@ -20,13 +20,17 @@ const DELEGATION_RULES = `- Browsing a book, an index or a range of images ("is
|
|
|
20
20
|
- Delegates never write to the research. You record their findings through
|
|
21
21
|
strom; a negative result of a delegate is recorded as a search "by reader".
|
|
22
22
|
- Only entries that will get a citation need your own eyes.`;
|
|
23
|
-
/** For agents that cannot pick a model for a subagent (Codex, Antigravity, OpenCode): read yourself, in small batches. */
|
|
24
|
-
export const SELF_READING = `## Reading scans (Codex, Antigravity, OpenCode)
|
|
23
|
+
/** For agents that cannot pick a model for a subagent (Codex, Antigravity, OpenCode, Grok): read yourself, in small batches. */
|
|
24
|
+
export const SELF_READING = `## Reading scans (Codex, Antigravity, OpenCode, Grok)
|
|
25
25
|
|
|
26
|
-
Read the images yourself,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
Read the images yourself, with your strongest model — never hand old
|
|
27
|
+
handwriting to a faster or cheaper model, subagent or pass. Read them
|
|
28
|
+
in batches of at most ten: open a batch, write down what it gave (strom
|
|
29
|
+
search add … for what was not found, facts for what was), then open the
|
|
30
|
+
next. Images stay in your context and are paid for on every turn, so never
|
|
31
|
+
keep more than one batch open. A name or place the next search depends on:
|
|
32
|
+
read it again on a crop at full resolution (\`strom media view … --crop\`)
|
|
33
|
+
before you record it.
|
|
30
34
|
`;
|
|
31
35
|
export const PROFILES = {
|
|
32
36
|
claude: {
|
|
@@ -85,6 +89,17 @@ ${DELEGATION_RULES}
|
|
|
85
89
|
models: {},
|
|
86
90
|
instructions: () => SELF_READING,
|
|
87
91
|
},
|
|
92
|
+
// xAI's Grok Build: its subagents take only its own models (no cheaper reader worth the images): it reads itself.
|
|
93
|
+
grok: {
|
|
94
|
+
id: "grok",
|
|
95
|
+
name: "Grok Build",
|
|
96
|
+
command: "grok",
|
|
97
|
+
exit: "/quit",
|
|
98
|
+
url: "https://x.ai/cli",
|
|
99
|
+
delegation: "strom",
|
|
100
|
+
models: {},
|
|
101
|
+
instructions: () => SELF_READING,
|
|
102
|
+
},
|
|
88
103
|
};
|
|
89
104
|
export const DEFAULT_AGENT = "claude";
|
|
90
105
|
export function profile(id) {
|
package/dist/cli/registry.js
CHANGED
|
@@ -22,7 +22,7 @@ export const GLOBAL_OPTIONS = [
|
|
|
22
22
|
{ name: "home", type: "string", value: "<dir>", description: "Strom home folder (default from config)" },
|
|
23
23
|
{ name: "shared", type: "string", value: "<dir>", description: "shared data folder (media, catalog, tools)" },
|
|
24
24
|
{ name: "trees", type: "string", value: "<dir>", description: "folder holding the trees" },
|
|
25
|
-
{ name: "agent", type: "string", value: "<agent>", description: "AI agent: claude, codex, antigravity, opencode (default from config)" },
|
|
25
|
+
{ name: "agent", type: "string", value: "<agent>", description: "AI agent: claude, codex, antigravity, opencode, grok (default from config)" },
|
|
26
26
|
{ name: "yes", type: "boolean", description: "accept suggested defaults, never ask" },
|
|
27
27
|
{ name: "debug", type: "boolean", description: "show technical details on errors" },
|
|
28
28
|
{ name: "version", type: "boolean", description: "print the version of strom" },
|
package/dist/cli/ui.js
CHANGED
|
@@ -154,7 +154,7 @@ export const UI = {
|
|
|
154
154
|
"ui.run.stop.time": "time limit",
|
|
155
155
|
"ui.run.stop.empty": "no open tasks",
|
|
156
156
|
"ui.run.stop.problems": "the evidence has problems — strom check",
|
|
157
|
-
"ui.run.stop.denied": "the agent was not allowed to run strom — its permissions
|
|
157
|
+
"ui.run.stop.denied": "the agent was not allowed to run strom — its permissions or its sandbox kept it away; nothing more was started",
|
|
158
158
|
"ui.run.stop.limit": "subscription limit{resets}",
|
|
159
159
|
"ui.run.stop.resets": " (resets {at})",
|
|
160
160
|
"ui.run.stop.auth": "the agent is not logged in — run {command} once and log in",
|
|
@@ -187,6 +187,7 @@ export const UI = {
|
|
|
187
187
|
"ui.doc.yes": "yes",
|
|
188
188
|
"ui.doc.default": "default",
|
|
189
189
|
"ui.doc.noagent": "no AI agent installed",
|
|
190
|
+
"ui.doc.agent.other": "none strom knows is installed — strom is run by another agent or program now, which does the research through strom",
|
|
190
191
|
"ui.doc.knows.yes": "yes, in any folder",
|
|
191
192
|
"ui.doc.knows.no": "not yet: {agents}",
|
|
192
193
|
"ui.doc.app.yes": "you use it",
|
package/dist/commands/guide.js
CHANGED
|
@@ -32,6 +32,12 @@ HARD RULES
|
|
|
32
32
|
the research needs has none yet: build one first (strom connector new,
|
|
33
33
|
its DISCOVERY.md) and tell the user in a sentence. The user saves images
|
|
34
34
|
by hand only where the archive does not allow automation.
|
|
35
|
+
8. Old handwriting is read by your strongest model — you, or a subagent on
|
|
36
|
+
that same model; never a faster, cheaper or "quick" pass. A name or place
|
|
37
|
+
the next search depends on is read again on a crop at full resolution
|
|
38
|
+
(strom media view … --crop) before it is recorded; unsure stays unsure.
|
|
39
|
+
9. A find is told to the user only once it is recorded (the source with its
|
|
40
|
+
transcript, the facts citing it), and told from strom: strom person card.
|
|
35
41
|
${language}
|
|
36
42
|
|
|
37
43
|
GETTING STARTED (once)
|
|
@@ -42,14 +48,20 @@ GETTING STARTED (once)
|
|
|
42
48
|
strom intake --text "<what the user told you, in their words>"
|
|
43
49
|
strom intake <file or folder> documents, photos, a GEDCOM or Strom tree
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
THE WORK, STEP BY STEP (every agent — also one strom did not start: a
|
|
52
|
+
conversation opened anywhere, a bot on its own server)
|
|
53
|
+
strom where things stand and the next step — run it before each step
|
|
54
|
+
strom session start the next task of the queue (or: strom session start T0003, the one
|
|
55
|
+
the user chose): its brief — the task, what is known, the method
|
|
56
|
+
… work on that task only; record every finding at once …
|
|
48
57
|
strom task done T0001 --result "…" a complete negative search is a result
|
|
49
|
-
strom session close --summary "…" --next "…" (not finished: --continue)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
strom session close --summary "…" --next "…" (not finished: --continue) — it proposes the next tasks
|
|
59
|
+
then tell the user what was found (strom person card …) and what comes next (strom plan), and ask
|
|
60
|
+
before the next session. The next step is strom's queue: what the user wants instead becomes a
|
|
61
|
+
task first (strom task add …, or strom research new …) — never search outside a session and its task.
|
|
62
|
+
In a conversation the next task is best begun in a fresh context (Claude Code: /clear) — suggest
|
|
63
|
+
it to the user; many tasks waiting: the agent working on its own (strom run, the user starts it
|
|
64
|
+
from strom's menu), every task in a fresh session
|
|
53
65
|
|
|
54
66
|
RECORDING
|
|
55
67
|
A record (register entry, certificate) — the source first, then its facts:
|
package/dist/commands/meta.js
CHANGED
|
@@ -20,6 +20,7 @@ import { currentSession, openSessions } from "../core/session.js";
|
|
|
20
20
|
import { readByOtherModels } from "../core/review.js";
|
|
21
21
|
import { liveHolder } from "../core/lock.js";
|
|
22
22
|
import { isAgent } from "../core/which.js";
|
|
23
|
+
import { agentsHere } from "../core/apps.js";
|
|
23
24
|
import { taskQueue, waitingForUser, waitingLines } from "./tasks.js";
|
|
24
25
|
import { liveWorkers, runAlive } from "../core/workers.js";
|
|
25
26
|
import { label } from "../core/people.js";
|
|
@@ -114,10 +115,13 @@ function orientation(ctx) {
|
|
|
114
115
|
const open = currentSession(tree, ctx.env) ?? sessions.find((s) => !busy(s) && !(s.worker && s.worker !== ctx.env.STROM_WORKER && !human));
|
|
115
116
|
const queued = taskQueue(tree).length;
|
|
116
117
|
// An agent outside the tree that strom did not start (it set strom up from the web page, or was
|
|
117
|
-
// opened in some folder): the research goes on in its own conversation, where the tree lives
|
|
118
|
+
// opened in some folder): the research goes on in its own conversation, where the tree lives — when
|
|
119
|
+
// strom can start one here (the research's agent is on this computer; not so for a bot on its own
|
|
120
|
+
// server, e.g. Grok Bot: it goes on where it is).
|
|
118
121
|
const rel = path.relative(tree.root, ctx.cwd);
|
|
119
122
|
const inTree = rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
120
|
-
const
|
|
123
|
+
const chosen = ctx.settings.agent(tree.config).value;
|
|
124
|
+
const elsewhere = isAgent(ctx.env) && !ctx.env.STROM_WORKER && !ctx.env.STROM_SESSION && !inTree && agentsHere(ctx.env).some((a) => a.id === chosen);
|
|
121
125
|
if (base.tree.errors > 0)
|
|
122
126
|
base.next = { why: t("ui.why.check"), command: "strom check" };
|
|
123
127
|
else if (elsewhere)
|
package/dist/commands/session.js
CHANGED
|
@@ -25,7 +25,7 @@ import { syncAgentFiles } from "../agents/files.js";
|
|
|
25
25
|
import { setTreeSetting } from "./setup.js";
|
|
26
26
|
import { RUNNERS } from "../runners/index.js";
|
|
27
27
|
import { PROFILES } from "../agents/profiles.js";
|
|
28
|
-
import { AGENTS, detectAgent, isAgent, which } from "../core/which.js";
|
|
28
|
+
import { AGENTS, detectAgent, isAgent, which, withoutAgentMarks } from "../core/which.js";
|
|
29
29
|
import { askGate, ensureGatesDir, loadGate } from "../core/gate.js";
|
|
30
30
|
import { keepAwake } from "../core/awake.js";
|
|
31
31
|
import { deadlineOf, WRAP_UP_MS } from "../core/clock.js";
|
|
@@ -385,7 +385,7 @@ register({
|
|
|
385
385
|
"the tree's permissions (anything else is denied), or with the terminal (--interactive). Stops at the\n" +
|
|
386
386
|
"subscription limit, when the queue is empty, or at --until (a session already running finishes, within\n" +
|
|
387
387
|
"--minutes). The agent knows when its session is stopped (the brief; strom's output counts down its last\n" +
|
|
388
|
-
"10 minutes, a short session its last quarter); at --minutes it is stopped, and Claude Code, Codex and
|
|
388
|
+
"10 minutes, a short session its last quarter); at --minutes it is stopped, and Claude Code, Codex, OpenCode and Grok\n" +
|
|
389
389
|
"get 5 minutes more to write down what they found and close (an unfinished task goes back to the queue); a task\n" +
|
|
390
390
|
"that comes back twice in a row with nothing recorded is parked. --task picks the tasks (one session each, in that order;\n" +
|
|
391
391
|
"one done or held by another agent meanwhile is left out). --loop: session after session for as long as there\n" +
|
|
@@ -578,7 +578,7 @@ register({
|
|
|
578
578
|
brief.text;
|
|
579
579
|
const kickoff = `You are the researcher in strom session ${session.id}. Run \`strom brief\` and follow it; work only through strom; finish with \`strom session close\`.`;
|
|
580
580
|
const env = {
|
|
581
|
-
...prependPath(runEnv, bin),
|
|
581
|
+
...prependPath(withoutAgentMarks(runEnv), bin),
|
|
582
582
|
STROM_SESSION: session.id,
|
|
583
583
|
STROM_DEADLINE: new Date(deadline).toISOString(),
|
|
584
584
|
STROM_MINUTES: String(minutes),
|
|
@@ -603,7 +603,9 @@ register({
|
|
|
603
603
|
shared: ctx.settings.shared()?.value,
|
|
604
604
|
...(models.lead ? { model: models.lead } : {}),
|
|
605
605
|
...(opts.interactive ? { interactive: true } : {}),
|
|
606
|
-
|
|
606
|
+
// the level for every agent (each maps it to its own switches); the browser and Remote Control are Claude Code's
|
|
607
|
+
permissions,
|
|
608
|
+
...(runnerId === "claude" ? { chrome: browser.length > 0, remote: ctx.settings.agentRemote() } : {}),
|
|
607
609
|
...(extra?.length ? { extraArgs: extra } : {}),
|
|
608
610
|
onProgress: (l) => out(` · ${l}`),
|
|
609
611
|
signal: stop.signal,
|
|
@@ -760,7 +762,7 @@ function costText(m) {
|
|
|
760
762
|
return m?.costUsd !== undefined ? `$${m.costUsd.toFixed(2)}` : "";
|
|
761
763
|
}
|
|
762
764
|
/** How the user clears the agent's context, in the agent's own words. */
|
|
763
|
-
const CLEAR = { claude: "/clear", codex: "/new", opencode: "/new" };
|
|
765
|
+
const CLEAR = { claude: "/clear", codex: "/new", opencode: "/new", grok: "/new" };
|
|
764
766
|
/** The hint after a session in a conversation: the next task in a fresh context (images stay in a context and are paid on every turn). */
|
|
765
767
|
function freshContext(ctx, tree, s) {
|
|
766
768
|
const since = Date.parse(s.started);
|
package/dist/commands/setup.js
CHANGED
|
@@ -176,7 +176,11 @@ function diagnose(ctx) {
|
|
|
176
176
|
const found = here.map((a) => a.id);
|
|
177
177
|
const tree = ctx.hasTree() ? ctx.tree().config : undefined;
|
|
178
178
|
const chosen = ctx.settings.agent(tree).value;
|
|
179
|
-
|
|
179
|
+
// None strom knows, and no person at a terminal: another agent or program runs strom (a bot on its own
|
|
180
|
+
// server, found live: Grok Bot) — it does the research through strom, nothing is missing for it.
|
|
181
|
+
if (!found.length && (isAgent(ctx.env) || !ctx.io.tty))
|
|
182
|
+
add("agent", "ok", t("ui.doc.agent.other"));
|
|
183
|
+
else if (!found.length)
|
|
180
184
|
add("agent", "fail", t("ui.doc.noagent"), `${FIX} (${researchUrl(lang, "agents")})`, "agent");
|
|
181
185
|
else {
|
|
182
186
|
const names = found.map((id) => `${PROFILES[id]?.name ?? id}${id === chosen ? ` (${t("ui.doc.default")})` : ""}`).join(", ");
|
package/dist/commands/start.js
CHANGED
|
@@ -13,7 +13,7 @@ import { PROFILES } from "../agents/profiles.js";
|
|
|
13
13
|
import { conversationArgs } from "../agents/launch.js";
|
|
14
14
|
import { globalTargets, installGlobal, isInstalled, uninstallGlobal } from "../agents/global.js";
|
|
15
15
|
import { syncAgentFiles } from "../agents/files.js";
|
|
16
|
-
import { AGENTS, findAgent, isAgent } from "../core/which.js";
|
|
16
|
+
import { AGENTS, findAgent, isAgent, withoutAgentMarks } from "../core/which.js";
|
|
17
17
|
import { DESKTOP_APPS, agentsHere, whereToTalk } from "../core/apps.js";
|
|
18
18
|
import { assertIntact } from "../core/integrity.js";
|
|
19
19
|
import { treeBrowserConnectors } from "../core/connector.js";
|
|
@@ -63,7 +63,7 @@ register({
|
|
|
63
63
|
"(agent.permissions: ask, auto, full), your model, and asks you what matters. Where you talk (agent.where): the\n" +
|
|
64
64
|
"agent's desktop app when it is installed (Claude, ChatGPT/Codex, OpenCode — the tree folder and the first message\n" +
|
|
65
65
|
"filled in; you confirm the folder and send it), else its CLI in this terminal (Claude Code, Codex, Antigravity,\n" +
|
|
66
|
-
"OpenCode). The first message is yours to give (--say); without it the agent reports where things\n" +
|
|
66
|
+
"OpenCode, Grok). The first message is yours to give (--say); without it the agent reports where things\n" +
|
|
67
67
|
"stand and suggests what next.",
|
|
68
68
|
options: [
|
|
69
69
|
{ name: "say", type: "string", value: "<text>", description: "your first message to the agent" },
|
|
@@ -124,6 +124,7 @@ register({
|
|
|
124
124
|
return { text: ui(lang, opened ? "ui.chat.handover" : "ui.chat.handover.failed", { agent: profile.name }), data: { agent, handover: opened ? "window" : "none", cwd: tree.root } };
|
|
125
125
|
}
|
|
126
126
|
const base = {
|
|
127
|
+
root: tree.root,
|
|
127
128
|
kickoff,
|
|
128
129
|
level,
|
|
129
130
|
model: ctx.settings.models(agent, tree.config).lead,
|
|
@@ -154,7 +155,7 @@ register({
|
|
|
154
155
|
// Claude Code asks once whether the folder is to be trusted — its own safety step, kept; the user knows what to answer.
|
|
155
156
|
if (agent === "claude" && !claudeTrusts(tree.root, ctx.env))
|
|
156
157
|
ctx.io.stdout(ui(lang, "ui.chat.trust") + "\n");
|
|
157
|
-
const env = { ...prependPath(ctx.env, shimDir(tree)), STROM_WORKER: worker, ...(base.model ? { STROM_MODEL: base.model } : {}) };
|
|
158
|
+
const env = { ...prependPath(withoutAgentMarks(ctx.env), shimDir(tree)), STROM_WORKER: worker, ...(base.model ? { STROM_MODEL: base.model } : {}) };
|
|
158
159
|
delete env.STROM_HANDOVER;
|
|
159
160
|
let code;
|
|
160
161
|
try {
|
package/dist/core/config.js
CHANGED
|
@@ -24,7 +24,7 @@ export const SETTINGS = [
|
|
|
24
24
|
{ key: "shared", env: "STROM_SHARED", tree: false, kind: "path", description: "shared data: scans, catalog, tools (default <home>/shared)" },
|
|
25
25
|
{ key: "trees", env: "STROM_TREES", tree: false, kind: "path", description: "folder holding the trees (default <home>)" },
|
|
26
26
|
{ key: "lang", env: "STROM_LANG", tree: true, kind: "lang", description: "research language: the agent talks and writes in it" },
|
|
27
|
-
{ key: "agent", env: "STROM_AGENT", tree: true, kind: "agent", description: "AI agent doing the research: claude, codex, antigravity, opencode" },
|
|
27
|
+
{ key: "agent", env: "STROM_AGENT", tree: true, kind: "agent", description: "AI agent doing the research: claude, codex, antigravity, opencode, grok" },
|
|
28
28
|
...TIERS.map((t) => ({
|
|
29
29
|
key: `model.${t}`,
|
|
30
30
|
env: t === "lead" ? "STROM_MODEL" : `STROM_MODEL_${t.toUpperCase()}`,
|
package/dist/core/install.js
CHANGED
|
@@ -22,7 +22,8 @@ export function runInstaller(inst, env) {
|
|
|
22
22
|
*/
|
|
23
23
|
export function agentBinDirs(env, platform = process.platform) {
|
|
24
24
|
const home = userHome(env);
|
|
25
|
+
// Grok Build's installer: ~/.grok/bin (on PATH through the shell's profile; Windows: added by hand)
|
|
25
26
|
if (platform === "win32")
|
|
26
|
-
return [path.join(home, ".local", "bin"), path.join(env.APPDATA ?? path.join(home, "AppData", "Roaming"), "npm")];
|
|
27
|
-
return [path.join(home, ".local", "bin"), path.join(home, ".claude", "local")];
|
|
27
|
+
return [path.join(home, ".local", "bin"), path.join(env.APPDATA ?? path.join(home, "AppData", "Roaming"), "npm"), path.join(home, ".grok", "bin")];
|
|
28
|
+
return [path.join(home, ".local", "bin"), path.join(home, ".claude", "local"), path.join(home, ".grok", "bin")];
|
|
28
29
|
}
|
package/dist/core/shortcut.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
8
|
import { configDir, desktopDir, userHome } from "./paths.js";
|
|
9
9
|
import { stromLauncher } from "./self.js";
|
|
10
|
+
import { isAgentMark } from "./which.js";
|
|
10
11
|
import { assetPath } from "./assets.js";
|
|
11
12
|
/** Strom Research's icon, as each system takes it. */
|
|
12
13
|
export const ICON = { png: assetPath("icon", "icon-512.png"), ico: assetPath("icon", "icon.ico") };
|
|
@@ -96,10 +97,6 @@ export function createShortcut(name, env, platform = process.platform) {
|
|
|
96
97
|
*/
|
|
97
98
|
/** One handover window at a time: another asked within this long is not opened. */
|
|
98
99
|
export const HANDOVER_GAP_MS = 15_000;
|
|
99
|
-
/** What says an agent runs strom (core/which.ts detectAgent) — not the user's own settings of an agent (…_HOME). */
|
|
100
|
-
function isAgentMark(k) {
|
|
101
|
-
return ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "AI_AGENT", "OPENCODE", "OPENCODE_PID"].includes(k) || (/^(CODEX|ANTIGRAVITY)_/.test(k) && !/_HOME$/.test(k));
|
|
102
|
-
}
|
|
103
100
|
export function openInNewTerminal(argv, cwd, env, platform = process.platform) {
|
|
104
101
|
if (env.STROM_NO_OPEN === "1")
|
|
105
102
|
return false;
|
package/dist/core/tree.js
CHANGED
|
@@ -23,7 +23,7 @@ import * as git from "./git.js";
|
|
|
23
23
|
import { commitSeal, createKey, fingerprint, loadKey, sha256, signOp } from "./seal.js";
|
|
24
24
|
import { stringifyCanonical } from "./json.js";
|
|
25
25
|
export const TREE_FILE = "strom.json";
|
|
26
|
-
export const VERSION = "1.
|
|
26
|
+
export const VERSION = "1.4.1";
|
|
27
27
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
28
28
|
.strom/
|
|
29
29
|
/media/
|
package/dist/core/which.js
CHANGED
|
@@ -26,9 +26,13 @@ export const AGENTS = [
|
|
|
26
26
|
{ id: "codex", command: "codex", name: "OpenAI Codex CLI" },
|
|
27
27
|
{ id: "antigravity", command: "agy", name: "Antigravity CLI" },
|
|
28
28
|
{ id: "opencode", command: "opencode", name: "OpenCode" },
|
|
29
|
+
{ id: "grok", command: "grok", name: "Grok Build" },
|
|
29
30
|
];
|
|
30
31
|
/** Which AI agent's shell tool runs strom? (they set these variables) */
|
|
31
32
|
export function detectAgent(env) {
|
|
33
|
+
// Grok first: it passes on the environment it was started from (a Claude Code terminal's too).
|
|
34
|
+
if (env.GROK_AGENT === "1")
|
|
35
|
+
return "grok";
|
|
32
36
|
if (env.CLAUDECODE)
|
|
33
37
|
return "claude";
|
|
34
38
|
if (Object.keys(env).some((k) => k.startsWith("ANTIGRAVITY_")))
|
|
@@ -37,10 +41,24 @@ export function detectAgent(env) {
|
|
|
37
41
|
return "codex";
|
|
38
42
|
if (env.OPENCODE === "1" || env.OPENCODE_PID)
|
|
39
43
|
return "opencode";
|
|
44
|
+
// Cursor's agents — also xAI's Grok Bot, on Cursor's platform (found live): not one strom starts, but an agent
|
|
45
|
+
if (env.CURSOR_AGENT)
|
|
46
|
+
return "cursor";
|
|
40
47
|
if (env.AI_AGENT)
|
|
41
48
|
return env.AI_AGENT.toLowerCase();
|
|
42
49
|
return undefined;
|
|
43
50
|
}
|
|
51
|
+
/** What says an agent runs strom (detectAgent) — not the user's own settings of an agent (…_HOME). */
|
|
52
|
+
export function isAgentMark(k) {
|
|
53
|
+
return ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "AI_AGENT", "OPENCODE", "OPENCODE_PID", "GROK_AGENT", "GROK_SESSION_ID", "CURSOR_AGENT"].includes(k) || (/^(CODEX|ANTIGRAVITY)_/.test(k) && !/_HOME$/.test(k));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The environment for an agent strom starts: without the marks of the agent strom itself was run
|
|
57
|
+
* from (a terminal of Grok's passes them on), so strom in the new agent's shell knows which one it is.
|
|
58
|
+
*/
|
|
59
|
+
export function withoutAgentMarks(env) {
|
|
60
|
+
return Object.fromEntries(Object.entries(env).filter(([k]) => !isAgentMark(k)));
|
|
61
|
+
}
|
|
44
62
|
/** Is an agent running strom — its shell tool, or an agent strom run started? */
|
|
45
63
|
export function isAgent(env) {
|
|
46
64
|
return Boolean(detectAgent(env) || env.STROM_SESSION);
|
package/dist/runners/codex.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
// OpenAI Codex CLI runner. Headless: `codex exec --json` in the tree folder
|
|
2
2
|
// with the brief on stdin. The user's level decides the sandbox: ask and auto
|
|
3
|
-
// work in Codex's sandbox on the tree folder (the shared folder
|
|
4
|
-
// network on — strom fetches), where what the sandbox refuses simply fails —
|
|
3
|
+
// work in Codex's sandbox on the tree folder (its .git and the shared folder
|
|
4
|
+
// writable too, the network on — strom fetches), where what the sandbox refuses simply fails —
|
|
5
5
|
// nothing asks, as with Claude Code's dontAsk; full runs without the sandbox.
|
|
6
6
|
// Interactive conversations are started by strom chat (agents/launch.ts).
|
|
7
|
+
import path from "node:path";
|
|
7
8
|
import { runJsonLines } from "./jsonl.js";
|
|
9
|
+
/**
|
|
10
|
+
* What Codex's sandbox lets it write besides the tree folder: the tree's .git — its sandbox keeps a .git
|
|
11
|
+
* read-only even inside the folder it works in, and every change strom makes is a commit (found live:
|
|
12
|
+
* `strom session note` failed on .git/index.lock) — and the shared folder (inbox, plugins).
|
|
13
|
+
*/
|
|
14
|
+
export function codexWritable(root, shared) {
|
|
15
|
+
return ["-c", `sandbox_workspace_write.writable_roots=${JSON.stringify([path.join(root, ".git"), ...(shared ? [shared] : [])])}`];
|
|
16
|
+
}
|
|
8
17
|
/** Command-line arguments of a headless run (exported for tests). */
|
|
9
18
|
export function codexArgs(opts) {
|
|
10
19
|
const sandbox = opts.permissions === "full"
|
|
11
20
|
? ["--dangerously-bypass-approvals-and-sandbox"]
|
|
12
|
-
: ["--sandbox", "workspace-write", "-c", "sandbox_workspace_write.network_access=true", ...(opts.
|
|
21
|
+
: ["--sandbox", "workspace-write", "-c", "sandbox_workspace_write.network_access=true", ...codexWritable(opts.cwd, opts.shared)];
|
|
13
22
|
return ["exec", "--json", "--skip-git-repo-check", ...sandbox, ...(opts.model ? ["--model", opts.model] : []), ...(opts.extraArgs ?? []), "-"];
|
|
14
23
|
}
|
|
15
24
|
/**
|
|
@@ -19,7 +28,7 @@ export function codexArgs(opts) {
|
|
|
19
28
|
export function codexResumeArgs(id, opts) {
|
|
20
29
|
const sandbox = opts.permissions === "full"
|
|
21
30
|
? ["--dangerously-bypass-approvals-and-sandbox"]
|
|
22
|
-
: ["-c", 'sandbox_mode="workspace-write"', "-c", "sandbox_workspace_write.network_access=true", ...(opts.
|
|
31
|
+
: ["-c", 'sandbox_mode="workspace-write"', "-c", "sandbox_workspace_write.network_access=true", ...codexWritable(opts.cwd, opts.shared)];
|
|
23
32
|
return ["exec", "resume", "--json", "--skip-git-repo-check", ...sandbox, ...(opts.model ? ["--model", opts.model] : []), id, "-"];
|
|
24
33
|
}
|
|
25
34
|
export const codexRunner = {
|
|
@@ -39,7 +48,7 @@ export const codexRunner = {
|
|
|
39
48
|
}
|
|
40
49
|
// The sandbox refused a command (a write outside the tree, the network off): counted like a denial.
|
|
41
50
|
if (msg.type === "item.completed" && item?.type === "command_execution" && item.exit_code !== 0 && /operation not permitted|sandbox|read-only file system/i.test(item.aggregated_output ?? ""))
|
|
42
|
-
heard.denied.push(`Bash: ${String(item.command ?? "").slice(0, 120)}`);
|
|
51
|
+
heard.denied.push(`Bash: ${String(item.command ?? "").replace(/^\S*sh -lc '(.*)'$/s, "$1").slice(0, 120)}`);
|
|
43
52
|
if (msg.type === "turn.completed") {
|
|
44
53
|
const u = (msg.usage ?? {});
|
|
45
54
|
const m = heard.metrics;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Grok Build runner (xAI's `grok`). Headless: `grok --trust --prompt-file <brief>
|
|
2
|
+
// --output-format streaming-json` in the tree folder — it reads no stdin, so the
|
|
3
|
+
// brief goes in a file beside the run's log. --trust: the tree folder's own files
|
|
4
|
+
// (AGENTS.md, .grok/config.toml with the tree's rules) load only in a trusted
|
|
5
|
+
// folder. The level: ask and auto — --permission-mode dontAsk (what the rules do
|
|
6
|
+
// not allow is refused; Grok still lets harmless commands of its own judgement
|
|
7
|
+
// through, so the deny rules are what holds); full — --always-approve (only the
|
|
8
|
+
// deny rules count). strom names the session (-s <uuid>) and resumes it by that
|
|
9
|
+
// id at the time limit (-r). Events are JSON lines: text, tool_call,
|
|
10
|
+
// tool_call_update, end (usage, cost) and error.
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { runJsonLines } from "./jsonl.js";
|
|
15
|
+
/** Command-line arguments of a headless run (exported for tests). */
|
|
16
|
+
export function grokArgs(opts, prompt, session) {
|
|
17
|
+
return [
|
|
18
|
+
"--trust",
|
|
19
|
+
...prompt,
|
|
20
|
+
"--output-format",
|
|
21
|
+
"streaming-json",
|
|
22
|
+
...(opts.permissions === "full" ? ["--always-approve"] : ["--permission-mode", "dontAsk"]),
|
|
23
|
+
...session,
|
|
24
|
+
...(opts.model ? ["--model", opts.model] : []),
|
|
25
|
+
"--no-auto-update",
|
|
26
|
+
...(opts.extraArgs ?? []),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The environment of a headless run: a command may take as long as the session and is never sent to the
|
|
31
|
+
* background when its time is up (a headless run ends with its turn, and what it left in the background with it).
|
|
32
|
+
*/
|
|
33
|
+
export function grokEnv(env, timeoutMs) {
|
|
34
|
+
const secs = Math.max(Math.round((timeoutMs ?? 0) / 1000), 30 * 60);
|
|
35
|
+
return { ...env, GROK_CONFIG: JSON.stringify({ toolset: { bash: { timeout_secs: secs, max_timeout_secs: secs, auto_background_on_timeout: false } } }) };
|
|
36
|
+
}
|
|
37
|
+
/** What a tool call was, in one line: its command, else the tool and its file. */
|
|
38
|
+
function describe(e) {
|
|
39
|
+
const input = (e.rawInput ?? {});
|
|
40
|
+
if (typeof input.command === "string")
|
|
41
|
+
return `$ ${input.command.split("\n")[0].slice(0, 140)}`;
|
|
42
|
+
const file = input.target_file ?? input.file_path ?? input.path ?? input.target_directory;
|
|
43
|
+
return typeof file === "string" ? `${String(e.toolName ?? e.title ?? "tool")} ${file}` : String(e.toolName ?? e.title ?? "tool");
|
|
44
|
+
}
|
|
45
|
+
export const grokRunner = {
|
|
46
|
+
id: "grok",
|
|
47
|
+
command: "grok",
|
|
48
|
+
run(opts) {
|
|
49
|
+
const promptFile = opts.logFile.replace(/\.log$/, "") + ".prompt.md";
|
|
50
|
+
fs.mkdirSync(path.dirname(promptFile), { recursive: true });
|
|
51
|
+
fs.writeFileSync(promptFile, opts.prompt);
|
|
52
|
+
const id = randomUUID();
|
|
53
|
+
const env = grokEnv(opts.env, opts.timeoutMs);
|
|
54
|
+
// Its text comes in pieces; the last message (after the last tool call) is its answer.
|
|
55
|
+
let said = "";
|
|
56
|
+
const calls = new Map();
|
|
57
|
+
const resume = (sid, message) => ({ args: grokArgs(opts, ["-p", message], ["--resume", sid]), input: "" });
|
|
58
|
+
return runJsonLines("grok", grokArgs(opts, ["--prompt-file", promptFile], ["--session-id", id]), env, opts, resume, (msg, heard) => {
|
|
59
|
+
heard.sessionId ??= id;
|
|
60
|
+
const type = msg.type;
|
|
61
|
+
if (type === "text" && typeof msg.data === "string") {
|
|
62
|
+
said += msg.data;
|
|
63
|
+
heard.text = said.trim();
|
|
64
|
+
}
|
|
65
|
+
else if (type === "tool_call") {
|
|
66
|
+
if (said.trim())
|
|
67
|
+
opts.onProgress?.(said.trim().split("\n")[0].slice(0, 160));
|
|
68
|
+
said = "";
|
|
69
|
+
const what = describe(msg);
|
|
70
|
+
if (typeof msg.toolCallId === "string")
|
|
71
|
+
calls.set(msg.toolCallId, `${msg.toolName === "run_terminal_command" ? "Bash" : String(msg.toolName ?? "tool")}: ${what.replace(/^\$ /, "").slice(0, 120)}`);
|
|
72
|
+
opts.onProgress?.(what);
|
|
73
|
+
}
|
|
74
|
+
else if (type === "tool_call_update" && msg.status === "failed") {
|
|
75
|
+
// Refused by the rules ("Denied by permission policy") or by dontAsk ("User cancelled the execution").
|
|
76
|
+
const content = JSON.stringify(msg.content ?? "");
|
|
77
|
+
if (/denied by permission|permission policy|user cancelled|not allowed/i.test(content))
|
|
78
|
+
heard.denied.push(calls.get(String(msg.toolCallId)) ?? "tool");
|
|
79
|
+
}
|
|
80
|
+
else if (type === "end") {
|
|
81
|
+
if (typeof msg.sessionId === "string")
|
|
82
|
+
heard.sessionId = msg.sessionId;
|
|
83
|
+
const u = (msg.usage ?? {});
|
|
84
|
+
const m = heard.metrics;
|
|
85
|
+
// (a resumed session adds its own)
|
|
86
|
+
m.inputTokens = (m.inputTokens ?? 0) + (u.input_tokens ?? 0);
|
|
87
|
+
m.outputTokens = (m.outputTokens ?? 0) + (u.output_tokens ?? 0) + (u.reasoning_tokens ?? 0);
|
|
88
|
+
m.cacheReadTokens = (m.cacheReadTokens ?? 0) + (u.cache_read_input_tokens ?? 0);
|
|
89
|
+
if (typeof msg.num_turns === "number")
|
|
90
|
+
m.turns = (m.turns ?? 0) + msg.num_turns;
|
|
91
|
+
// A cost only when Grok knows it whole (a subscription often does not say it).
|
|
92
|
+
if (typeof msg.total_cost_usd === "number")
|
|
93
|
+
m.costUsd = (m.costUsd ?? 0) + msg.total_cost_usd;
|
|
94
|
+
else if (msg.cost_is_partial === true || msg.usage_is_incomplete === true)
|
|
95
|
+
m.costPartial = true;
|
|
96
|
+
const models = Object.keys((msg.modelUsage ?? {}));
|
|
97
|
+
if (models[0])
|
|
98
|
+
m.model = models[0];
|
|
99
|
+
if (msg.stopReason === "refusal")
|
|
100
|
+
heard.isError = true;
|
|
101
|
+
}
|
|
102
|
+
else if (type === "error") {
|
|
103
|
+
heard.isError = true;
|
|
104
|
+
heard.text = String(msg.message ?? "error");
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
package/dist/runners/index.js
CHANGED
|
@@ -2,12 +2,14 @@ import { claudeRunner } from "./claude.js";
|
|
|
2
2
|
import { codexRunner } from "./codex.js";
|
|
3
3
|
import { antigravityRunner } from "./antigravity.js";
|
|
4
4
|
import { opencodeRunner } from "./opencode.js";
|
|
5
|
+
import { grokRunner } from "./grok.js";
|
|
5
6
|
import { scriptRunner } from "./script.js";
|
|
6
7
|
export const RUNNERS = {
|
|
7
8
|
claude: claudeRunner,
|
|
8
9
|
codex: codexRunner,
|
|
9
10
|
antigravity: antigravityRunner,
|
|
10
11
|
opencode: opencodeRunner,
|
|
12
|
+
grok: grokRunner,
|
|
11
13
|
script: scriptRunner,
|
|
12
14
|
};
|
|
13
15
|
export const DEFAULT_RUNNER = "claude";
|
package/dist/runners/opencode.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// OpenCode runner. Headless: `opencode run --format json "<kickoff>"` in the tree
|
|
2
2
|
// folder with the brief on stdin (it joins what is piped in with the message).
|
|
3
3
|
// The tree's rules are its project config (opencode.json: strom allowed, the
|
|
4
|
-
// evidence and git denied);
|
|
5
|
-
//
|
|
4
|
+
// evidence and git denied); under ask and auto a run nobody watches is its
|
|
5
|
+
// agent "strom-run" there — the same rules, what would ask refused (OpenCode 2
|
|
6
|
+
// ends a headless run at the first question it cannot ask); full adds --auto
|
|
6
7
|
// (everything the rules do not deny). Events are JSON lines: text, tool_use,
|
|
7
8
|
// step_start, step_finish (tokens and cost of a step), error; each names its
|
|
8
9
|
// session (sessionID), which `--session` resumes.
|
|
9
10
|
import { runJsonLines } from "./jsonl.js";
|
|
11
|
+
import { OPENCODE_RUN_AGENT } from "../agents/files.js";
|
|
10
12
|
/** Command-line arguments of a headless run (exported for tests). */
|
|
11
13
|
export function opencodeArgs(opts) {
|
|
12
|
-
|
|
14
|
+
const level = opts.permissions === "full" ? ["--auto"] : ["--agent", OPENCODE_RUN_AGENT];
|
|
15
|
+
return ["run", "--format", "json", ...level, ...(opts.model ? ["--model", opts.model] : []), ...(opts.extraArgs ?? []), opts.kickoff];
|
|
13
16
|
}
|
|
14
17
|
export const opencodeRunner = {
|
|
15
18
|
id: "opencode",
|
|
@@ -28,8 +31,9 @@ export const opencodeRunner = {
|
|
|
28
31
|
const input = state.input ?? {};
|
|
29
32
|
const what = typeof input.command === "string" ? `$ ${input.command}` : typeof input.filePath === "string" ? `${String(part.tool)} ${input.filePath}` : String(part.tool ?? "tool");
|
|
30
33
|
opts.onProgress?.(what.split("\n")[0].slice(0, 140));
|
|
34
|
+
// (its shell tool: "bash", "shell" from OpenCode 2 — named "Bash" like Claude Code's, so a refused strom stops the run)
|
|
31
35
|
if (state.status === "error" && /reject|denied|not allowed|permission/i.test(state.error ?? ""))
|
|
32
|
-
heard.denied.push(`${String(part.tool)}: ${what.slice(0, 120)}`);
|
|
36
|
+
heard.denied.push(typeof input.command === "string" ? `Bash: ${input.command.slice(0, 120)}` : `${String(part.tool)}: ${what.slice(0, 120)}`);
|
|
33
37
|
}
|
|
34
38
|
if (msg.type === "text" && typeof part.text === "string" && part.text.trim())
|
|
35
39
|
heard.text = part.text;
|