strom-research 1.2.0 → 1.4.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/README.md +4 -1
- package/assets/lang/cs.json +164 -7
- package/assets/lang/de.json +164 -7
- package/assets/plugins/README.md +8 -1
- package/assets/plugins/gates/README.md +1 -1
- package/assets/plugins/hooks/README.md +91 -0
- package/dist/agents/files.js +142 -99
- package/dist/agents/global.js +83 -6
- package/dist/agents/launch.js +12 -5
- package/dist/agents/profiles.js +13 -2
- package/dist/cli/main.js +17 -1
- package/dist/cli/menu-parts.js +140 -0
- package/dist/cli/menu-research.js +194 -0
- package/dist/cli/menu-settings.js +273 -0
- package/dist/cli/menu-waiting.js +79 -0
- package/dist/cli/menu.js +91 -97
- package/dist/cli/registry.js +1 -1
- package/dist/cli/ui.js +162 -6
- package/dist/cli/wizard.js +48 -2
- package/dist/commands/browse.js +2 -2
- package/dist/commands/connectors.js +25 -2
- package/dist/commands/hooks.js +141 -0
- package/dist/commands/index.js +1 -0
- package/dist/commands/intake.js +2 -2
- package/dist/commands/meta.js +6 -2
- package/dist/commands/research.js +55 -16
- package/dist/commands/session.js +15 -7
- package/dist/commands/setup.js +51 -2
- package/dist/commands/start.js +9 -4
- package/dist/commands/tasks.js +41 -4
- package/dist/core/browser.js +50 -0
- package/dist/core/config.js +7 -1
- package/dist/core/hooks.js +210 -0
- package/dist/core/install.js +3 -2
- package/dist/core/media.js +21 -7
- package/dist/core/phrases.js +1 -0
- package/dist/core/relocate.js +111 -0
- package/dist/core/shortcut.js +1 -4
- package/dist/core/tree.js +19 -2
- package/dist/core/which.js +18 -0
- package/dist/runners/claude.js +3 -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/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";
|
|
@@ -42,7 +43,7 @@ the researcher; \`strom\` is your only way to read and change the research.
|
|
|
42
43
|
already short, listings take \`--limit\` and \`--page\`, and piped commands may
|
|
43
44
|
be refused by your permissions.
|
|
44
45
|
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
|
|
46
|
+
OpenCode, Grok — the user's choice). \`strom\` shows who is working now; never take a task
|
|
46
47
|
another one has started.
|
|
47
48
|
|
|
48
49
|
## Working with the user
|
|
@@ -130,76 +131,100 @@ export function permissionPath(abs) {
|
|
|
130
131
|
return win ? `//${win[1].toLowerCase()}/${win[2]}` : `/${p}`;
|
|
131
132
|
}
|
|
132
133
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* rules
|
|
134
|
+
* The tree's rules for Claude Code or Grok Build. Grok reads Claude Code's rules too, but an absolute
|
|
135
|
+
* path in Claude Code's words ("//Users/x") is plain text to it: its own file gets the path as it is.
|
|
136
|
+
* File rules are Edit(…) only: Claude Code checks every file-editing tool against them (Write(…) rules
|
|
137
|
+
* match nothing and are reported as a mistake); Grok's Edit covers Write as well.
|
|
136
138
|
*/
|
|
137
|
-
|
|
139
|
+
function treeRules(tree, agent) {
|
|
138
140
|
const settings = new Settings(tree.env, {});
|
|
139
141
|
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})`];
|
|
142
|
+
const claude = agent === "claude";
|
|
143
|
+
const at = (abs) => (claude ? permissionPath(abs) : abs.replace(/\\/g, "/"));
|
|
144
|
+
const keys = at(configDir(tree.env));
|
|
145
|
+
// Connectors of this tree whose images come through the user's browser: browser tools (Claude in Chrome), for their sites only.
|
|
146
|
+
const sites = claude ? [...new Set(treeBrowserConnectors(tree, shared).flatMap((c) => c.manifest.hosts.map(chromeDomain)))] : [];
|
|
147
|
+
const downloads = at(settings.downloads());
|
|
148
|
+
// A rule for each of Claude Code's shells: Bash, and PowerShell (on Windows) — a Bash rule does not cover it. Grok has one.
|
|
149
|
+
const shell = (cmd) => (claude ? [`Bash(${cmd})`, `PowerShell(${cmd})`] : [`Bash(${cmd})`]);
|
|
148
150
|
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
|
-
|
|
151
|
+
allow: [
|
|
152
|
+
...shell("strom:*"),
|
|
153
|
+
"Read(inputs/**)",
|
|
154
|
+
"Read(output/**)",
|
|
155
|
+
"Read(notes/**)",
|
|
156
|
+
"Read(.strom/views/**)",
|
|
157
|
+
// Files the user drops for the research; scans are seen through views only (strom media view).
|
|
158
|
+
...(shared ? [`Read(${at(path.join(shared, "inbox"))}/**)`] : []),
|
|
159
|
+
"Edit(notes/**)",
|
|
160
|
+
// A connector it builds for an archive the research needs (strom connector new); strom runs it.
|
|
161
|
+
...(shared ? ["Read", "Edit"].map((t) => `${t}(${at(path.join(shared, "plugins", "connectors"))}/**)`) : []),
|
|
162
|
+
"WebSearch",
|
|
163
|
+
"WebFetch",
|
|
164
|
+
...(sites.length ? [...CHROME_ALLOW, ...sites] : []),
|
|
165
|
+
],
|
|
166
|
+
deny: [
|
|
167
|
+
"Read(data/**)",
|
|
168
|
+
// The media store: images are looked at through views, so strom knows what was seen.
|
|
169
|
+
...(shared ? [`Read(${at(path.join(shared, "media"))}/**)`] : []),
|
|
170
|
+
"Edit(data/**)",
|
|
171
|
+
"Edit(strom.json)",
|
|
172
|
+
"Edit(.git/**)",
|
|
173
|
+
...shell("git:*"),
|
|
174
|
+
// A password is typed by the user in their own terminal, and so is installing a plugin; the seal is strom's.
|
|
175
|
+
// (Consents the agent may ask for — strom allow …: strom asks the person in a window.)
|
|
176
|
+
...shell("strom login:*"),
|
|
177
|
+
...shell("strom connector add:*"),
|
|
178
|
+
...shell("strom connector remove:*"),
|
|
179
|
+
...shell("strom seal:*"),
|
|
180
|
+
// The limiter's memory (pace, refusals).
|
|
181
|
+
...(shared ? [`Edit(${at(path.join(shared, "net"))}/**)`] : []),
|
|
182
|
+
// A session ends with its turn: nothing wakes it up later (a live run waited for a wake-up that never came).
|
|
183
|
+
...(claude ? ["ScheduleWakeup", "CronCreate"] : []),
|
|
184
|
+
// The seal keys: an agent that could read them could forge the seal.
|
|
185
|
+
`Read(${keys}/**)`,
|
|
186
|
+
`Edit(${keys}/**)`,
|
|
187
|
+
// What the browser downloads is strom's to take over; the rest of the folder is the user's.
|
|
188
|
+
`Read(${downloads}/**)`,
|
|
189
|
+
...(claude ? CHROME_DENY : []),
|
|
190
|
+
// With full permissions only the deny rules count: what the allow list kept away is kept away here.
|
|
191
|
+
...(settings.agentPermissions() === "full" ? BYPASS_DENY : []),
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** Permissions for Claude Code in this tree (paths of this computer). */
|
|
196
|
+
export function claudeSettings(tree) {
|
|
197
|
+
const lead = new Settings(tree.env, {}).models("claude", tree.config).lead;
|
|
198
|
+
return {
|
|
199
|
+
permissions: treeRules(tree, "claude"),
|
|
194
200
|
// The user's model: the desktop app cannot be given one when it opens (the CLI is, with --model).
|
|
195
201
|
...(lead ? { model: lead } : {}),
|
|
196
202
|
};
|
|
197
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Grok Build's rules for this tree (.grok/config.toml, read once the folder is trusted — strom starts it
|
|
206
|
+
* so). It reads .claude/settings.json as well; this file says the same with paths it understands.
|
|
207
|
+
*/
|
|
208
|
+
export function grokConfig(tree) {
|
|
209
|
+
const { allow, deny } = treeRules(tree, "grok");
|
|
210
|
+
// A TOML basic string is written like a JSON one.
|
|
211
|
+
const list = (xs) => `[\n${xs.map((x) => ` ${JSON.stringify(x)},`).join("\n")}\n]`;
|
|
212
|
+
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`;
|
|
213
|
+
}
|
|
214
|
+
/** Grok Build reads CLAUDE.md too (for Claude Code's projects): what in it is not for Grok. */
|
|
215
|
+
const GROK_RULES = `# Grok Build in this family tree
|
|
216
|
+
|
|
217
|
+
CLAUDE.md in this folder is written for Claude Code: its way of delegating (the
|
|
218
|
+
Agent tool, the models opus, sonnet and haiku) is not yours. Follow AGENTS.md,
|
|
219
|
+
and read scans yourself as it says under "Reading scans".
|
|
220
|
+
`;
|
|
198
221
|
/** Denied as well when the user lets the agent do everything else (agent.permissions full). */
|
|
199
222
|
export const BYPASS_DENY = [
|
|
200
223
|
// its own permissions and instructions
|
|
201
224
|
"Edit(.claude/**)",
|
|
202
225
|
...["AGENTS.md", "CLAUDE.md"].map((f) => `Edit(${f})`),
|
|
226
|
+
// (Grok Build's own rules and instructions)
|
|
227
|
+
"Edit(.grok/**)",
|
|
203
228
|
// downloads round strom's limiter
|
|
204
229
|
"Bash(curl:*)",
|
|
205
230
|
"Bash(wget:*)",
|
|
@@ -218,47 +243,63 @@ export function opencodeConfig(tree) {
|
|
|
218
243
|
const abs = (p) => p.replace(/\\/g, "/");
|
|
219
244
|
const full = settings.agentPermissions() === "full";
|
|
220
245
|
const users = ["strom login *", "strom seal *", "strom connector add *", "strom connector remove *"];
|
|
246
|
+
// The user's model ("provider/model"): its conversation takes no --model, it reads it here.
|
|
247
|
+
const lead = settings.models("opencode", tree.config).lead;
|
|
248
|
+
const permission = {
|
|
249
|
+
bash: {
|
|
250
|
+
"*": "ask",
|
|
251
|
+
"strom *": "allow",
|
|
252
|
+
...Object.fromEntries(users.map((c) => [c, "deny"])),
|
|
253
|
+
"git *": "deny",
|
|
254
|
+
...(full ? { "curl *": "deny", "wget *": "deny" } : {}),
|
|
255
|
+
},
|
|
256
|
+
read: { "*": "allow", "data/*": "deny", ".git/*": "deny" },
|
|
257
|
+
edit: {
|
|
258
|
+
"*": "ask",
|
|
259
|
+
"notes/*": "allow",
|
|
260
|
+
"data/*": "deny",
|
|
261
|
+
".git/*": "deny",
|
|
262
|
+
"strom.json": "deny",
|
|
263
|
+
// its own rules and instructions
|
|
264
|
+
"opencode.json": "deny",
|
|
265
|
+
...Object.fromEntries(["AGENTS.md", "CLAUDE.md", ".claude/*"].map((f) => [f, "deny"])),
|
|
266
|
+
},
|
|
267
|
+
external_directory: {
|
|
268
|
+
"*": "ask",
|
|
269
|
+
...(shared
|
|
270
|
+
? {
|
|
271
|
+
[`${abs(path.join(shared, "inbox"))}/*`]: "allow",
|
|
272
|
+
[`${abs(path.join(shared, "plugins", "connectors"))}/*`]: "allow",
|
|
273
|
+
// images are looked at through views, so strom knows what was seen; the limiter's memory is strom's
|
|
274
|
+
[`${abs(path.join(shared, "media"))}/*`]: "deny",
|
|
275
|
+
[`${abs(path.join(shared, "net"))}/*`]: "deny",
|
|
276
|
+
}
|
|
277
|
+
: {}),
|
|
278
|
+
// the seal keys, and what the browser downloads (strom's to take over)
|
|
279
|
+
[`${abs(configDir(tree.env))}/*`]: "deny",
|
|
280
|
+
[`${abs(settings.downloads())}/*`]: "deny",
|
|
281
|
+
},
|
|
282
|
+
webfetch: "allow",
|
|
283
|
+
websearch: "allow",
|
|
284
|
+
};
|
|
285
|
+
// What would ask is refused when nobody watches: OpenCode 2 ends a whole headless run at the first question
|
|
286
|
+
// it cannot ask (found live), where a refusal only fails that call.
|
|
287
|
+
const refused = (v) => v === "ask" ? "deny" : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, refused(x)])) : v;
|
|
221
288
|
return {
|
|
222
289
|
$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",
|
|
290
|
+
...(lead ? { model: lead } : {}),
|
|
291
|
+
permission,
|
|
292
|
+
agent: {
|
|
293
|
+
[OPENCODE_RUN_AGENT]: {
|
|
294
|
+
mode: "primary",
|
|
295
|
+
description: "strom run: the agent working alone, nobody to ask — what the rules do not allow is refused",
|
|
296
|
+
permission: refused(permission),
|
|
256
297
|
},
|
|
257
|
-
webfetch: "allow",
|
|
258
|
-
websearch: "allow",
|
|
259
298
|
},
|
|
260
299
|
};
|
|
261
300
|
}
|
|
301
|
+
/** OpenCode's agent for a run nobody watches under ask and auto (opencode run --agent). */
|
|
302
|
+
export const OPENCODE_RUN_AGENT = "strom-run";
|
|
262
303
|
/** Files strom wrote for Gemini CLI (gone: Google ended it for personal accounts) — taken away when they are strom's. */
|
|
263
304
|
const OBSOLETE = [
|
|
264
305
|
["GEMINI.md", (t) => t.startsWith("@./AGENTS.md") && t.includes(MARKER) && t.slice(t.indexOf(MARKER) + MARKER.length).trim() === ""],
|
|
@@ -277,7 +318,7 @@ function withUserPart(file, generated) {
|
|
|
277
318
|
}
|
|
278
319
|
return generated;
|
|
279
320
|
}
|
|
280
|
-
export const AGENT_FILES = ["AGENTS.md", "CLAUDE.md", path.join(".claude", "settings.json"), "opencode.json"];
|
|
321
|
+
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
322
|
export function syncAgentFiles(tree) {
|
|
282
323
|
const written = [];
|
|
283
324
|
const write = (rel, content) => {
|
|
@@ -300,6 +341,8 @@ export function syncAgentFiles(tree) {
|
|
|
300
341
|
write("CLAUDE.md", claudeMd(tree));
|
|
301
342
|
write(path.join(".claude", "settings.json"), JSON.stringify(claudeSettings(tree), null, 2) + "\n");
|
|
302
343
|
write("opencode.json", JSON.stringify(opencodeConfig(tree), null, 2) + "\n");
|
|
344
|
+
write(path.join(".grok", "config.toml"), grokConfig(tree));
|
|
345
|
+
write(path.join(".grok", "rules", "strom.md"), GROK_RULES);
|
|
303
346
|
for (const [rel, ours] of OBSOLETE) {
|
|
304
347
|
const file = path.join(tree.root, rel);
|
|
305
348
|
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";
|
|
@@ -26,6 +28,8 @@ mentions strom, a family tree or the Strom app.
|
|
|
26
28
|
|
|
27
29
|
1. Run \`strom\` first. It says where the research stands and what to do next
|
|
28
30
|
(\`strom --json\` for the same as data; \`strom guide\` explains the work).
|
|
31
|
+
Run each strom command on its own — no pipes, no \`;\` or \`&&\` chains: you
|
|
32
|
+
may run strom without asking, but not what is chained to it.
|
|
29
33
|
2. Not set up yet: ask the user where to keep the research (suggest
|
|
30
34
|
~/Documents/Strom) and which language they want, then run
|
|
31
35
|
\`strom setup --home "<folder>" --lang <code> --yes\`, then
|
|
@@ -66,7 +70,46 @@ function allowRules(agent) {
|
|
|
66
70
|
if (agent === "antigravity")
|
|
67
71
|
return ["command(strom)"];
|
|
68
72
|
const commands = ["strom", ...(installation().launchers ?? [])];
|
|
69
|
-
|
|
73
|
+
// Grok Build: one shell tool (its rules know no PowerShell)
|
|
74
|
+
return commands.flatMap((c) => (agent === "grok" ? [`Bash(${c}:*)`] : [`Bash(${c}:*)`, `PowerShell(${c}:*)`]));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Grok Build's config.toml, strom's lines marked: a [permission] table of strom's when there is none,
|
|
78
|
+
* else strom's allow list in the user's table — when it has no allow list of its own (strom does not
|
|
79
|
+
* rewrite the user's list: Grok then asks before it runs strom, as the user set it).
|
|
80
|
+
*/
|
|
81
|
+
const TOML_BEGIN = "# strom: begin (strom agents install; removed by strom agents uninstall)";
|
|
82
|
+
const TOML_END = "# strom: end";
|
|
83
|
+
function grokWithout(text) {
|
|
84
|
+
const lines = text.split("\n");
|
|
85
|
+
const out = [];
|
|
86
|
+
let inside = false;
|
|
87
|
+
for (const l of lines) {
|
|
88
|
+
if (l.trim() === TOML_BEGIN)
|
|
89
|
+
inside = true;
|
|
90
|
+
else if (l.trim() === TOML_END)
|
|
91
|
+
inside = false;
|
|
92
|
+
else if (!inside)
|
|
93
|
+
out.push(l);
|
|
94
|
+
}
|
|
95
|
+
return out.join("\n");
|
|
96
|
+
}
|
|
97
|
+
function grokWith(text) {
|
|
98
|
+
const rest = grokWithout(text);
|
|
99
|
+
const allow = `allow = [${allowRules("grok").map((r) => JSON.stringify(r)).join(", ")}]`;
|
|
100
|
+
const lines = rest.split("\n");
|
|
101
|
+
const header = lines.findIndex((l) => /^\s*\[permission\]\s*(#.*)?$/.test(l));
|
|
102
|
+
if (header < 0) {
|
|
103
|
+
const body = rest.replace(/\s+$/, "");
|
|
104
|
+
return `${body ? `${body}\n\n` : ""}${TOML_BEGIN}\n[permission]\n${allow}\n${TOML_END}\n`;
|
|
105
|
+
}
|
|
106
|
+
// the user's table: up to the next table
|
|
107
|
+
let end = lines.findIndex((l, i) => i > header && /^\s*\[/.test(l));
|
|
108
|
+
if (end < 0)
|
|
109
|
+
end = lines.length;
|
|
110
|
+
if (lines.slice(header + 1, end).some((l) => /^\s*allow\s*=/.test(l)))
|
|
111
|
+
return undefined;
|
|
112
|
+
return [...lines.slice(0, header + 1), TOML_BEGIN, allow, TOML_END, ...lines.slice(header + 1)].join("\n");
|
|
70
113
|
}
|
|
71
114
|
/** A rule of strom's in Claude Code's or Antigravity's settings, whichever installation wrote it. */
|
|
72
115
|
function isStromRule(agent, rule) {
|
|
@@ -74,12 +117,26 @@ function isStromRule(agent, rule) {
|
|
|
74
117
|
return rule === "command(strom)";
|
|
75
118
|
return /^(?:Bash|PowerShell)\((?:.*[\\/])?strom(?:\.exe|\.cmd)?:\*\)$/.test(rule);
|
|
76
119
|
}
|
|
120
|
+
/** Grok Build's folder (GROK_HOME, else ~/.grok). */
|
|
121
|
+
function grokDir(env) {
|
|
122
|
+
return env.GROK_HOME ?? path.join(userHome(env), ".grok");
|
|
123
|
+
}
|
|
77
124
|
/** OpenCode: the rule that lets it run strom without asking. */
|
|
78
125
|
const OPENCODE_STROM = "strom *";
|
|
79
126
|
/** OpenCode's global config folder (it follows XDG on every system). */
|
|
80
127
|
function opencodeDir(env) {
|
|
81
128
|
return path.join(env.XDG_CONFIG_HOME ?? path.join(userHome(env), ".config"), "opencode");
|
|
82
129
|
}
|
|
130
|
+
/** Claude Code starts its own conversations with Remote Control (its setting remoteControlAtStartup, the user's own). */
|
|
131
|
+
export function claudeRemoteAtStartup(env) {
|
|
132
|
+
try {
|
|
133
|
+
const file = path.join(env.CLAUDE_CONFIG_DIR ?? path.join(userHome(env), ".claude"), "settings.json");
|
|
134
|
+
return JSON.parse(fs.readFileSync(file, "utf8")).remoteControlAtStartup === true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false; // no settings, or not readable: not on
|
|
138
|
+
}
|
|
139
|
+
}
|
|
83
140
|
export function globalTargets(env) {
|
|
84
141
|
const home = userHome(env);
|
|
85
142
|
return [
|
|
@@ -91,6 +148,8 @@ export function globalTargets(env) {
|
|
|
91
148
|
{ agent: "antigravity", file: path.join(home, ".gemini", "antigravity-cli", "settings.json"), kind: "allow" },
|
|
92
149
|
{ agent: "opencode", file: path.join(opencodeDir(env), "strom.md"), kind: "own" },
|
|
93
150
|
{ agent: "opencode", file: path.join(opencodeDir(env), "opencode.json"), kind: "allow" },
|
|
151
|
+
{ agent: "grok", file: path.join(grokDir(env), "skills", "strom", "SKILL.md"), kind: "own" },
|
|
152
|
+
{ agent: "grok", file: path.join(grokDir(env), "config.toml"), kind: "allow" },
|
|
94
153
|
];
|
|
95
154
|
}
|
|
96
155
|
const SKILL = `---
|
|
@@ -199,7 +258,13 @@ function removeAllow(t, s) {
|
|
|
199
258
|
export function installGlobal(t) {
|
|
200
259
|
const cur = read(t.file);
|
|
201
260
|
let next;
|
|
202
|
-
if (t.kind === "allow") {
|
|
261
|
+
if (t.kind === "allow" && t.agent === "grok") {
|
|
262
|
+
const with_ = grokWith(cur ?? "");
|
|
263
|
+
if (with_ === undefined)
|
|
264
|
+
return false;
|
|
265
|
+
next = with_;
|
|
266
|
+
}
|
|
267
|
+
else if (t.kind === "allow") {
|
|
203
268
|
const s = settingsOf(cur);
|
|
204
269
|
if (!s || hasAllow(t, s))
|
|
205
270
|
return false;
|
|
@@ -207,7 +272,7 @@ export function installGlobal(t) {
|
|
|
207
272
|
next = JSON.stringify(s, null, 2) + "\n";
|
|
208
273
|
}
|
|
209
274
|
else if (t.kind === "own")
|
|
210
|
-
next = t.agent === "claude" ? SKILL : GLOBAL_TEXT;
|
|
275
|
+
next = t.agent === "claude" || t.agent === "grok" ? SKILL : GLOBAL_TEXT;
|
|
211
276
|
else {
|
|
212
277
|
const rest = cur ? withoutBlock(cur).replace(/\s+$/, "") : "";
|
|
213
278
|
next = `${rest ? `${rest}\n\n` : ""}${BEGIN}\n${GLOBAL_TEXT}${END}\n`;
|
|
@@ -223,6 +288,16 @@ export function uninstallGlobal(t) {
|
|
|
223
288
|
const cur = read(t.file);
|
|
224
289
|
if (cur === undefined)
|
|
225
290
|
return false;
|
|
291
|
+
if (t.kind === "allow" && t.agent === "grok") {
|
|
292
|
+
if (!cur.includes(TOML_BEGIN))
|
|
293
|
+
return false;
|
|
294
|
+
const rest = grokWithout(cur);
|
|
295
|
+
if (rest.trim())
|
|
296
|
+
writeFileAtomic(t.file, rest.replace(/\n{3,}/g, "\n\n").replace(/\s*$/, "\n"));
|
|
297
|
+
else
|
|
298
|
+
fs.rmSync(t.file);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
226
301
|
if (t.kind === "allow") {
|
|
227
302
|
const s = settingsOf(cur);
|
|
228
303
|
// Any of strom's rules, also those another installation of strom wrote.
|
|
@@ -234,8 +309,8 @@ export function uninstallGlobal(t) {
|
|
|
234
309
|
return true;
|
|
235
310
|
}
|
|
236
311
|
if (t.kind === "own") {
|
|
237
|
-
//
|
|
238
|
-
if (t.agent === "claude")
|
|
312
|
+
// A skill is a folder of its own; OpenCode's instructions one file among the user's.
|
|
313
|
+
if (t.agent === "claude" || t.agent === "grok")
|
|
239
314
|
fs.rmSync(path.dirname(t.file), { recursive: true, force: true });
|
|
240
315
|
else
|
|
241
316
|
fs.rmSync(t.file, { force: true });
|
|
@@ -262,6 +337,8 @@ export function isInstalled(t) {
|
|
|
262
337
|
const cur = read(t.file);
|
|
263
338
|
if (cur === undefined)
|
|
264
339
|
return false;
|
|
340
|
+
if (t.kind === "allow" && t.agent === "grok")
|
|
341
|
+
return cur.includes(TOML_BEGIN) && grokWith(cur) === cur;
|
|
265
342
|
if (t.kind === "allow")
|
|
266
343
|
return hasAllow(t, settingsOf(cur) ?? {});
|
|
267
344
|
return t.kind === "own" || cur.includes(BEGIN);
|
package/dist/agents/launch.js
CHANGED
|
@@ -11,26 +11,33 @@
|
|
|
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")
|
|
22
|
-
return claudeArgs({ interactive: true, kickoff: o.kickoff, permissions: o.level, settingsFile: o.settingsFile, ...(o.name ? { name: o.name } : {}), ...(o.model ? { model: o.model } : {}), ...(o.chrome !== undefined ? { chrome: o.chrome } : {}) });
|
|
26
|
+
return claudeArgs({ interactive: true, kickoff: o.kickoff, permissions: o.level, settingsFile: o.settingsFile, ...(o.name ? { name: o.name } : {}), ...(o.model ? { model: o.model } : {}), ...(o.chrome !== undefined ? { chrome: o.chrome } : {}), ...(o.remote ? { remote: true } : {}) });
|
|
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,8 +20,8 @@ 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
26
|
Read the images yourself, in batches of at most ten: open a batch, write down
|
|
27
27
|
what it gave (strom search add … for what was not found, facts for what was),
|
|
@@ -85,6 +85,17 @@ ${DELEGATION_RULES}
|
|
|
85
85
|
models: {},
|
|
86
86
|
instructions: () => SELF_READING,
|
|
87
87
|
},
|
|
88
|
+
// xAI's Grok Build: its subagents take only its own models (no cheaper reader worth the images): it reads itself.
|
|
89
|
+
grok: {
|
|
90
|
+
id: "grok",
|
|
91
|
+
name: "Grok Build",
|
|
92
|
+
command: "grok",
|
|
93
|
+
exit: "/quit",
|
|
94
|
+
url: "https://x.ai/cli",
|
|
95
|
+
delegation: "strom",
|
|
96
|
+
models: {},
|
|
97
|
+
instructions: () => SELF_READING,
|
|
98
|
+
},
|
|
88
99
|
};
|
|
89
100
|
export const DEFAULT_AGENT = "claude";
|
|
90
101
|
export function profile(id) {
|
package/dist/cli/main.js
CHANGED
|
@@ -6,7 +6,10 @@ import { groupHelp, helpFor } from "./help.js";
|
|
|
6
6
|
import { autoCommit } from "./commit.js";
|
|
7
7
|
import { assertIntact } from "../core/integrity.js";
|
|
8
8
|
import { resetCache } from "../core/git.js";
|
|
9
|
-
import { VERSION } from "../core/tree.js";
|
|
9
|
+
import { Tree, VERSION } from "../core/tree.js";
|
|
10
|
+
import { fireHooks, HOOK_INTERFACE } from "../core/hooks.js";
|
|
11
|
+
import { prependPath } from "../runners/runner.js";
|
|
12
|
+
import { shimDir } from "../commands/session.js";
|
|
10
13
|
import { isAgent } from "../core/which.js";
|
|
11
14
|
import { noticeStromApp } from "../core/stromapp.js";
|
|
12
15
|
import { isNewer } from "../core/update.js";
|
|
@@ -116,6 +119,19 @@ export async function main(argv, io, env, cwd) {
|
|
|
116
119
|
const ctx = Context.fromOptions({ env, cwd, io, json, values: v });
|
|
117
120
|
ran = ctx;
|
|
118
121
|
command = def.path.join(" ");
|
|
122
|
+
// What is saved into a research, the user's hooks are told of (in the background).
|
|
123
|
+
Tree.onCommit = (tree, ops, commit) => {
|
|
124
|
+
if (!ctx.settings.config.hooks?.length || env.STROM_HOOK)
|
|
125
|
+
return;
|
|
126
|
+
// this strom on the hook's PATH: it reads more of what was saved (strom person card P0012)
|
|
127
|
+
fireHooks(ctx.settings.shared()?.value, ctx.settings.config.hooks, prependPath(env, shimDir(tree)), {
|
|
128
|
+
interface: HOOK_INTERFACE,
|
|
129
|
+
tree: { name: tree.config.name, id: tree.config.id, root: tree.root, lang: tree.lang },
|
|
130
|
+
commit,
|
|
131
|
+
at: new Date().toISOString(),
|
|
132
|
+
events: ops.map((o) => ({ op: o.op, targets: o.targets, summary: o.summary, at: o.at, by: o.by, ...(o.reason ? { reason: o.reason } : {}) })),
|
|
133
|
+
});
|
|
134
|
+
};
|
|
119
135
|
// Started by the Strom app: remembered quietly (it is where the results go).
|
|
120
136
|
if (env.STROM_APP)
|
|
121
137
|
noticeStromApp(ctx.settings, env);
|