strom-research 1.0.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/LICENSE +373 -0
- package/README.md +142 -0
- package/assets/lang/cs.json +302 -0
- package/assets/lang/de.json +302 -0
- package/assets/method/core.md +43 -0
- package/assets/method/enrich.md +11 -0
- package/assets/method/intake.md +30 -0
- package/assets/method/link.md +28 -0
- package/assets/method/locate.md +28 -0
- package/assets/method/narrate.md +13 -0
- package/assets/method/reading.md +62 -0
- package/assets/method/recording.md +59 -0
- package/assets/method/request.md +10 -0
- package/assets/method/verify.md +17 -0
- package/assets/plugins/README.md +23 -0
- package/assets/plugins/connectors/DISCOVERY.md +159 -0
- package/assets/plugins/connectors/README.md +376 -0
- package/assets/plugins/connectors/sdk.ts +168 -0
- package/assets/plugins/connectors/template.ts +38 -0
- package/assets/plugins/gitignore +4 -0
- package/dist/agents/files.js +313 -0
- package/dist/agents/global.js +257 -0
- package/dist/agents/launch.js +36 -0
- package/dist/agents/profiles.js +95 -0
- package/dist/brief/brief.js +345 -0
- package/dist/cli/commit.js +44 -0
- package/dist/cli/context.js +311 -0
- package/dist/cli/execute.js +154 -0
- package/dist/cli/fixes.js +78 -0
- package/dist/cli/format.js +53 -0
- package/dist/cli/help.js +59 -0
- package/dist/cli/main.js +152 -0
- package/dist/cli/menu.js +212 -0
- package/dist/cli/registry.js +96 -0
- package/dist/cli/ui.js +266 -0
- package/dist/cli/wizard.js +142 -0
- package/dist/cli.js +14 -0
- package/dist/commands/analysis.js +622 -0
- package/dist/commands/batch.js +181 -0
- package/dist/commands/checks.js +153 -0
- package/dist/commands/connectors.js +1377 -0
- package/dist/commands/guide.js +160 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/intake.js +234 -0
- package/dist/commands/media.js +406 -0
- package/dist/commands/meta.js +195 -0
- package/dist/commands/output.js +117 -0
- package/dist/commands/people.js +664 -0
- package/dist/commands/read.js +199 -0
- package/dist/commands/research.js +139 -0
- package/dist/commands/session.js +605 -0
- package/dist/commands/setup.js +465 -0
- package/dist/commands/sources.js +634 -0
- package/dist/commands/start.js +383 -0
- package/dist/commands/story.js +75 -0
- package/dist/commands/tasks.js +436 -0
- package/dist/commands/trees.js +128 -0
- package/dist/core/actions.js +852 -0
- package/dist/core/age.js +95 -0
- package/dist/core/apps.js +74 -0
- package/dist/core/assets.js +34 -0
- package/dist/core/awake.js +33 -0
- package/dist/core/browser.js +281 -0
- package/dist/core/calibration.js +48 -0
- package/dist/core/check.js +112 -0
- package/dist/core/chromium.js +88 -0
- package/dist/core/config.js +348 -0
- package/dist/core/connector.js +811 -0
- package/dist/core/deps.js +73 -0
- package/dist/core/dialog.js +61 -0
- package/dist/core/errors.js +89 -0
- package/dist/core/evidence.js +58 -0
- package/dist/core/frontier.js +219 -0
- package/dist/core/gdate.js +77 -0
- package/dist/core/git.js +300 -0
- package/dist/core/guard.js +124 -0
- package/dist/core/http2.js +76 -0
- package/dist/core/import.js +541 -0
- package/dist/core/install.js +28 -0
- package/dist/core/integrity.js +219 -0
- package/dist/core/json.js +87 -0
- package/dist/core/lang.js +70 -0
- package/dist/core/live.js +244 -0
- package/dist/core/lock.js +112 -0
- package/dist/core/logins.js +67 -0
- package/dist/core/media.js +223 -0
- package/dist/core/model.js +101 -0
- package/dist/core/net.js +366 -0
- package/dist/core/open.js +29 -0
- package/dist/core/paths.js +84 -0
- package/dist/core/people.js +283 -0
- package/dist/core/phrases.js +85 -0
- package/dist/core/queue.js +113 -0
- package/dist/core/reader.js +76 -0
- package/dist/core/records.js +105 -0
- package/dist/core/roles.js +30 -0
- package/dist/core/schema.js +261 -0
- package/dist/core/seal.js +77 -0
- package/dist/core/self.js +40 -0
- package/dist/core/session.js +155 -0
- package/dist/core/shortcut.js +90 -0
- package/dist/core/stories.js +61 -0
- package/dist/core/stromapp.js +138 -0
- package/dist/core/text.js +104 -0
- package/dist/core/tree.js +507 -0
- package/dist/core/uninstall.js +128 -0
- package/dist/core/update.js +193 -0
- package/dist/core/validate.js +260 -0
- package/dist/core/views.js +164 -0
- package/dist/core/which.js +51 -0
- package/dist/core/workers.js +42 -0
- package/dist/gedcom/export.js +454 -0
- package/dist/gedcom/labels.js +103 -0
- package/dist/gedcom/lines.js +91 -0
- package/dist/gedcom/parse.js +53 -0
- package/dist/gedcom/validate.js +183 -0
- package/dist/image/image.js +223 -0
- package/dist/image/index.js +114 -0
- package/dist/image/jpeg-decode.js +552 -0
- package/dist/image/jpeg-encode.js +254 -0
- package/dist/image/png.js +241 -0
- package/dist/runners/antigravity.js +70 -0
- package/dist/runners/claude.js +179 -0
- package/dist/runners/codex.js +45 -0
- package/dist/runners/index.js +13 -0
- package/dist/runners/jsonl.js +86 -0
- package/dist/runners/opencode.js +50 -0
- package/dist/runners/runner.js +63 -0
- package/dist/runners/script.js +58 -0
- package/package.json +44 -0
|
@@ -0,0 +1,313 @@
|
|
|
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 and
|
|
3
|
+
// OpenCode natively; CLAUDE.md imports it) and holds nothing agent-specific;
|
|
4
|
+
// each agent's own file adds what only that agent needs. .claude/settings.json
|
|
5
|
+
// (and OpenCode's opencode.json) allow `strom` and deny direct access to the
|
|
6
|
+
// evidence — the first line of defence; the seal is the one that always holds.
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { langName } from "../core/lang.js";
|
|
10
|
+
import { writeFileAtomic } from "../core/json.js";
|
|
11
|
+
import { PROFILES, SELF_READING } from "./profiles.js";
|
|
12
|
+
import { Settings } from "../core/config.js";
|
|
13
|
+
import { configDir } from "../core/paths.js";
|
|
14
|
+
import { browserConnectors } from "../core/connector.js";
|
|
15
|
+
import { CHROME_ALLOW, CHROME_DENY, chromeDomain } from "../core/browser.js";
|
|
16
|
+
export const MARKER = "<!-- strom: generated above this line (strom agents sync); your own notes below are kept -->";
|
|
17
|
+
function agentsMd(tree) {
|
|
18
|
+
const lang = langName(tree.config.lang);
|
|
19
|
+
return `# Family research: ${tree.config.name}
|
|
20
|
+
|
|
21
|
+
This folder is a family tree researched with **strom**, a command-line tool
|
|
22
|
+
that holds the evidence, the method and the history of the research. You are
|
|
23
|
+
the researcher; \`strom\` is your only way to read and change the research.
|
|
24
|
+
|
|
25
|
+
1. Start with \`strom\` — it says where things stand and what to do next.
|
|
26
|
+
\`strom guide\` explains the work; \`strom help <command>\` any command.
|
|
27
|
+
2. Work in sessions: \`strom session start\` gives you the brief for the next
|
|
28
|
+
task; finish with \`strom session close --summary "…" --next "…"\`.
|
|
29
|
+
3. Never create, edit or delete anything in \`data/\`, \`strom.json\` or \`.git\`,
|
|
30
|
+
never read the files in \`data/\` (strom shows them better and shorter), and
|
|
31
|
+
never run git yourself. strom detects changes and then refuses to write.
|
|
32
|
+
4. The research language is ${lang}: talk to the user in ${lang} and write notes,
|
|
33
|
+
tasks and summaries in ${lang}. Transcripts stay in the record's language.
|
|
34
|
+
5. Material from the user goes in with \`strom intake <file or folder>\` or
|
|
35
|
+
\`strom intake --text "…"\`. Exit code 3 means strom needs an answer from the
|
|
36
|
+
user; exit code 4 means the USER must run the given command in their own
|
|
37
|
+
terminal — never do it yourself.
|
|
38
|
+
6. You may read \`inputs/\`, \`output/\`, \`notes/\` and \`.strom/views/\`, and write
|
|
39
|
+
your own working notes in \`notes/\`. Scans are looked at through views:
|
|
40
|
+
\`strom media view B0001:57 --half left\` writes one to \`.strom/views/\`.
|
|
41
|
+
7. Run strom commands on their own — no pipes (\`| head\`, \`| grep\`): output is
|
|
42
|
+
already short, listings take \`--limit\` and \`--page\`, and piped commands may
|
|
43
|
+
be refused by your permissions.
|
|
44
|
+
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
|
+
another one has started.
|
|
47
|
+
|
|
48
|
+
## Working with the user
|
|
49
|
+
|
|
50
|
+
The user watches this conversation and is usually not technical. Talk in
|
|
51
|
+
${lang}, plainly: say "the baptism of Karel in 1782", not record IDs, unless
|
|
52
|
+
they ask.
|
|
53
|
+
|
|
54
|
+
- **First conversation** (no research yet): explain in a few sentences how you
|
|
55
|
+
work together — you search registers and archives, record only what a
|
|
56
|
+
record proves, and ask them for decisions and for what only they can do —
|
|
57
|
+
then ask whom to research and what they know or have: names, dates, places,
|
|
58
|
+
documents, photos, a family tree file.
|
|
59
|
+
- **The user decides**: whom to research, what next, anything that costs
|
|
60
|
+
money, what you may do. Suggest; do not decide for them.
|
|
61
|
+
- **Ways of working** — explain them when it helps:
|
|
62
|
+
- this conversation: the two of you together;
|
|
63
|
+
- the agent working on its own: they start it from strom's menu ("Let the
|
|
64
|
+
agent work on its own"); it works the task queue and tells them the result;
|
|
65
|
+
with many tasks in the queue, suggest it — every task gets a fresh session;
|
|
66
|
+
- one task, one fresh context: after a session closes, strom says how the
|
|
67
|
+
user clears your context (Claude Code: /clear) — nothing is lost, strom
|
|
68
|
+
keeps it all; suggest it in a sentence, go on here if they prefer;
|
|
69
|
+
- what only they can do: when strom needs their consent it opens a window
|
|
70
|
+
on their screen — tell them to answer it, you cannot; images a portal gives
|
|
71
|
+
only by hand — strom says which ones and where to save them.
|
|
72
|
+
- **Downloaders**: the research needs an archive strom has no downloader
|
|
73
|
+
(connector) for — build one yourself, do not wait to be asked; tell them in
|
|
74
|
+
a sentence what you are doing ("I am preparing a downloader for this
|
|
75
|
+
archive — a few minutes; it then fetches only the images we need, slowly").
|
|
76
|
+
- **Stories of their ancestors**: on by default — once records tell a life,
|
|
77
|
+
strom proposes a task to write it for the family book in the Strom app
|
|
78
|
+
(the method: a story rests on recorded facts only). When the research
|
|
79
|
+
starts, tell them in a sentence and that they may say no (\`strom\` shows
|
|
80
|
+
whether they chose; \`strom config set stories no\`). A story stays a draft
|
|
81
|
+
until they approve it.
|
|
82
|
+
- **What waits for them** (\`strom\` shows it): tell them plainly what to do
|
|
83
|
+
and where, one thing at a time.
|
|
84
|
+
- **Results and the Strom app**: \`output/tree-strom.ged\` is the family tree
|
|
85
|
+
for the Strom app (https://stromapp.info) — strom's companion: a free family
|
|
86
|
+
tree app, no account, their data stay on their computer; it shows the tree,
|
|
87
|
+
the sources, a map, a family book. When they want to see the results (or
|
|
88
|
+
once, when the first ones are there), suggest it gently, in a sentence or
|
|
89
|
+
two: best installed as an app from the browser, from
|
|
90
|
+
https://stromapp.info/run/ — \`strom app install\` opens it there and says
|
|
91
|
+
where to click; it works offline then; \`strom app\` opens it, with this
|
|
92
|
+
research when the app can take it — run by you, the app then follows the
|
|
93
|
+
research live, what you record shows there by itself (else in the app:
|
|
94
|
+
Import, and this file). \`strom\` says which, in its results line. A program they already use is fine too:
|
|
95
|
+
\`output/tree.ged\`. If they do not want it, do not bring it up again.
|
|
96
|
+
- **The tree in this conversation, app or not**: they can simply ask you —
|
|
97
|
+
about anyone in the tree, a family, a line, what is proven and by which
|
|
98
|
+
record, what is still missing. Answer from strom (\`strom person show\`,
|
|
99
|
+
\`family show\`, \`research show\`, \`find\`, \`source show\`, \`story show\`,
|
|
100
|
+
\`gaps\`, \`frontier\`) in plain words: names, dates, places and the record
|
|
101
|
+
behind each fact, no IDs. Tell them once that they can ask like this.
|
|
102
|
+
|
|
103
|
+
${SELF_READING}
|
|
104
|
+
${MARKER}
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
function claudeMd(tree) {
|
|
108
|
+
const models = new Settings(tree.env, {}).models("claude", tree.config);
|
|
109
|
+
return `@AGENTS.md
|
|
110
|
+
|
|
111
|
+
Claude Code: use Bash for \`strom\` commands only; read images and documents
|
|
112
|
+
with the Read tool. You can choose a model for a subagent: delegate reading as
|
|
113
|
+
below, not the way "Reading scans" in AGENTS.md says for other agents.
|
|
114
|
+
|
|
115
|
+
${PROFILES.claude.instructions(models)}
|
|
116
|
+
${MARKER}
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
/** An absolute path as a Claude Code permission path ("//Users/x", "//c/Users/x"). */
|
|
120
|
+
export function permissionPath(abs) {
|
|
121
|
+
const p = abs.replace(/\\/g, "/");
|
|
122
|
+
const win = /^([A-Za-z]):\/(.*)$/.exec(p);
|
|
123
|
+
return win ? `//${win[1].toLowerCase()}/${win[2]}` : `/${p}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Permissions for Claude Code in this tree (paths of this computer). File rules are
|
|
127
|
+
* Edit(…) only: Claude Code checks every file-editing tool against them (Write(…)
|
|
128
|
+
* rules match nothing and are reported as a mistake).
|
|
129
|
+
*/
|
|
130
|
+
export function claudeSettings(tree) {
|
|
131
|
+
const settings = new Settings(tree.env, {});
|
|
132
|
+
const shared = settings.shared()?.value;
|
|
133
|
+
const keys = permissionPath(configDir(tree.env));
|
|
134
|
+
// Connectors whose images come through the user's browser: browser tools, for their sites only.
|
|
135
|
+
const browser = browserConnectors(tree.env, shared);
|
|
136
|
+
const sites = [...new Set(browser.flatMap((c) => c.manifest.hosts.map(chromeDomain)))];
|
|
137
|
+
const downloads = permissionPath(settings.downloads());
|
|
138
|
+
const lead = settings.models("claude", tree.config).lead;
|
|
139
|
+
return {
|
|
140
|
+
permissions: {
|
|
141
|
+
allow: [
|
|
142
|
+
"Bash(strom:*)",
|
|
143
|
+
"Read(inputs/**)",
|
|
144
|
+
"Read(output/**)",
|
|
145
|
+
"Read(notes/**)",
|
|
146
|
+
"Read(.strom/views/**)",
|
|
147
|
+
// Files the user drops for the research; scans are seen through views only (strom media view).
|
|
148
|
+
...(shared ? [`Read(${permissionPath(path.join(shared, "inbox"))}/**)`] : []),
|
|
149
|
+
"Edit(notes/**)",
|
|
150
|
+
// A connector it builds for an archive the research needs (strom connector new); strom runs it.
|
|
151
|
+
...(shared ? ["Read", "Edit"].map((t) => `${t}(${permissionPath(path.join(shared, "plugins", "connectors"))}/**)`) : []),
|
|
152
|
+
"WebSearch",
|
|
153
|
+
"WebFetch",
|
|
154
|
+
...(sites.length ? [...CHROME_ALLOW, ...sites] : []),
|
|
155
|
+
],
|
|
156
|
+
deny: [
|
|
157
|
+
"Read(data/**)",
|
|
158
|
+
// The media store: images are looked at through views, so strom knows what was seen.
|
|
159
|
+
...(shared ? [`Read(${permissionPath(path.join(shared, "media"))}/**)`] : []),
|
|
160
|
+
"Edit(data/**)",
|
|
161
|
+
"Edit(strom.json)",
|
|
162
|
+
"Edit(.git/**)",
|
|
163
|
+
"Bash(git:*)",
|
|
164
|
+
// A password is typed by the user in their own terminal, and so is installing a plugin; the seal is strom's.
|
|
165
|
+
// (Consents the agent may ask for — strom allow …: strom asks the person in a window.)
|
|
166
|
+
"Bash(strom login:*)",
|
|
167
|
+
"Bash(strom connector add:*)",
|
|
168
|
+
"Bash(strom connector remove:*)",
|
|
169
|
+
"Bash(strom seal:*)",
|
|
170
|
+
// The limiter's memory (pace, refusals).
|
|
171
|
+
...(shared ? [`Edit(${permissionPath(path.join(shared, "net"))}/**)`] : []),
|
|
172
|
+
// A session ends with its turn: nothing wakes it up later (a live run waited for a wake-up that never came).
|
|
173
|
+
"ScheduleWakeup",
|
|
174
|
+
"CronCreate",
|
|
175
|
+
// The seal keys: an agent that could read them could forge the seal.
|
|
176
|
+
`Read(${keys}/**)`,
|
|
177
|
+
`Edit(${keys}/**)`,
|
|
178
|
+
// What the browser downloads is strom's to take over; the rest of the folder is the user's.
|
|
179
|
+
`Read(${downloads}/**)`,
|
|
180
|
+
...CHROME_DENY,
|
|
181
|
+
// With full permissions only the deny rules count: what the allow list kept away is kept away here.
|
|
182
|
+
...(settings.agentPermissions() === "full" ? BYPASS_DENY : []),
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
// The user's model: the desktop app cannot be given one when it opens (the CLI is, with --model).
|
|
186
|
+
...(lead ? { model: lead } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** Denied as well when the user lets the agent do everything else (agent.permissions full). */
|
|
190
|
+
export const BYPASS_DENY = [
|
|
191
|
+
// its own permissions and instructions
|
|
192
|
+
"Edit(.claude/**)",
|
|
193
|
+
...["AGENTS.md", "CLAUDE.md"].map((f) => `Edit(${f})`),
|
|
194
|
+
// downloads round strom's limiter
|
|
195
|
+
"Bash(curl:*)",
|
|
196
|
+
"Bash(wget:*)",
|
|
197
|
+
];
|
|
198
|
+
/**
|
|
199
|
+
* OpenCode's rules for this tree (opencode.json in the tree folder, found as the
|
|
200
|
+
* project's config): strom commands allowed; the evidence, the seal and git are
|
|
201
|
+
* strom's; a password and installing a plugin the user's own. Paths inside the
|
|
202
|
+
* tree are relative to it; folders outside it (the shared inbox and plugins) are
|
|
203
|
+
* "external directories". Of several matching rules the last one counts, so the
|
|
204
|
+
* general rule comes first.
|
|
205
|
+
*/
|
|
206
|
+
export function opencodeConfig(tree) {
|
|
207
|
+
const settings = new Settings(tree.env, {});
|
|
208
|
+
const shared = settings.shared()?.value;
|
|
209
|
+
const abs = (p) => p.replace(/\\/g, "/");
|
|
210
|
+
const full = settings.agentPermissions() === "full";
|
|
211
|
+
const users = ["strom login *", "strom seal *", "strom connector add *", "strom connector remove *"];
|
|
212
|
+
return {
|
|
213
|
+
$schema: "https://opencode.ai/config.json",
|
|
214
|
+
permission: {
|
|
215
|
+
bash: {
|
|
216
|
+
"*": "ask",
|
|
217
|
+
"strom *": "allow",
|
|
218
|
+
...Object.fromEntries(users.map((c) => [c, "deny"])),
|
|
219
|
+
"git *": "deny",
|
|
220
|
+
...(full ? { "curl *": "deny", "wget *": "deny" } : {}),
|
|
221
|
+
},
|
|
222
|
+
read: { "*": "allow", "data/*": "deny", ".git/*": "deny" },
|
|
223
|
+
edit: {
|
|
224
|
+
"*": "ask",
|
|
225
|
+
"notes/*": "allow",
|
|
226
|
+
"data/*": "deny",
|
|
227
|
+
".git/*": "deny",
|
|
228
|
+
"strom.json": "deny",
|
|
229
|
+
// its own rules and instructions
|
|
230
|
+
"opencode.json": "deny",
|
|
231
|
+
...Object.fromEntries(["AGENTS.md", "CLAUDE.md", ".claude/*"].map((f) => [f, "deny"])),
|
|
232
|
+
},
|
|
233
|
+
external_directory: {
|
|
234
|
+
"*": "ask",
|
|
235
|
+
...(shared
|
|
236
|
+
? {
|
|
237
|
+
[`${abs(path.join(shared, "inbox"))}/*`]: "allow",
|
|
238
|
+
[`${abs(path.join(shared, "plugins", "connectors"))}/*`]: "allow",
|
|
239
|
+
// images are looked at through views, so strom knows what was seen; the limiter's memory is strom's
|
|
240
|
+
[`${abs(path.join(shared, "media"))}/*`]: "deny",
|
|
241
|
+
[`${abs(path.join(shared, "net"))}/*`]: "deny",
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
244
|
+
// the seal keys, and what the browser downloads (strom's to take over)
|
|
245
|
+
[`${abs(configDir(tree.env))}/*`]: "deny",
|
|
246
|
+
[`${abs(settings.downloads())}/*`]: "deny",
|
|
247
|
+
},
|
|
248
|
+
webfetch: "allow",
|
|
249
|
+
websearch: "allow",
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** Files strom wrote for Gemini CLI (gone: Google ended it for personal accounts) — taken away when they are strom's. */
|
|
254
|
+
const OBSOLETE = [
|
|
255
|
+
["GEMINI.md", (t) => t.startsWith("@./AGENTS.md") && t.includes(MARKER) && t.slice(t.indexOf(MARKER) + MARKER.length).trim() === ""],
|
|
256
|
+
[path.join(".gemini", "strom-policy.toml"), (t) => t.startsWith("# strom: generated")],
|
|
257
|
+
];
|
|
258
|
+
/** Keep what the user wrote below the marker. */
|
|
259
|
+
function withUserPart(file, generated) {
|
|
260
|
+
try {
|
|
261
|
+
const old = fs.readFileSync(file, "utf8");
|
|
262
|
+
const i = old.indexOf(MARKER);
|
|
263
|
+
if (i >= 0)
|
|
264
|
+
return generated + old.slice(i + MARKER.length + 1);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// new file
|
|
268
|
+
}
|
|
269
|
+
return generated;
|
|
270
|
+
}
|
|
271
|
+
export const AGENT_FILES = ["AGENTS.md", "CLAUDE.md", path.join(".claude", "settings.json"), "opencode.json"];
|
|
272
|
+
export function syncAgentFiles(tree) {
|
|
273
|
+
const written = [];
|
|
274
|
+
const write = (rel, content) => {
|
|
275
|
+
const file = path.join(tree.root, rel);
|
|
276
|
+
const next = rel.endsWith(".md") ? withUserPart(file, content) : content;
|
|
277
|
+
let cur;
|
|
278
|
+
try {
|
|
279
|
+
cur = fs.readFileSync(file, "utf8");
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
cur = undefined;
|
|
283
|
+
}
|
|
284
|
+
if (cur === next)
|
|
285
|
+
return;
|
|
286
|
+
if (!tree.dryRun)
|
|
287
|
+
writeFileAtomic(file, next);
|
|
288
|
+
written.push(rel.split(path.sep).join("/"));
|
|
289
|
+
};
|
|
290
|
+
write("AGENTS.md", agentsMd(tree));
|
|
291
|
+
write("CLAUDE.md", claudeMd(tree));
|
|
292
|
+
write(path.join(".claude", "settings.json"), JSON.stringify(claudeSettings(tree), null, 2) + "\n");
|
|
293
|
+
write("opencode.json", JSON.stringify(opencodeConfig(tree), null, 2) + "\n");
|
|
294
|
+
for (const [rel, ours] of OBSOLETE) {
|
|
295
|
+
const file = path.join(tree.root, rel);
|
|
296
|
+
let text;
|
|
297
|
+
try {
|
|
298
|
+
text = fs.readFileSync(file, "utf8");
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (!ours(text))
|
|
304
|
+
continue; // the user's own
|
|
305
|
+
if (!tree.dryRun) {
|
|
306
|
+
fs.rmSync(file);
|
|
307
|
+
if (path.dirname(rel) !== "." && fs.readdirSync(path.dirname(file)).length === 0)
|
|
308
|
+
fs.rmdirSync(path.dirname(file));
|
|
309
|
+
}
|
|
310
|
+
written.push(rel.split(path.sep).join("/"));
|
|
311
|
+
}
|
|
312
|
+
return written;
|
|
313
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Teaching an agent CLI about strom in any folder: a user who opens Claude
|
|
2
|
+
// Code, Codex or Antigravity anywhere and says "I want to research my ancestors"
|
|
3
|
+
// gets an agent that knows strom is there and how to start. Claude Code gets
|
|
4
|
+
// a skill of its own; Codex and Antigravity a marked block in their
|
|
5
|
+
// global instruction file (the user's own text around it is kept) — and in
|
|
6
|
+
// the settings of Claude Code (also its desktop app) and Antigravity, strom
|
|
7
|
+
// allowed: an agent opened anywhere runs strom without asking each time (what
|
|
8
|
+
// is the user's — a password, a consent — strom itself keeps for the user).
|
|
9
|
+
// OpenCode gets a file of its own named in its global config (a global
|
|
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.
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { userHome } from "../core/paths.js";
|
|
15
|
+
import { writeFileAtomic } from "../core/json.js";
|
|
16
|
+
import { installation } from "../core/self.js";
|
|
17
|
+
const BEGIN = "<!-- strom: begin (strom agents install; removed by strom agents uninstall) -->";
|
|
18
|
+
const END = "<!-- strom: end -->";
|
|
19
|
+
export const GLOBAL_TEXT = `# strom — family history research
|
|
20
|
+
|
|
21
|
+
strom (the \`strom\` command) is installed on this computer: a tool for
|
|
22
|
+
researching a family's history with you as the researcher — people, parish
|
|
23
|
+
registers, archives, evidence, a family tree the Strom app can import.
|
|
24
|
+
Use it whenever the user wants to research their ancestors or family, or
|
|
25
|
+
mentions strom, a family tree or the Strom app.
|
|
26
|
+
|
|
27
|
+
1. Run \`strom\` first. It says where the research stands and what to do next
|
|
28
|
+
(\`strom --json\` for the same as data; \`strom guide\` explains the work).
|
|
29
|
+
2. Not set up yet: ask the user where to keep the research (suggest
|
|
30
|
+
~/Documents/Strom) and which language they want, then run
|
|
31
|
+
\`strom setup --home "<folder>" --lang <code> --yes\`, then
|
|
32
|
+
\`strom init "<name of the family tree>"\`.
|
|
33
|
+
3. Talk to the user in their language. Ask them before decisions: whom to
|
|
34
|
+
research, what they already know, anything that costs money or needs
|
|
35
|
+
their permission.
|
|
36
|
+
4. The research lives in a family tree folder; change it ONLY through
|
|
37
|
+
\`strom\` commands, never by editing its files. In that folder its own
|
|
38
|
+
AGENTS.md says the rest.
|
|
39
|
+
5. You work best started by strom: it opens you in the tree folder with the
|
|
40
|
+
right permissions. Tell the user once: next time run \`strom\` in a
|
|
41
|
+
terminal (or the "Strom research" shortcut on the desktop) and choose
|
|
42
|
+
"Continue the research with the agent".
|
|
43
|
+
6. When strom needs the user's consent it opens a window on their screen —
|
|
44
|
+
tell them to answer it; you cannot. Exit code 4 means the user must run
|
|
45
|
+
the given command themselves.
|
|
46
|
+
7. Scans come through strom. An archive the research needs has no
|
|
47
|
+
downloader (connector) yet: build one yourself, unasked
|
|
48
|
+
(\`strom connector new\`), and tell the user in a sentence.
|
|
49
|
+
8. The Strom app (https://stromapp.info) is strom's companion: a free family
|
|
50
|
+
tree app, no account, the data stay on the user's computer. The research's
|
|
51
|
+
result (\`output/tree-strom.ged\`) opens in it. When the user wants to see
|
|
52
|
+
the tree, suggest it gently — best installed as an app from the browser,
|
|
53
|
+
from https://stromapp.info/run/ (\`strom app install\` opens it there; it
|
|
54
|
+
then works offline); \`strom app\` opens it — with the research, followed
|
|
55
|
+
live while you work, when the app can take it. Without it, the user can ask
|
|
56
|
+
you about anyone in the tree: answer from \`strom person show\` and the like.
|
|
57
|
+
`;
|
|
58
|
+
/**
|
|
59
|
+
* The rules that let the agent run strom without asking, in its own settings' words.
|
|
60
|
+
* Claude Code: `strom` on PATH, and the installer's command by its path — right after
|
|
61
|
+
* the installer an agent calls it so, until a new terminal has it on PATH.
|
|
62
|
+
*/
|
|
63
|
+
function allowRules(agent) {
|
|
64
|
+
if (agent === "antigravity")
|
|
65
|
+
return ["command(strom)"];
|
|
66
|
+
return ["Bash(strom:*)", ...(installation().launchers ?? []).map((p) => `Bash(${p}:*)`)];
|
|
67
|
+
}
|
|
68
|
+
/** A rule of strom's in Claude Code's or Antigravity's settings, whichever installation wrote it. */
|
|
69
|
+
function isStromRule(agent, rule) {
|
|
70
|
+
if (agent === "antigravity")
|
|
71
|
+
return rule === "command(strom)";
|
|
72
|
+
return /^Bash\((?:.*[\\/])?strom(?:\.exe|\.cmd)?:\*\)$/.test(rule);
|
|
73
|
+
}
|
|
74
|
+
/** OpenCode: the rule that lets it run strom without asking. */
|
|
75
|
+
const OPENCODE_STROM = "strom *";
|
|
76
|
+
/** OpenCode's global config folder (it follows XDG on every system). */
|
|
77
|
+
function opencodeDir(env) {
|
|
78
|
+
return path.join(env.XDG_CONFIG_HOME ?? path.join(userHome(env), ".config"), "opencode");
|
|
79
|
+
}
|
|
80
|
+
export function globalTargets(env) {
|
|
81
|
+
const home = userHome(env);
|
|
82
|
+
return [
|
|
83
|
+
{ agent: "claude", file: path.join(env.CLAUDE_CONFIG_DIR ?? path.join(home, ".claude"), "skills", "strom", "SKILL.md"), kind: "own" },
|
|
84
|
+
{ agent: "claude", file: path.join(env.CLAUDE_CONFIG_DIR ?? path.join(home, ".claude"), "settings.json"), kind: "allow" },
|
|
85
|
+
{ agent: "codex", file: path.join(env.CODEX_HOME ?? path.join(home, ".codex"), "AGENTS.md"), kind: "block" },
|
|
86
|
+
// Antigravity CLI reads its global rules where Gemini CLI did, and its permissions from its settings.
|
|
87
|
+
{ agent: "antigravity", file: path.join(home, ".gemini", "GEMINI.md"), kind: "block" },
|
|
88
|
+
{ agent: "antigravity", file: path.join(home, ".gemini", "antigravity-cli", "settings.json"), kind: "allow" },
|
|
89
|
+
{ agent: "opencode", file: path.join(opencodeDir(env), "strom.md"), kind: "own" },
|
|
90
|
+
{ agent: "opencode", file: path.join(opencodeDir(env), "opencode.json"), kind: "allow" },
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
const SKILL = `---
|
|
94
|
+
name: strom
|
|
95
|
+
description: Family history and genealogy research with the strom command — ancestors, family trees, parish registers and archives, evidence, GEDCOM and the Strom app. Use when the user wants to research their family or mentions strom or the Strom app.
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
${GLOBAL_TEXT}`;
|
|
99
|
+
function read(file) {
|
|
100
|
+
try {
|
|
101
|
+
return fs.readFileSync(file, "utf8");
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function withoutBlock(text) {
|
|
108
|
+
const i = text.indexOf(BEGIN);
|
|
109
|
+
const j = text.indexOf(END);
|
|
110
|
+
if (i < 0 || j < i)
|
|
111
|
+
return text;
|
|
112
|
+
const before = text.slice(0, i).replace(/\s+$/, "");
|
|
113
|
+
const after = text.slice(j + END.length).replace(/^\s+/, "");
|
|
114
|
+
const rest = before && after ? `${before}\n\n${after}` : before || after;
|
|
115
|
+
return rest ? `${rest.replace(/\s+$/, "")}\n` : "";
|
|
116
|
+
}
|
|
117
|
+
/** The agent's settings as JSON, or undefined when they are not (strom then leaves them alone). */
|
|
118
|
+
function settingsOf(text) {
|
|
119
|
+
if (text === undefined || !text.trim())
|
|
120
|
+
return {};
|
|
121
|
+
try {
|
|
122
|
+
const v = JSON.parse(text);
|
|
123
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function allowList(s) {
|
|
130
|
+
const p = (s.permissions ?? {});
|
|
131
|
+
return Array.isArray(p.allow) ? p.allow : [];
|
|
132
|
+
}
|
|
133
|
+
function isObject(v) {
|
|
134
|
+
return Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
135
|
+
}
|
|
136
|
+
/** Is strom allowed in these settings (and, for OpenCode, its instructions named)? */
|
|
137
|
+
function hasAllow(t, s) {
|
|
138
|
+
if (t.agent !== "opencode")
|
|
139
|
+
return allowRules(t.agent).every((r) => allowList(s).includes(r));
|
|
140
|
+
const bash = isObject(s.permission) ? s.permission.bash : undefined;
|
|
141
|
+
const own = path.join(path.dirname(t.file), "strom.md");
|
|
142
|
+
return isObject(bash) && bash[OPENCODE_STROM] === "allow" && Array.isArray(s.instructions) && s.instructions.includes(own);
|
|
143
|
+
}
|
|
144
|
+
function addAllow(t, s) {
|
|
145
|
+
if (t.agent !== "opencode") {
|
|
146
|
+
const have = allowList(s);
|
|
147
|
+
s.permissions = { ...(s.permissions ?? {}), allow: [...have, ...allowRules(t.agent).filter((r) => !have.includes(r))] };
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const perm = isObject(s.permission) ? s.permission : typeof s.permission === "string" ? { "*": s.permission } : {};
|
|
151
|
+
// A plain value for bash ("ask") becomes the general rule; strom's comes after it (the last match counts).
|
|
152
|
+
const bash = isObject(perm.bash) ? perm.bash : typeof perm.bash === "string" ? { "*": perm.bash } : {};
|
|
153
|
+
s.permission = { ...perm, bash: { ...bash, [OPENCODE_STROM]: "allow" } };
|
|
154
|
+
const own = path.join(path.dirname(t.file), "strom.md");
|
|
155
|
+
const list = Array.isArray(s.instructions) ? s.instructions : [];
|
|
156
|
+
if (!list.includes(own))
|
|
157
|
+
s.instructions = [...list, own];
|
|
158
|
+
}
|
|
159
|
+
function removeAllow(t, s) {
|
|
160
|
+
if (t.agent !== "opencode") {
|
|
161
|
+
const rest = allowList(s).filter((r) => !isStromRule(t.agent, r));
|
|
162
|
+
const perms = { ...s.permissions };
|
|
163
|
+
if (rest.length)
|
|
164
|
+
perms.allow = rest;
|
|
165
|
+
else
|
|
166
|
+
delete perms.allow;
|
|
167
|
+
if (Object.keys(perms).length)
|
|
168
|
+
s.permissions = perms;
|
|
169
|
+
else
|
|
170
|
+
delete s.permissions;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const own = path.join(path.dirname(t.file), "strom.md");
|
|
174
|
+
if (Array.isArray(s.instructions)) {
|
|
175
|
+
const rest = s.instructions.filter((i) => i !== own);
|
|
176
|
+
if (rest.length)
|
|
177
|
+
s.instructions = rest;
|
|
178
|
+
else
|
|
179
|
+
delete s.instructions;
|
|
180
|
+
}
|
|
181
|
+
if (isObject(s.permission) && isObject(s.permission.bash)) {
|
|
182
|
+
const bash = { ...s.permission.bash };
|
|
183
|
+
delete bash[OPENCODE_STROM];
|
|
184
|
+
const perm = { ...s.permission };
|
|
185
|
+
if (Object.keys(bash).length)
|
|
186
|
+
perm.bash = bash;
|
|
187
|
+
else
|
|
188
|
+
delete perm.bash;
|
|
189
|
+
if (Object.keys(perm).length)
|
|
190
|
+
s.permission = perm;
|
|
191
|
+
else
|
|
192
|
+
delete s.permission;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Write it for one agent; false when it was already there as it is. */
|
|
196
|
+
export function installGlobal(t) {
|
|
197
|
+
const cur = read(t.file);
|
|
198
|
+
let next;
|
|
199
|
+
if (t.kind === "allow") {
|
|
200
|
+
const s = settingsOf(cur);
|
|
201
|
+
if (!s || hasAllow(t, s))
|
|
202
|
+
return false;
|
|
203
|
+
addAllow(t, s);
|
|
204
|
+
next = JSON.stringify(s, null, 2) + "\n";
|
|
205
|
+
}
|
|
206
|
+
else if (t.kind === "own")
|
|
207
|
+
next = t.agent === "claude" ? SKILL : GLOBAL_TEXT;
|
|
208
|
+
else {
|
|
209
|
+
const rest = cur ? withoutBlock(cur).replace(/\s+$/, "") : "";
|
|
210
|
+
next = `${rest ? `${rest}\n\n` : ""}${BEGIN}\n${GLOBAL_TEXT}${END}\n`;
|
|
211
|
+
}
|
|
212
|
+
if (cur === next)
|
|
213
|
+
return false;
|
|
214
|
+
fs.mkdirSync(path.dirname(t.file), { recursive: true });
|
|
215
|
+
writeFileAtomic(t.file, next);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
/** Take it away again; false when there was nothing of strom's. */
|
|
219
|
+
export function uninstallGlobal(t) {
|
|
220
|
+
const cur = read(t.file);
|
|
221
|
+
if (cur === undefined)
|
|
222
|
+
return false;
|
|
223
|
+
if (t.kind === "allow") {
|
|
224
|
+
const s = settingsOf(cur);
|
|
225
|
+
// Any of strom's rules, also those another installation of strom wrote.
|
|
226
|
+
const any = t.agent === "opencode" ? s && hasAllow(t, s) : s && allowList(s).some((r) => isStromRule(t.agent, r));
|
|
227
|
+
if (!s || !any)
|
|
228
|
+
return false;
|
|
229
|
+
removeAllow(t, s);
|
|
230
|
+
writeFileAtomic(t.file, JSON.stringify(s, null, 2) + "\n");
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (t.kind === "own") {
|
|
234
|
+
// The skill is a folder of its own; OpenCode's instructions one file among the user's.
|
|
235
|
+
if (t.agent === "claude")
|
|
236
|
+
fs.rmSync(path.dirname(t.file), { recursive: true, force: true });
|
|
237
|
+
else
|
|
238
|
+
fs.rmSync(t.file, { force: true });
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
if (!cur.includes(BEGIN))
|
|
242
|
+
return false;
|
|
243
|
+
const rest = withoutBlock(cur);
|
|
244
|
+
if (rest.trim())
|
|
245
|
+
writeFileAtomic(t.file, rest);
|
|
246
|
+
else
|
|
247
|
+
fs.rmSync(t.file);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
export function isInstalled(t) {
|
|
251
|
+
const cur = read(t.file);
|
|
252
|
+
if (cur === undefined)
|
|
253
|
+
return false;
|
|
254
|
+
if (t.kind === "allow")
|
|
255
|
+
return hasAllow(t, settingsOf(cur) ?? {});
|
|
256
|
+
return t.kind === "own" || cur.includes(BEGIN);
|
|
257
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Starting an agent CLI for a conversation with the user, set up the way the
|
|
2
|
+
// user chose once (agent.permissions) — so a person who is not technical never
|
|
3
|
+
// has to know an agent's own switches. Each agent names the levels its own way:
|
|
4
|
+
//
|
|
5
|
+
// level Claude Code Codex
|
|
6
|
+
// ask its own mode (it asks) sandbox on the folder, asks on request
|
|
7
|
+
// auto --permission-mode auto sandbox on the folder, own review
|
|
8
|
+
// full bypassPermissions no sandbox, no approvals
|
|
9
|
+
//
|
|
10
|
+
// Antigravity CLI (agy): ask — its own mode; auto — accept-edits; full —
|
|
11
|
+
// --dangerously-skip-permissions; `strom` is allowed in its settings (strom agents install).
|
|
12
|
+
// OpenCode: ask and auto — the tree's rules (opencode.json), anything else it
|
|
13
|
+
// asks (it has no review of its own); full — --auto, everything the rules do not deny.
|
|
14
|
+
//
|
|
15
|
+
// Claude Code always gets the tree's allow and deny lists (--settings); Codex
|
|
16
|
+
// works in its sandbox on the tree folder with the shared folder added and the
|
|
17
|
+
// network on (strom fetches).
|
|
18
|
+
import { claudeArgs } from "../runners/claude.js";
|
|
19
|
+
/** The command line of a conversation with this agent. */
|
|
20
|
+
export function conversationArgs(agent, o) {
|
|
21
|
+
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 } : {}) });
|
|
23
|
+
if (agent === "codex") {
|
|
24
|
+
const args = o.level === "full"
|
|
25
|
+
? ["--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.shared ? ["--add-dir", o.shared] : [])];
|
|
27
|
+
return [...args, "--search", ...(o.model ? ["--model", o.model] : []), o.kickoff];
|
|
28
|
+
}
|
|
29
|
+
if (agent === "antigravity") {
|
|
30
|
+
const mode = o.level === "full" ? ["--dangerously-skip-permissions"] : o.level === "auto" ? ["--mode", "accept-edits"] : [];
|
|
31
|
+
return [...mode, ...(o.shared ? ["--add-dir", o.shared] : []), ...(o.model ? ["--model", o.model] : []), "--prompt-interactive", o.kickoff];
|
|
32
|
+
}
|
|
33
|
+
if (agent === "opencode")
|
|
34
|
+
return [...(o.level === "full" ? ["--auto"] : []), ...(o.model ? ["--model", o.model] : []), "--prompt", o.kickoff];
|
|
35
|
+
throw new Error(`no conversation launcher for agent "${agent}"`);
|
|
36
|
+
}
|