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,181 @@
1
+ // strom batch — several writing commands as one change.
2
+ //
3
+ // Recording one register entry takes half a dozen commands (the source, the
4
+ // child, the parents, the family, the baptism with its godparents). As single
5
+ // calls that is half a dozen agent turns, each paying for its tool call and
6
+ // for git. A batch runs them in one process: one integrity check, one
7
+ // commit, all or nothing. A line may end in "#name" to label the record it
8
+ // creates; later lines use it as "@name".
9
+ import fs from "node:fs";
10
+ import { register } from "../cli/registry.js";
11
+ import { lines, table } from "../cli/format.js";
12
+ import { AmbiguousError, StromError, UsageError } from "../core/errors.js";
13
+ /** A label: a word in any script ("anna", "šimon", "otec_2"), its accents composed or not. */
14
+ const LABEL = "[\\p{L}_][\\p{L}\\p{M}\\p{N}_-]*";
15
+ import { checkArgs, parseOptions, resolveCommand } from "../cli/execute.js";
16
+ /** Shell-like words: "double" (with \" \\ escapes), 'single', backslash escapes, # comments. */
17
+ function tokenize(line) {
18
+ const out = [];
19
+ let cur = "";
20
+ let quoted = false;
21
+ let started = false;
22
+ let q = null;
23
+ for (let i = 0; i < line.length; i++) {
24
+ const ch = line[i];
25
+ if (q === "'") {
26
+ if (ch === "'")
27
+ q = null;
28
+ else
29
+ cur += ch;
30
+ continue;
31
+ }
32
+ if (q === '"') {
33
+ if (ch === "\\" && i + 1 < line.length && '"\\$`'.includes(line[i + 1]))
34
+ cur += line[++i];
35
+ else if (ch === '"')
36
+ q = null;
37
+ else
38
+ cur += ch;
39
+ continue;
40
+ }
41
+ if (ch === "'" || ch === '"') {
42
+ q = ch;
43
+ quoted = true;
44
+ started = true;
45
+ continue;
46
+ }
47
+ if (ch === "\\" && i + 1 < line.length) {
48
+ cur += line[++i];
49
+ started = true;
50
+ continue;
51
+ }
52
+ if (/\s/.test(ch)) {
53
+ if (started)
54
+ out.push({ text: cur, quoted });
55
+ cur = "";
56
+ quoted = false;
57
+ started = false;
58
+ continue;
59
+ }
60
+ if (ch === "#" && !started && out.length === 0)
61
+ return out; // a comment line
62
+ cur += ch;
63
+ started = true;
64
+ }
65
+ if (q)
66
+ throw new UsageError(`unclosed ${q === '"' ? "double" : "single"} quote`);
67
+ if (started)
68
+ out.push({ text: cur, quoted });
69
+ return out;
70
+ }
71
+ /** Lines of a batch: blank lines and # comments skipped, "\" at the end continues a line. */
72
+ export function parseBatch(text) {
73
+ const out = [];
74
+ const raw = text.replace(/\r\n?/g, "\n").split("\n");
75
+ for (let i = 0; i < raw.length; i++) {
76
+ const n = i + 1;
77
+ // "\" at the end continues the line — also with spaces after it, and at the very end of the file
78
+ let line = raw[i].trimEnd();
79
+ while (line.endsWith("\\"))
80
+ line = line.slice(0, -1) + (i + 1 < raw.length ? " " + raw[++i].trimEnd() : "");
81
+ let tokens;
82
+ try {
83
+ tokens = tokenize(line);
84
+ }
85
+ catch (err) {
86
+ throw new UsageError(`line ${n}: ${err.message}`);
87
+ }
88
+ if (tokens.length === 0)
89
+ continue;
90
+ if (tokens[0].text === "strom" && !tokens[0].quoted)
91
+ tokens.shift();
92
+ const last = tokens.at(-1);
93
+ let label;
94
+ if (last && !last.quoted && last.text.startsWith("#")) {
95
+ if (!new RegExp(`^#${LABEL}$`, "u").test(last.text))
96
+ throw new UsageError(`line ${n}: invalid label "${last.text}"`, { hint: "a label is a word: #anna, #šimon, #otec_2" });
97
+ label = last.text.slice(1).normalize("NFC");
98
+ tokens.pop();
99
+ }
100
+ if (tokens.length)
101
+ out.push({ n, argv: tokens.map((t) => t.text), ...(label ? { label } : {}) });
102
+ }
103
+ return out;
104
+ }
105
+ /** Commands that make no sense inside a batch. */
106
+ const NOT_IN_BATCH = new Set(["batch", "run", "session start", "session close", "init", "setup"]);
107
+ function lineError(l, err) {
108
+ const where = `line ${l.n} (${l.argv.slice(0, 2).join(" ")})`;
109
+ if (err instanceof AmbiguousError)
110
+ return new UsageError(`${where}: ${err.message}`, { hint: err.hint ?? "", details: { candidates: err.candidates } });
111
+ if (err instanceof StromError)
112
+ return new StromError(`${where}: ${err.message}`, { exitCode: err.exitCode, ...(err.hint ? { hint: err.hint } : {}), ...(err.details !== undefined ? { details: err.details } : {}) });
113
+ return new StromError(`${where}: ${err.message}`);
114
+ }
115
+ register({
116
+ path: ["batch"],
117
+ summary: "Run several writing commands as one change: one call, one commit, all or nothing",
118
+ group: "people",
119
+ tree: true,
120
+ writes: true,
121
+ description: "Each argument is one command line (without \"strom\"); or give --file, or pipe the lines in.\n" +
122
+ "End a line with #name to label the record it creates (P…, F…, S…, E…); use it later as @name.\n" +
123
+ "If any line fails, nothing is written. Lines starting with # are comments.",
124
+ args: [{ name: "commands", description: "command lines, e.g. 'person add \"Anna /Svobodová/\" --sex F #anna'", variadic: true }],
125
+ options: [{ name: "file", type: "string", value: "<file>", description: "read the command lines from this file" }],
126
+ examples: [
127
+ `strom batch 'person add "Anna /Svobodová/" --sex F #anna' 'family add --partner P0001 --partner @anna --married 1910'`,
128
+ "strom batch --file krest-1885.txt",
129
+ ],
130
+ run: async (ctx, { args, opts }) => {
131
+ const source = args.length
132
+ ? args.join("\n")
133
+ : typeof opts.file === "string"
134
+ ? fs.readFileSync(ctx.resolvePath(opts.file), "utf8")
135
+ : (ctx.io.stdinText?.() ?? "");
136
+ const batch = parseBatch(source);
137
+ if (batch.length === 0)
138
+ throw new UsageError("no commands in the batch", { hint: "strom help batch" });
139
+ const tree = ctx.tree();
140
+ const labels = new Map();
141
+ const report = [];
142
+ for (const l of batch) {
143
+ const argv = l.argv.map((t) => t.replace(new RegExp(`@(${LABEL})`, "gu"), (m, name) => {
144
+ if (labels.has(name.normalize("NFC")))
145
+ return labels.get(name.normalize("NFC"));
146
+ return m;
147
+ }));
148
+ // "@anna" or "godparent:@anna" left over = a label no earlier line defined.
149
+ const unknown = argv.map((t) => new RegExp(`(?:^|[:=])@(${LABEL})$`, "u").exec(t)?.[1]).find(Boolean);
150
+ if (unknown)
151
+ throw lineError(l, new UsageError(`@${unknown} is not defined`, { hint: `end the line that creates it with #${unknown}` }));
152
+ try {
153
+ const { def, rest } = resolveCommand(argv);
154
+ const name = def.path.join(" ");
155
+ if (!def.writes || !def.tree || NOT_IN_BATCH.has(name))
156
+ throw new UsageError(`strom ${name || "(orientation)"} cannot run in a batch`, { hint: "a batch runs commands that write to the tree: person/family/event/source/cite/task/search …" });
157
+ const parsed = parseOptions(def, rest);
158
+ if (parsed.values.tree)
159
+ throw new UsageError("--tree applies to the whole batch: strom batch --tree <name> …");
160
+ checkArgs(def, parsed.positionals);
161
+ const before = tree.written.length;
162
+ const r = await def.run(ctx, { args: parsed.positionals, opts: parsed.values });
163
+ if (r.exitCode)
164
+ throw new StromError(r.text.split("\n")[0] ?? "failed");
165
+ const ops = tree.written.slice(before);
166
+ const created = ops.map((o) => /^\+([A-Z]\d{4,})/.exec(o.summary)?.[1]).find(Boolean);
167
+ if (l.label) {
168
+ if (!created)
169
+ throw new UsageError(`#${l.label}: this line creates no record to label`);
170
+ labels.set(l.label, created);
171
+ }
172
+ report.push({ line: l.n, command: name, ...(created ? { created } : {}), ...(l.label ? { label: l.label } : {}), summaries: ops.map((o) => o.summary) });
173
+ }
174
+ catch (err) {
175
+ throw lineError(l, err);
176
+ }
177
+ }
178
+ const text = lines(table(report.flatMap((r) => r.summaries.map((s, i) => [i === 0 ? `${r.line}` : "", s, i === 0 && r.label ? `#${r.label}` : ""]))), `${report.length} command(s) as one change${tree.dryRun ? " (dry run — nothing written)" : ""}`);
179
+ return { text, data: { lines: report, labels: Object.fromEntries(labels) } };
180
+ },
181
+ });
@@ -0,0 +1,153 @@
1
+ // check · verify · guard · repair · seal adopt · history
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { register } from "../cli/registry.js";
5
+ import { lines, table } from "../cli/format.js";
6
+ import { check } from "../core/check.js";
7
+ import { guard } from "../core/guard.js";
8
+ import { verifyFast, verifyFull } from "../core/integrity.js";
9
+ import { createKey, verifyCommitSeal } from "../core/seal.js";
10
+ import { NeedsConsentError, StromError } from "../core/errors.js";
11
+ import * as git from "../core/git.js";
12
+ export function report(findings, okText) {
13
+ const errors = findings.filter((f) => f.level === "error");
14
+ const text = findings.length === 0
15
+ ? okText
16
+ : lines(table(findings.map((f) => [f.level === "error" ? "ERROR" : "warn", f.code, f.id ?? f.file ?? "", f.message, f.hint ? `→ ${f.hint}` : ""])), "", `${errors.length} error(s), ${findings.length - errors.length} warning(s)`);
17
+ return { text, data: { ok: errors.length === 0, findings }, exitCode: errors.length ? 1 : 0 };
18
+ }
19
+ register({
20
+ path: ["check"],
21
+ summary: "Is the evidence consistent and untouched? (schema, references, seals, losses)",
22
+ group: "history",
23
+ tree: true,
24
+ run(ctx) {
25
+ const tree = ctx.tree();
26
+ return report([...verifyFull(tree).findings, ...check(tree), ...guard(tree)], "ok — data consistent, sealed, nothing lost");
27
+ },
28
+ }, {
29
+ path: ["verify"],
30
+ summary: "Was anything changed outside strom? (seal check of every record and commit)",
31
+ group: "history",
32
+ tree: true,
33
+ options: [{ name: "fast", type: "boolean", description: "only what changed since the last commit (what runs before every write)" }],
34
+ run(ctx, { opts }) {
35
+ const tree = ctx.tree();
36
+ return report((opts.fast ? verifyFast(tree) : verifyFull(tree)).findings, "ok — every record matches its seal");
37
+ },
38
+ }, {
39
+ path: ["guard"],
40
+ summary: "Did anything get lost since the last commit?",
41
+ group: "history",
42
+ tree: true,
43
+ run(ctx) {
44
+ return report(guard(ctx.tree()), "ok — nothing lost since the last commit");
45
+ },
46
+ });
47
+ function quarantine(tree, rel, stamp) {
48
+ const abs = path.join(tree.root, rel);
49
+ if (!fs.existsSync(abs))
50
+ return;
51
+ const dest = path.join(tree.root, ".strom", "quarantine", stamp, rel);
52
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
53
+ fs.copyFileSync(abs, dest);
54
+ }
55
+ /** Return data/ and strom.json to the newest sealed commit. */
56
+ function repair(tree) {
57
+ const key = tree.requireKey();
58
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
59
+ const head = git.headInfo(tree.root);
60
+ const sealed = git.findCommit(tree.root, (t, m) => verifyCommitSeal(key, t, m));
61
+ if (!sealed)
62
+ throw new StromError("no sealed commit found — nothing to restore from", { hint: "ask the user; the history may have been rewritten" });
63
+ const headSealed = head !== undefined && verifyCommitSeal(key, head.tree, head.message);
64
+ // Paths to compare: whatever differs between the sealed commit and the working tree.
65
+ const candidates = new Set();
66
+ for (const c of git.changes(tree.root, ["data", "strom.json"]))
67
+ candidates.add(c.path);
68
+ if (!headSealed) {
69
+ const r = git.runGit(tree.root, ["diff", "--name-only", "-z", sealed, "HEAD", "--", "data", "strom.json"]);
70
+ for (const p of r.stdout.split("\0").filter(Boolean))
71
+ candidates.add(p);
72
+ }
73
+ const sealedContent = git.showFiles(tree.root, sealed, [...candidates]);
74
+ const restored = [];
75
+ const quarantined = [];
76
+ for (const rel of [...candidates].sort()) {
77
+ const abs = path.join(tree.root, rel);
78
+ const want = sealedContent.get(rel);
79
+ const have = fs.existsSync(abs) ? fs.readFileSync(abs, "utf8") : undefined;
80
+ if (want === have)
81
+ continue;
82
+ quarantine(tree, rel, stamp);
83
+ if (want === undefined) {
84
+ fs.rmSync(abs);
85
+ quarantined.push(rel);
86
+ }
87
+ else {
88
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
89
+ fs.writeFileSync(abs, want);
90
+ restored.push(rel);
91
+ }
92
+ }
93
+ if (!headSealed)
94
+ tree.commit(`Repair: restore the sealed state of ${sealed.slice(0, 8)}\n\nCommits made outside strom were undone for data/ and strom.json.`, ["data", "strom.json"]);
95
+ return { restored, quarantined, from: sealed };
96
+ }
97
+ register({
98
+ path: ["repair"],
99
+ summary: "Undo changes made outside strom: restore the last sealed state",
100
+ group: "history",
101
+ tree: true,
102
+ description: "Edited records are restored, records created outside strom are removed; copies of everything\n" +
103
+ "that was replaced are kept in .strom/quarantine/. Commits made with git directly are undone for data/.",
104
+ run(ctx) {
105
+ const tree = ctx.tree();
106
+ const r = tree.withTreeLock(() => repair(tree));
107
+ const after = verifyFast(tree).findings.filter((f) => f.level === "error");
108
+ const text = r.restored.length + r.quarantined.length === 0
109
+ ? "nothing to repair — every record matches its seal"
110
+ : lines(...r.restored.map((f) => `restored ${f}`), ...r.quarantined.map((f) => `removed ${f}`), `sealed state: ${r.from.slice(0, 8)} · copies of replaced files: .strom/quarantine/`, after.length ? `still ${after.length} problem(s) → strom verify` : undefined);
111
+ return { text, data: { ...r, ok: after.length === 0 }, exitCode: after.length ? 1 : 0 };
112
+ },
113
+ }, {
114
+ path: ["seal", "adopt"],
115
+ summary: "Take over a tree sealed on another computer (the user runs this in a terminal)",
116
+ group: "history",
117
+ tree: true,
118
+ description: "Checks the chain and every record hash, then creates this computer's seal key for the tree.\nAgents must not run this — it needs the user's confirmation.",
119
+ run: async (ctx) => {
120
+ const tree = ctx.tree();
121
+ if (tree.key)
122
+ return { text: "this tree is already sealed on this computer", data: { adopted: false } };
123
+ if (!ctx.interactive)
124
+ throw new NeedsConsentError([
125
+ { key: "seal", kind: "consent", question: "Take over this tree on this computer?", set: "strom seal adopt" },
126
+ ]);
127
+ const problems = verifyFull(tree).findings.filter((f) => f.level === "error");
128
+ if (problems.length)
129
+ return report(problems, "");
130
+ if (!(await ctx.confirm(`Take over "${tree.config.name}" on this computer?`, true)))
131
+ return { text: "cancelled", data: { adopted: false } };
132
+ tree.withTreeLock(() => {
133
+ createKey(tree.env, tree.config.id);
134
+ tree.reloadKey();
135
+ tree.appendOp({ op: "seal.adopt", targets: [], files: [], summary: "Tree adopted on this computer" });
136
+ tree.commit("Seal: tree adopted on this computer");
137
+ });
138
+ return { text: `"${tree.config.name}" is now sealed on this computer`, data: { adopted: true } };
139
+ },
140
+ }, {
141
+ path: ["history"],
142
+ summary: "What changed recently (one line per change)",
143
+ group: "history",
144
+ tree: true,
145
+ run(ctx) {
146
+ const tree = ctx.tree();
147
+ const commits = git.log(tree.root, ctx.limit);
148
+ const text = commits.length
149
+ ? table(commits.map((c) => [c.hash.slice(0, 8), c.at.slice(0, 16).replace("T", " "), c.subject]))
150
+ : "no history yet";
151
+ return { text, data: { commits } };
152
+ },
153
+ });