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.
Files changed (130) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +142 -0
  3. package/assets/lang/cs.json +302 -0
  4. package/assets/lang/de.json +302 -0
  5. package/assets/method/core.md +43 -0
  6. package/assets/method/enrich.md +11 -0
  7. package/assets/method/intake.md +30 -0
  8. package/assets/method/link.md +28 -0
  9. package/assets/method/locate.md +28 -0
  10. package/assets/method/narrate.md +13 -0
  11. package/assets/method/reading.md +62 -0
  12. package/assets/method/recording.md +59 -0
  13. package/assets/method/request.md +10 -0
  14. package/assets/method/verify.md +17 -0
  15. package/assets/plugins/README.md +23 -0
  16. package/assets/plugins/connectors/DISCOVERY.md +159 -0
  17. package/assets/plugins/connectors/README.md +376 -0
  18. package/assets/plugins/connectors/sdk.ts +168 -0
  19. package/assets/plugins/connectors/template.ts +38 -0
  20. package/assets/plugins/gitignore +4 -0
  21. package/dist/agents/files.js +313 -0
  22. package/dist/agents/global.js +257 -0
  23. package/dist/agents/launch.js +36 -0
  24. package/dist/agents/profiles.js +95 -0
  25. package/dist/brief/brief.js +345 -0
  26. package/dist/cli/commit.js +44 -0
  27. package/dist/cli/context.js +311 -0
  28. package/dist/cli/execute.js +154 -0
  29. package/dist/cli/fixes.js +78 -0
  30. package/dist/cli/format.js +53 -0
  31. package/dist/cli/help.js +59 -0
  32. package/dist/cli/main.js +152 -0
  33. package/dist/cli/menu.js +212 -0
  34. package/dist/cli/registry.js +96 -0
  35. package/dist/cli/ui.js +266 -0
  36. package/dist/cli/wizard.js +142 -0
  37. package/dist/cli.js +14 -0
  38. package/dist/commands/analysis.js +622 -0
  39. package/dist/commands/batch.js +181 -0
  40. package/dist/commands/checks.js +153 -0
  41. package/dist/commands/connectors.js +1377 -0
  42. package/dist/commands/guide.js +160 -0
  43. package/dist/commands/index.js +19 -0
  44. package/dist/commands/intake.js +234 -0
  45. package/dist/commands/media.js +406 -0
  46. package/dist/commands/meta.js +195 -0
  47. package/dist/commands/output.js +117 -0
  48. package/dist/commands/people.js +664 -0
  49. package/dist/commands/read.js +199 -0
  50. package/dist/commands/research.js +139 -0
  51. package/dist/commands/session.js +605 -0
  52. package/dist/commands/setup.js +465 -0
  53. package/dist/commands/sources.js +634 -0
  54. package/dist/commands/start.js +383 -0
  55. package/dist/commands/story.js +75 -0
  56. package/dist/commands/tasks.js +436 -0
  57. package/dist/commands/trees.js +128 -0
  58. package/dist/core/actions.js +852 -0
  59. package/dist/core/age.js +95 -0
  60. package/dist/core/apps.js +74 -0
  61. package/dist/core/assets.js +34 -0
  62. package/dist/core/awake.js +33 -0
  63. package/dist/core/browser.js +281 -0
  64. package/dist/core/calibration.js +48 -0
  65. package/dist/core/check.js +112 -0
  66. package/dist/core/chromium.js +88 -0
  67. package/dist/core/config.js +348 -0
  68. package/dist/core/connector.js +811 -0
  69. package/dist/core/deps.js +73 -0
  70. package/dist/core/dialog.js +61 -0
  71. package/dist/core/errors.js +89 -0
  72. package/dist/core/evidence.js +58 -0
  73. package/dist/core/frontier.js +219 -0
  74. package/dist/core/gdate.js +77 -0
  75. package/dist/core/git.js +300 -0
  76. package/dist/core/guard.js +124 -0
  77. package/dist/core/http2.js +76 -0
  78. package/dist/core/import.js +541 -0
  79. package/dist/core/install.js +28 -0
  80. package/dist/core/integrity.js +219 -0
  81. package/dist/core/json.js +87 -0
  82. package/dist/core/lang.js +70 -0
  83. package/dist/core/live.js +244 -0
  84. package/dist/core/lock.js +112 -0
  85. package/dist/core/logins.js +67 -0
  86. package/dist/core/media.js +223 -0
  87. package/dist/core/model.js +101 -0
  88. package/dist/core/net.js +366 -0
  89. package/dist/core/open.js +29 -0
  90. package/dist/core/paths.js +84 -0
  91. package/dist/core/people.js +283 -0
  92. package/dist/core/phrases.js +85 -0
  93. package/dist/core/queue.js +113 -0
  94. package/dist/core/reader.js +76 -0
  95. package/dist/core/records.js +105 -0
  96. package/dist/core/roles.js +30 -0
  97. package/dist/core/schema.js +261 -0
  98. package/dist/core/seal.js +77 -0
  99. package/dist/core/self.js +40 -0
  100. package/dist/core/session.js +155 -0
  101. package/dist/core/shortcut.js +90 -0
  102. package/dist/core/stories.js +61 -0
  103. package/dist/core/stromapp.js +138 -0
  104. package/dist/core/text.js +104 -0
  105. package/dist/core/tree.js +507 -0
  106. package/dist/core/uninstall.js +128 -0
  107. package/dist/core/update.js +193 -0
  108. package/dist/core/validate.js +260 -0
  109. package/dist/core/views.js +164 -0
  110. package/dist/core/which.js +51 -0
  111. package/dist/core/workers.js +42 -0
  112. package/dist/gedcom/export.js +454 -0
  113. package/dist/gedcom/labels.js +103 -0
  114. package/dist/gedcom/lines.js +91 -0
  115. package/dist/gedcom/parse.js +53 -0
  116. package/dist/gedcom/validate.js +183 -0
  117. package/dist/image/image.js +223 -0
  118. package/dist/image/index.js +114 -0
  119. package/dist/image/jpeg-decode.js +552 -0
  120. package/dist/image/jpeg-encode.js +254 -0
  121. package/dist/image/png.js +241 -0
  122. package/dist/runners/antigravity.js +70 -0
  123. package/dist/runners/claude.js +179 -0
  124. package/dist/runners/codex.js +45 -0
  125. package/dist/runners/index.js +13 -0
  126. package/dist/runners/jsonl.js +86 -0
  127. package/dist/runners/opencode.js +50 -0
  128. package/dist/runners/runner.js +63 -0
  129. package/dist/runners/script.js +58 -0
  130. 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
+ }