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,383 @@
|
|
|
1
|
+
// How a person starts: the guided menu, a conversation with the agent set up
|
|
2
|
+
// the way they chose, the Strom app, the shortcut on the desktop, and teaching
|
|
3
|
+
// the agents about strom in any folder.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { configDir, userHome } from "../core/paths.js";
|
|
7
|
+
import { prependPath, spawnAgent } from "../runners/runner.js";
|
|
8
|
+
import { register } from "../cli/registry.js";
|
|
9
|
+
import { lines } from "../cli/format.js";
|
|
10
|
+
import { ui } from "../cli/ui.js";
|
|
11
|
+
import { StromError, UsageError } from "../core/errors.js";
|
|
12
|
+
import { PROFILES } from "../agents/profiles.js";
|
|
13
|
+
import { conversationArgs } from "../agents/launch.js";
|
|
14
|
+
import { globalTargets, installGlobal, isInstalled, uninstallGlobal } from "../agents/global.js";
|
|
15
|
+
import { syncAgentFiles } from "../agents/files.js";
|
|
16
|
+
import { AGENTS, findAgent, isAgent } from "../core/which.js";
|
|
17
|
+
import { DESKTOP_APPS, agentsHere, whereToTalk } from "../core/apps.js";
|
|
18
|
+
import { assertIntact } from "../core/integrity.js";
|
|
19
|
+
import { browserConnectors } from "../core/connector.js";
|
|
20
|
+
import { appOpensResearch, importAppUrl, installedStromApp, liveAppUrl, noticeStromApp, STROM_APP_URL, stromAppUrl } from "../core/stromapp.js";
|
|
21
|
+
import { liveRunning, serveLive, startLive, stopLive } from "../core/live.js";
|
|
22
|
+
import { openForUser } from "../core/open.js";
|
|
23
|
+
import { createShortcut, openInNewTerminal } from "../core/shortcut.js";
|
|
24
|
+
import { chromiumBrowser, openFileWith, openInBrowser, revealFile } from "../core/chromium.js";
|
|
25
|
+
import { uninstallPlan } from "../core/uninstall.js";
|
|
26
|
+
import { Tree } from "../core/tree.js";
|
|
27
|
+
import { commitNow, shimDir } from "./session.js";
|
|
28
|
+
import { abandonedSessions, closeSession, openSessions } from "../core/session.js";
|
|
29
|
+
import { enterWorker, liveWorkers } from "../core/workers.js";
|
|
30
|
+
import { phrase } from "../core/phrases.js";
|
|
31
|
+
import { writeGedcoms, writeTreeGed } from "./output.js";
|
|
32
|
+
import { shortcutName } from "../cli/wizard.js";
|
|
33
|
+
/** Has the user trusted this folder in Claude Code already? (Its own record, read only.) */
|
|
34
|
+
function claudeTrusts(root, env) {
|
|
35
|
+
const file = env.CLAUDE_CONFIG_DIR ? path.join(env.CLAUDE_CONFIG_DIR, ".claude.json") : path.join(userHome(env), ".claude.json");
|
|
36
|
+
try {
|
|
37
|
+
const projects = JSON.parse(fs.readFileSync(file, "utf8")).projects ?? {};
|
|
38
|
+
const real = fs.realpathSync(root);
|
|
39
|
+
return Boolean(projects[root]?.hasTrustDialogAccepted || projects[real]?.hasTrustDialogAccepted);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** An argument as a shell would need it (for --print). */
|
|
46
|
+
function shellQuote(a) {
|
|
47
|
+
return /^[\w./:=@,+-]+$/.test(a) ? a : `'${a.replace(/'/g, "'\\''")}'`;
|
|
48
|
+
}
|
|
49
|
+
/** Wait for a child that has the terminal. */
|
|
50
|
+
function waitFor(child) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
child.on("error", () => resolve(127));
|
|
53
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
register({
|
|
57
|
+
path: ["chat"],
|
|
58
|
+
summary: "Talk with the AI agent about the research — in its desktop app or in this terminal, set up the way you chose",
|
|
59
|
+
group: "start",
|
|
60
|
+
tree: true,
|
|
61
|
+
description: "The default way to research: the agent works in the tree folder with the tree's permissions and your level\n" +
|
|
62
|
+
"(agent.permissions: ask, auto, full), your model, and asks you what matters. Where you talk (agent.where): the\n" +
|
|
63
|
+
"agent's desktop app when it is installed (Claude, ChatGPT/Codex, OpenCode — the tree folder and the first message\n" +
|
|
64
|
+
"filled in; you confirm the folder and send it), else its CLI in this terminal (Claude Code, Codex, Antigravity,\n" +
|
|
65
|
+
"OpenCode). The first message is yours to give (--say); without it the agent reports where things\n" +
|
|
66
|
+
"stand and suggests what next.",
|
|
67
|
+
options: [
|
|
68
|
+
{ name: "say", type: "string", value: "<text>", description: "your first message to the agent" },
|
|
69
|
+
{ name: "print", type: "boolean", description: "print the command line instead of starting the agent" },
|
|
70
|
+
],
|
|
71
|
+
examples: ["strom chat", 'strom chat --say "Find the baptism of Karel"', "strom chat --agent codex", "strom chat --print"],
|
|
72
|
+
run: async (ctx, { opts }) => {
|
|
73
|
+
// An agent strom started (a conversation, a run) is where the research happens already. Any other
|
|
74
|
+
// agent — the one that set strom up from the web page, one opened in some folder — hands the
|
|
75
|
+
// research over: its own conversation, opened where it lives (the tree folder, its permissions).
|
|
76
|
+
const agentHere = isAgent(ctx.env);
|
|
77
|
+
if (agentHere && (ctx.env.STROM_WORKER || ctx.env.STROM_SESSION) && !opts.print)
|
|
78
|
+
throw new UsageError("strom chat opens a conversation for the user — you are in one already", { hint: "go on with strom here" });
|
|
79
|
+
const tree = ctx.tree();
|
|
80
|
+
assertIntact(tree);
|
|
81
|
+
const lang = tree.lang;
|
|
82
|
+
const agent = ctx.settings.agent(tree.config).value;
|
|
83
|
+
const profile = PROFILES[agent];
|
|
84
|
+
if (!profile)
|
|
85
|
+
throw new UsageError(`strom chat does not know agent "${agent}"`, { hint: `${Object.keys(PROFILES).join(", ")} — strom agents use claude` });
|
|
86
|
+
const where = whereToTalk(agent, ctx.settings.agentWhere(), ctx.env);
|
|
87
|
+
const program = findAgent(profile.command, ctx.env);
|
|
88
|
+
if (!program && where === "terminal" && !opts.print)
|
|
89
|
+
throw new StromError(ui(lang, "ui.chat.agent.missing", { agent: profile.name }), { hint: `strom setup — or install it: ${profile.url}` });
|
|
90
|
+
const files = syncAgentFiles(tree);
|
|
91
|
+
if (files.length)
|
|
92
|
+
tree.withTreeLock(() => tree.commit(`Agent instructions: ${files.join(", ")}`, files));
|
|
93
|
+
const shared = ctx.settings.shared()?.value;
|
|
94
|
+
const researches = tree.list("research");
|
|
95
|
+
const kickoff = typeof opts.say === "string" && opts.say.trim() ? opts.say.trim() : ui(lang, researches.length ? "ui.chat.continue" : "ui.chat.first");
|
|
96
|
+
const level = ctx.settings.agentPermissions();
|
|
97
|
+
// In the agent's desktop app: a link opens a conversation in the tree folder with the first
|
|
98
|
+
// message filled in. The app reads the tree's instructions and permissions itself; the level of
|
|
99
|
+
// a Claude app is the person's pick in the app (it cannot be set from outside), so strom says which.
|
|
100
|
+
if (where === "app") {
|
|
101
|
+
const app = DESKTOP_APPS[agent];
|
|
102
|
+
const link = app.link(tree.root, kickoff);
|
|
103
|
+
if (opts.print)
|
|
104
|
+
return { text: link, data: { app: app.name, link, cwd: tree.root } };
|
|
105
|
+
const opened = openForUser(link, ctx.env);
|
|
106
|
+
const say = [opened ? ui(lang, "ui.chat.app.open", { app: app.name }) : ui(lang, "ui.chat.app.closed", { app: app.name, dir: ctx.display(tree.root), message: kickoff })];
|
|
107
|
+
if (agent === "claude" && level !== "ask")
|
|
108
|
+
say.push(ui(lang, level === "full" ? "ui.chat.app.full" : "ui.chat.app.auto"));
|
|
109
|
+
// Claude's app takes the model from the tree's settings; the others' the person picks there.
|
|
110
|
+
if (agent !== "claude")
|
|
111
|
+
say.push(ui(lang, "ui.chat.app.model", { app: app.name }));
|
|
112
|
+
if (agentHere && opened)
|
|
113
|
+
say.push(ui(lang, "ui.chat.handover.end"));
|
|
114
|
+
return { text: say.join("\n"), data: { agent, app: app.name, link, opened, ...(agentHere ? { handover: "app" } : {}) } };
|
|
115
|
+
}
|
|
116
|
+
// In a terminal: a new window, not a conversation inside this one.
|
|
117
|
+
if (agentHere && !opts.print) {
|
|
118
|
+
const say = typeof opts.say === "string" && opts.say.trim() ? ["--say", opts.say.trim()] : [];
|
|
119
|
+
const opened = openInNewTerminal(["chat", "--agent", agent, ...say], tree.root, ctx.env);
|
|
120
|
+
return { text: ui(lang, opened ? "ui.chat.handover" : "ui.chat.handover.failed", { agent: profile.name }), data: { agent, handover: opened ? "window" : "none", cwd: tree.root } };
|
|
121
|
+
}
|
|
122
|
+
const base = {
|
|
123
|
+
kickoff,
|
|
124
|
+
level,
|
|
125
|
+
model: ctx.settings.models(agent, tree.config).lead,
|
|
126
|
+
settingsFile: path.join(tree.root, ".claude", "settings.json"),
|
|
127
|
+
// Found again in Claude Code's list of sessions (/resume) and the terminal's title: the family,
|
|
128
|
+
// its research when there is one, the day.
|
|
129
|
+
name: ["Strom", tree.config.name, researches.length === 1 ? researches[0].name : undefined, new Date().toLocaleDateString(lang, { day: "numeric", month: "numeric" })].filter(Boolean).join(" · "),
|
|
130
|
+
shared,
|
|
131
|
+
...(agent === "claude" ? { chrome: browserConnectors(ctx.env, shared).length > 0 } : {}),
|
|
132
|
+
};
|
|
133
|
+
const args = conversationArgs(agent, base);
|
|
134
|
+
if (opts.print)
|
|
135
|
+
return { text: [program ?? profile.command, ...args].map(shellQuote).join(" "), data: { command: program ?? profile.command, args, cwd: tree.root } };
|
|
136
|
+
// Other agents may work in this tree too: this one gets a name of its own, is present while it
|
|
137
|
+
// works, and has its own session. Sessions of conversations that ended without closing are closed.
|
|
138
|
+
for (const s of abandonedSessions(Tree.open(tree.root, ctx.env))) {
|
|
139
|
+
const t = Tree.open(tree.root, ctx.env);
|
|
140
|
+
closeSession(t, s, { summary: phrase(t.lang, "session.chat"), next: "", interrupted: true, endedBy: "chat" });
|
|
141
|
+
commitNow(t, `${s.id} interrupted: its conversation had ended`);
|
|
142
|
+
}
|
|
143
|
+
const worker = `${agent}-${process.pid}-${Date.now().toString(36)}`;
|
|
144
|
+
const leave = enterWorker(tree.root, worker, `${profile.name} conversation`);
|
|
145
|
+
ctx.io.stdout(ui(lang, "ui.chat.open", { agent: profile.name, exit: profile.exit }) + "\n");
|
|
146
|
+
// Claude Code asks once whether the folder is to be trusted — its own safety step, kept; the user knows what to answer.
|
|
147
|
+
if (agent === "claude" && !claudeTrusts(tree.root, ctx.env))
|
|
148
|
+
ctx.io.stdout(ui(lang, "ui.chat.trust") + "\n");
|
|
149
|
+
const env = { ...prependPath(ctx.env, shimDir(tree)), STROM_WORKER: worker };
|
|
150
|
+
let code;
|
|
151
|
+
try {
|
|
152
|
+
const started = Date.now();
|
|
153
|
+
code = await waitFor(spawnAgent(profile.command, args, { cwd: tree.root, env, stdio: "inherit" }));
|
|
154
|
+
// A Claude Code without the auto mode (an older one, or a plan without it) stops at once: go on in its own mode.
|
|
155
|
+
if (code !== 0 && agent === "claude" && level === "auto" && Date.now() - started < 5000)
|
|
156
|
+
code = await waitFor(spawnAgent(profile.command, conversationArgs(agent, { ...base, level: "ask" }), { cwd: tree.root, env, stdio: "inherit" }));
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
leave();
|
|
160
|
+
}
|
|
161
|
+
// A session the agent left open goes back: its task returns to the queue.
|
|
162
|
+
for (const s of openSessions(Tree.open(tree.root, ctx.env)).filter((x) => x.worker === worker)) {
|
|
163
|
+
const t = Tree.open(tree.root, ctx.env);
|
|
164
|
+
closeSession(t, s, { summary: phrase(t.lang, "session.chat"), next: "", interrupted: true, endedBy: "chat" });
|
|
165
|
+
}
|
|
166
|
+
// What the conversation recorded goes into the results at once.
|
|
167
|
+
const after = Tree.open(tree.root, ctx.env);
|
|
168
|
+
writeGedcoms(ctx, after);
|
|
169
|
+
commitNow(after, "Results after a conversation with the agent");
|
|
170
|
+
return { text: "", data: { agent, exitCode: code } };
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
register({
|
|
174
|
+
path: ["app"],
|
|
175
|
+
summary: "Open the Strom app (the family tree app) — the installed one, else in the browser",
|
|
176
|
+
group: "output",
|
|
177
|
+
description: "In a tree, with an app that can take it (from its version 3.0.0), the app opens this research itself — the first time as\n" +
|
|
178
|
+
"a new tree, after that the same tree updated — and follows it while somebody is at work on it (the live bridge).\n" +
|
|
179
|
+
`Otherwise the results are ready for it in output/tree-strom.ged (in the app: Import). Web: ${STROM_APP_URL}`,
|
|
180
|
+
options: [{ name: "live", type: "boolean", description: "the app follows this research while it goes on (the live bridge)" }],
|
|
181
|
+
examples: ["strom app", "strom app --live", "strom app install"],
|
|
182
|
+
run(ctx, { opts }) {
|
|
183
|
+
const lang = ctx.uiLang();
|
|
184
|
+
const root = appOpensResearch(ctx.env) ? ctx.locateTree() : undefined;
|
|
185
|
+
if (root) {
|
|
186
|
+
const tree = Tree.open(root, ctx.env);
|
|
187
|
+
const name = tree.config.name;
|
|
188
|
+
// Followed while somebody is at work on it: an agent strom started, or the agent asking for it now.
|
|
189
|
+
const follow = Boolean(opts.live) || liveWorkers(root).length > 0 || isAgent(ctx.env);
|
|
190
|
+
// The file as the research is now: the installed app takes it, or the user drags it in.
|
|
191
|
+
const file = writeTreeGed(tree, { for: "strom", stromVersion: ctx.settings.stromVersion(tree.config) }).file;
|
|
192
|
+
// B, D: the app reaches strom on this computer — in a Chromium browser, never whatever is the default (Safari cannot).
|
|
193
|
+
const browser = chromiumBrowser(ctx.env);
|
|
194
|
+
if (browser) {
|
|
195
|
+
// The bridge serves the tree's GEDCOM to the app; it ends by itself when nobody asks it anything.
|
|
196
|
+
const info = startLive(root, ctx.env);
|
|
197
|
+
if (!info)
|
|
198
|
+
throw new StromError("the bridge did not start", { hint: "strom live serve shows why" });
|
|
199
|
+
const url = follow ? liveAppUrl(info.url, ctx.env) : importAppUrl(`${info.url}/tree.ged`, ctx.env);
|
|
200
|
+
const opened = openInBrowser(browser, url, ctx.env);
|
|
201
|
+
const text = opened ? ui(lang, follow ? "ui.app.following" : "ui.app.research", { name, where: browser.name }) : ui(lang, "ui.app.url", { url });
|
|
202
|
+
return { text, data: { opened, via: "browser", browser: browser.name, url, bridge: info.url, follow } };
|
|
203
|
+
}
|
|
204
|
+
// A: no such browser to hand it over, but the Strom app installed from one takes the file itself (its
|
|
205
|
+
// file handler) — not when strom is pointed at another copy of the app (STROM_APP_URL: a beta, a development one).
|
|
206
|
+
const installed = installedStromApp(ctx.env);
|
|
207
|
+
if (!follow && !ctx.env.STROM_APP_URL && installed && /Chrome|Edge|Brave|Chromium|Vivaldi/.test(installed.kind) && openFileWith(installed.path, file, ctx.env))
|
|
208
|
+
return { text: ui(lang, "ui.app.research", { name, where: installed.kind }), data: { opened: true, via: "file", file, app: installed.path, follow } };
|
|
209
|
+
// C: no such browser — the app opens, the file is shown, the user drags it into the app's window.
|
|
210
|
+
openForUser(installed?.path ?? stromAppUrl(ctx.env), ctx.env);
|
|
211
|
+
const shown = revealFile(file, ctx.env);
|
|
212
|
+
return {
|
|
213
|
+
text: lines(follow ? ui(lang, "ui.app.nolive") : undefined, ui(lang, "ui.app.drag", { file: ctx.display(file), shown: shown ? ui(lang, "ui.app.drag.shown") : "" })),
|
|
214
|
+
data: { opened: shown, via: "drag", file, follow },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (opts.live) {
|
|
218
|
+
const info = startLive(ctx.tree().root, ctx.env);
|
|
219
|
+
if (!info)
|
|
220
|
+
throw new StromError("the bridge did not start", { hint: "strom live serve shows why" });
|
|
221
|
+
const url = liveAppUrl(info.url, ctx.env);
|
|
222
|
+
const opened = openForUser(url, ctx.env);
|
|
223
|
+
return { text: opened ? ui(lang, "ui.app.opened", { where: ui(lang, "ui.app.browser") }) : ui(lang, "ui.app.url", { url }), data: { opened, url, bridge: info.url } };
|
|
224
|
+
}
|
|
225
|
+
const app = noticeStromApp(ctx.settings, ctx.env, { look: true }) ?? installedStromApp(ctx.env);
|
|
226
|
+
const opened = openForUser(app?.path ?? stromAppUrl(ctx.env), ctx.env);
|
|
227
|
+
const text = opened ? ui(lang, "ui.app.opened", { where: app ? app.kind : ui(lang, "ui.app.browser") }) : ui(lang, "ui.app.url", { url: stromAppUrl(ctx.env) });
|
|
228
|
+
return { text, data: { opened, url: stromAppUrl(ctx.env), ...(app ? { installed: app } : {}) } };
|
|
229
|
+
},
|
|
230
|
+
}, {
|
|
231
|
+
path: ["live"],
|
|
232
|
+
summary: "The live bridge: the Strom app follows this research while it goes on (read only, this computer only)",
|
|
233
|
+
group: "output",
|
|
234
|
+
tree: true,
|
|
235
|
+
description: "A small web server on this computer only (127.0.0.1, a secret address), that only reads: the Strom app, opened with\n" +
|
|
236
|
+
"strom app --live, takes the tree from it and hears what changes — who is at work on what, what was recorded, what\n" +
|
|
237
|
+
"waits for the user. It ends by itself when nobody asks it anything for two hours, or with strom live stop.",
|
|
238
|
+
examples: ["strom live", "strom live start", "strom live stop"],
|
|
239
|
+
run(ctx) {
|
|
240
|
+
const tree = ctx.tree();
|
|
241
|
+
const info = liveRunning(tree.root);
|
|
242
|
+
return info
|
|
243
|
+
? { text: lines(`the bridge runs: ${info.url}`, ` the Strom app following it: ${liveAppUrl(info.url, ctx.env)}`, " stop: strom live stop"), data: { running: true, ...info, app: liveAppUrl(info.url, ctx.env) } }
|
|
244
|
+
: { text: "the bridge does not run — strom live start (or strom app --live)", data: { running: false } };
|
|
245
|
+
},
|
|
246
|
+
}, {
|
|
247
|
+
path: ["live", "start"],
|
|
248
|
+
summary: "Start the live bridge in the background (or say where it runs)",
|
|
249
|
+
group: "output",
|
|
250
|
+
tree: true,
|
|
251
|
+
run(ctx) {
|
|
252
|
+
const tree = ctx.tree();
|
|
253
|
+
const info = startLive(tree.root, ctx.env);
|
|
254
|
+
if (!info)
|
|
255
|
+
throw new StromError("the bridge did not start", { hint: "strom live serve shows why" });
|
|
256
|
+
return { text: lines(`the bridge runs: ${info.url}`, ` the Strom app following it: ${liveAppUrl(info.url, ctx.env)}`), data: { ...info, app: liveAppUrl(info.url, ctx.env) } };
|
|
257
|
+
},
|
|
258
|
+
}, {
|
|
259
|
+
path: ["live", "stop"],
|
|
260
|
+
summary: "Stop the live bridge",
|
|
261
|
+
group: "output",
|
|
262
|
+
tree: true,
|
|
263
|
+
run(ctx) {
|
|
264
|
+
const stopped = stopLive(ctx.tree().root);
|
|
265
|
+
return { text: stopped ? "the bridge stopped" : "no bridge ran", data: { stopped } };
|
|
266
|
+
},
|
|
267
|
+
}, {
|
|
268
|
+
path: ["live", "serve"],
|
|
269
|
+
summary: "Run the live bridge here, in the foreground (strom live start runs it in the background)",
|
|
270
|
+
group: "output",
|
|
271
|
+
tree: true,
|
|
272
|
+
async run(ctx) {
|
|
273
|
+
const tree = ctx.tree();
|
|
274
|
+
await serveLive(tree.root, ctx.env);
|
|
275
|
+
return { text: "the bridge ended", data: {} };
|
|
276
|
+
},
|
|
277
|
+
}, {
|
|
278
|
+
path: ["app", "install"],
|
|
279
|
+
summary: "Help install the Strom app: it opens in the browser and strom says where to click",
|
|
280
|
+
group: "output",
|
|
281
|
+
run(ctx) {
|
|
282
|
+
const lang = ctx.uiLang();
|
|
283
|
+
const app = installedStromApp(ctx.env);
|
|
284
|
+
if (app) {
|
|
285
|
+
noticeStromApp(ctx.settings, ctx.env, { look: true });
|
|
286
|
+
const opened = openForUser(app.path, ctx.env);
|
|
287
|
+
return { text: ui(lang, "ui.app.installed", { kind: app.kind }), data: { installed: app, opened } };
|
|
288
|
+
}
|
|
289
|
+
const opened = openForUser(stromAppUrl(ctx.env), ctx.env);
|
|
290
|
+
return { text: opened ? ui(lang, "ui.app.install") : ui(lang, "ui.app.url", { url: stromAppUrl(ctx.env) }), data: { installed: false, opened, url: stromAppUrl(ctx.env) } };
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
register({
|
|
294
|
+
path: ["agents", "install"],
|
|
295
|
+
summary: "Teach the installed AI agents about strom in any folder (a Claude Code skill, a note in the others' instructions) and let them run it without asking",
|
|
296
|
+
group: "setup",
|
|
297
|
+
description: "Done by the setup already. A user who opens their agent anywhere and asks about their family gets an agent that knows\nstrom is there. The user's own text in those files is kept; strom agents uninstall takes it away again.",
|
|
298
|
+
options: [{ name: "all", type: "boolean", description: "also for agents not installed yet" }],
|
|
299
|
+
run(ctx, { opts }) {
|
|
300
|
+
const installed = agentsHere(ctx.env).map((a) => a.id);
|
|
301
|
+
// One file may serve two agents: written once.
|
|
302
|
+
const seen = new Set();
|
|
303
|
+
const rows = globalTargets(ctx.env)
|
|
304
|
+
.filter((t) => (opts.all || installed.includes(t.agent)) && !seen.has(`${t.kind}:${t.file}`) && seen.add(`${t.kind}:${t.file}`))
|
|
305
|
+
.map((t) => ({ agent: t.agent, file: t.file, written: installGlobal(t) }));
|
|
306
|
+
const text = rows.length
|
|
307
|
+
? lines(...rows.map((r) => `${PROFILES[r.agent].name}: ${ctx.display(r.file)}${r.written ? "" : " (already there)"}`))
|
|
308
|
+
: "no AI agent installed — strom setup offers one";
|
|
309
|
+
return { text, data: { agents: rows } };
|
|
310
|
+
},
|
|
311
|
+
}, {
|
|
312
|
+
path: ["agents", "uninstall"],
|
|
313
|
+
summary: "Take strom's note out of the AI agents' own instructions again",
|
|
314
|
+
group: "setup",
|
|
315
|
+
run(ctx) {
|
|
316
|
+
const rows = globalTargets(ctx.env)
|
|
317
|
+
.filter((t) => isInstalled(t))
|
|
318
|
+
.map((t) => ({ agent: t.agent, file: t.file, removed: uninstallGlobal(t) }))
|
|
319
|
+
.filter((r) => r.removed);
|
|
320
|
+
return { text: rows.length ? lines(...rows.map((r) => `${PROFILES[r.agent].name}: removed from ${ctx.display(r.file)}`)) : "nothing to remove", data: { agents: rows } };
|
|
321
|
+
},
|
|
322
|
+
}, {
|
|
323
|
+
path: ["uninstall"],
|
|
324
|
+
summary: "Take strom off this computer: what the agents were taught, the shortcut, its own git, its PATH lines and the program — the research and the settings stay",
|
|
325
|
+
group: "setup",
|
|
326
|
+
description: "The user's decision: a person at their terminal is asked there (--yes: no question); asked by an agent, strom asks\n" +
|
|
327
|
+
"in a window of the system. Installed through npm, the program itself goes with npm uninstall -g strom-research.",
|
|
328
|
+
examples: ["strom uninstall"],
|
|
329
|
+
async run(ctx) {
|
|
330
|
+
const lang = ctx.uiLang();
|
|
331
|
+
const plan = uninstallPlan(ctx.env, ["en", "cs", "de"].map(shortcutName));
|
|
332
|
+
const label = (r) => ui(lang, `ui.uninstall.${r.kind}`, { agent: r.agent ?? "" });
|
|
333
|
+
const home = ctx.settings.home()?.value;
|
|
334
|
+
const keeps = ui(lang, "ui.uninstall.keeps", { home: home ? ctx.display(home) : "—", config: ctx.display(configDir(ctx.env)) });
|
|
335
|
+
if (!plan.remove.length)
|
|
336
|
+
return { text: lines(ui(lang, "ui.uninstall.nothing"), plan.npm ? ui(lang, "ui.uninstall.npm") : undefined, keeps), data: { removed: [], npm: plan.npm } };
|
|
337
|
+
const list = plan.remove.map((r) => ` ${label(r)}: ${ctx.display(r.path)}`);
|
|
338
|
+
if (isAgent(ctx.env) || !ctx.yes) {
|
|
339
|
+
if (!isAgent(ctx.env) && ctx.interactive) {
|
|
340
|
+
ctx.io.stdout(`${lines(ui(lang, "ui.uninstall.list"), ...list, "", keeps)}
|
|
341
|
+
`);
|
|
342
|
+
if (!(await ctx.confirm(ui(lang, "ui.uninstall.sure"), false)))
|
|
343
|
+
return { text: ui(lang, "ui.uninstall.kept"), data: { removed: [] } };
|
|
344
|
+
}
|
|
345
|
+
else
|
|
346
|
+
ctx.requireHuman("uninstall strom from this computer", "strom uninstall", "uninstall", ui(lang, "ui.uninstall.window"));
|
|
347
|
+
}
|
|
348
|
+
const removed = plan.remove.filter((r) => {
|
|
349
|
+
try {
|
|
350
|
+
return r.remove();
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
const failed = plan.remove.filter((r) => !removed.includes(r));
|
|
357
|
+
return {
|
|
358
|
+
text: lines(...removed.map((r) => `✓ ${label(r)}: ${ctx.display(r.path)}`), ...failed.map((r) => `✗ ${label(r)}: ${ctx.display(r.path)}`), "", plan.npm ? ui(lang, "ui.uninstall.npm") : undefined, keeps, failed.length ? undefined : ui(lang, "ui.uninstall.done")),
|
|
359
|
+
data: { removed: removed.map((r) => ({ kind: r.kind, path: r.path })), failed: failed.map((r) => ({ kind: r.kind, path: r.path })), npm: plan.npm },
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
}, {
|
|
363
|
+
path: ["shortcut"],
|
|
364
|
+
summary: 'Put a "Strom research" shortcut on the desktop — a double-click opens strom\'s menu',
|
|
365
|
+
group: "setup",
|
|
366
|
+
run(ctx) {
|
|
367
|
+
const lang = ctx.uiLang();
|
|
368
|
+
const files = createShortcut(shortcutName(lang), ctx.env);
|
|
369
|
+
return { text: lines(...files.map((f) => ui(lang, "ui.shortcut.done", { file: ctx.display(f) }))), data: { files } };
|
|
370
|
+
},
|
|
371
|
+
}, {
|
|
372
|
+
path: ["menu"],
|
|
373
|
+
summary: "The guided menu for a person — what `strom` opens in a terminal",
|
|
374
|
+
group: "start",
|
|
375
|
+
run: async (ctx) => {
|
|
376
|
+
if (isAgent(ctx.env))
|
|
377
|
+
throw new UsageError("the menu is for a person at a terminal", { hint: "strom (orientation) — or strom --json" });
|
|
378
|
+
const { runMenu } = await import("../cli/menu.js");
|
|
379
|
+
const { main } = await import("../cli/main.js");
|
|
380
|
+
await runMenu(ctx, (argv, quiet) => main(argv, quiet ? { ...ctx.io, stdout: () => undefined } : ctx.io, ctx.env, ctx.cwd));
|
|
381
|
+
return { text: "" };
|
|
382
|
+
},
|
|
383
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// story set · show — the story of a person or a couple, written from the facts
|
|
2
|
+
// for the family. It goes into the GEDCOM as _STORY (Strom sets it in the
|
|
3
|
+
// family book after the facts).
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { register } from "../cli/registry.js";
|
|
6
|
+
import { lines } from "../cli/format.js";
|
|
7
|
+
import { setStory, findEventOwner } from "../core/actions.js";
|
|
8
|
+
import { UsageError } from "../core/errors.js";
|
|
9
|
+
import { csvOpt, normId, textOpt } from "../core/records.js";
|
|
10
|
+
import { resolvePerson } from "../core/people.js";
|
|
11
|
+
function owner(tree, ref) {
|
|
12
|
+
return /^F\d+$/i.test(ref.trim()) ? normId(ref, "family") : resolvePerson(tree, ref).id;
|
|
13
|
+
}
|
|
14
|
+
register({
|
|
15
|
+
path: ["story", "set"],
|
|
16
|
+
summary: "Write the story of a person or a couple from the facts (a draft until the user approves it)",
|
|
17
|
+
group: "people",
|
|
18
|
+
tree: true,
|
|
19
|
+
writes: true,
|
|
20
|
+
description: "Every statement rests on a recorded fact: list them with --fact. Paragraphs are separated by a blank line;\n" +
|
|
21
|
+
"**bold** is kept. Writing it again replaces it (the history keeps the old one).",
|
|
22
|
+
args: [{ name: "who", description: "person (ID or name) or family (F…)", required: true }],
|
|
23
|
+
options: [
|
|
24
|
+
{ name: "text", type: "string", value: "<text|@file>", description: "the story, in the research language" },
|
|
25
|
+
{ name: "title", type: "string", value: "<text>", description: "a heading" },
|
|
26
|
+
{ name: "fact", type: "string", multiple: true, value: "<E…>", description: "a fact it leans on (repeatable)" },
|
|
27
|
+
{ name: "note", type: "string", value: "<text>", description: "your caveat: what is inferred, what is unknown" },
|
|
28
|
+
{ name: "final", type: "boolean", description: "the user approved it" },
|
|
29
|
+
],
|
|
30
|
+
examples: ['strom story set P0001 --text @notes/story-P0001.md --title "The miller of Týnec" --fact E0001 --fact E0002 --note "The house is inferred from the census."'],
|
|
31
|
+
run(ctx, { args, opts }) {
|
|
32
|
+
const tree = ctx.tree();
|
|
33
|
+
const text = textOpt(opts.text, (p) => fs.readFileSync(ctx.resolvePath(p), "utf8"));
|
|
34
|
+
if (!text?.trim())
|
|
35
|
+
throw new UsageError("--text is required", { hint: "--text @notes/story.md (a file) or the text itself" });
|
|
36
|
+
const rec = setStory(tree, owner(tree, args[0]), {
|
|
37
|
+
text,
|
|
38
|
+
title: opts.title,
|
|
39
|
+
facts: csvOpt(opts.fact).map((f) => normId(f)),
|
|
40
|
+
note: opts.note,
|
|
41
|
+
final: Boolean(opts.final),
|
|
42
|
+
});
|
|
43
|
+
const missing = rec.story.facts.length === 0 ? "note: no --fact given — say which facts the story rests on" : undefined;
|
|
44
|
+
return { text: lines(...tree.written.map((o) => o.summary), tree.dryRun ? "(dry run — nothing written)" : undefined, missing), data: { story: rec.story } };
|
|
45
|
+
},
|
|
46
|
+
}, {
|
|
47
|
+
path: ["story", "show"],
|
|
48
|
+
summary: "The story of a person or a couple, with the facts it rests on",
|
|
49
|
+
group: "people",
|
|
50
|
+
tree: true,
|
|
51
|
+
args: [{ name: "who", description: "person (ID or name) or family (F…)", required: true }],
|
|
52
|
+
run(ctx, { args }) {
|
|
53
|
+
const tree = ctx.tree();
|
|
54
|
+
const id = owner(tree, args[0]);
|
|
55
|
+
const rec = tree.get(id);
|
|
56
|
+
if (!rec)
|
|
57
|
+
throw new UsageError(`no family ${id}`);
|
|
58
|
+
const st = rec.story;
|
|
59
|
+
if (!st)
|
|
60
|
+
return { text: `${id} has no story yet → strom story set ${id} --text @file --fact E…`, data: { story: null } };
|
|
61
|
+
const facts = st.facts.map((f) => {
|
|
62
|
+
try {
|
|
63
|
+
const { owner: o, event: e } = findEventOwner(tree, f);
|
|
64
|
+
return ` ${e.id} ${o.id} ${e.kind}${e.date ? ` ${e.date}` : ""}${e.place ? ` ${e.place}` : ""} [${e.status}]`;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return ` ${f} (no longer exists)`;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
text: lines(`${id} story · ${st.status} · ${st.at.slice(0, 10)}${st.title ? ` · ${st.title}` : ""}`, "", st.text, "", st.facts.length ? "rests on" : "rests on no recorded fact", ...facts, st.note ? `\nnote: ${st.note}` : undefined),
|
|
72
|
+
data: { story: st },
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
});
|