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,465 @@
|
|
|
1
|
+
// setup · doctor · config — the environment around the trees.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { register } from "../cli/registry.js";
|
|
6
|
+
import { lines, table } from "../cli/format.js";
|
|
7
|
+
import { checkValue, configFile, DEFAULT_BUDGET, DEFAULT_RUN_MINUTES, OTHER_ENV, SETTINGS, settingDef, writeStored } from "../core/config.js";
|
|
8
|
+
import { syncAgentFiles } from "../agents/files.js";
|
|
9
|
+
import { gitVersion } from "../core/git.js";
|
|
10
|
+
import { linuxGitCommand } from "../core/deps.js";
|
|
11
|
+
import { fixGit, offerAgent } from "../cli/fixes.js";
|
|
12
|
+
import { createShortcut } from "../core/shortcut.js";
|
|
13
|
+
import { openForUser } from "../core/open.js";
|
|
14
|
+
import { isValidLang, langName } from "../core/lang.js";
|
|
15
|
+
import { detectAgent, isAgent } from "../core/which.js";
|
|
16
|
+
import { agentsHere, DESKTOP_APPS, inDesktopApp, whereToTalk } from "../core/apps.js";
|
|
17
|
+
import { setupWizard } from "../cli/wizard.js";
|
|
18
|
+
import { installation, stromLauncher } from "../core/self.js";
|
|
19
|
+
import { VERSION } from "../core/tree.js";
|
|
20
|
+
import { installUpdate, isNewer, knownNewerVersion, latestVersion, newerVersion } from "../core/update.js";
|
|
21
|
+
import { desktopDir } from "../core/paths.js";
|
|
22
|
+
import { researchUrl, stromAppUrl, stromAppState } from "../core/stromapp.js";
|
|
23
|
+
import { isInstalled } from "../agents/global.js";
|
|
24
|
+
import { shortcutName } from "../cli/wizard.js";
|
|
25
|
+
import { globalTargets, installGlobal } from "../agents/global.js";
|
|
26
|
+
import { ui } from "../cli/ui.js";
|
|
27
|
+
import { PERMISSION_LEVELS } from "../core/config.js";
|
|
28
|
+
import { PROFILES } from "../agents/profiles.js";
|
|
29
|
+
import { NeedsInputError, StromError, UsageError } from "../core/errors.js";
|
|
30
|
+
import { check } from "../core/check.js";
|
|
31
|
+
import { assertIntact, verifyFull } from "../core/integrity.js";
|
|
32
|
+
import { ensurePluginsDir } from "../core/connector.js";
|
|
33
|
+
import { downloadsDir } from "../core/browser.js";
|
|
34
|
+
export const SHARED_DIRS = ["media", "catalog", "tools", "cache", "inbox"];
|
|
35
|
+
export function ensureShared(dir) {
|
|
36
|
+
for (const d of SHARED_DIRS)
|
|
37
|
+
fs.mkdirSync(path.join(dir, d), { recursive: true });
|
|
38
|
+
ensurePluginsDir(dir);
|
|
39
|
+
}
|
|
40
|
+
register({
|
|
41
|
+
path: ["setup"],
|
|
42
|
+
summary: "First-time setup: where Strom keeps trees and shared data, research language",
|
|
43
|
+
group: "setup",
|
|
44
|
+
description: "A person at a terminal gets a wizard in their language (run it again any time to change an answer): language,\n" +
|
|
45
|
+
"folder, git and an AI agent (offered to install), the model, what the agent may do without asking, a desktop\n" +
|
|
46
|
+
"shortcut. Without a terminal (an agent): pass the values or --yes to accept the suggestions.\n" +
|
|
47
|
+
"Trees and shared data (scans, catalog) live under one home by default; --shared/--trees separate them.\n" +
|
|
48
|
+
"Either way the installed agents learn about strom in any folder (strom agents install).",
|
|
49
|
+
examples: ["strom setup", "strom setup --yes --lang cs", 'strom setup --home "~/Documents/Strom" --shared "/Volumes/Big/strom-shared" --lang cs'],
|
|
50
|
+
run: async (ctx) => {
|
|
51
|
+
const s = ctx.settings;
|
|
52
|
+
if (ctx.interactive && !isAgent(ctx.env)) {
|
|
53
|
+
const r = await setupWizard(ctx);
|
|
54
|
+
return { text: "", data: { ...r, trees: s.trees().value, shared: s.shared().value, config: configFile(ctx.env) } };
|
|
55
|
+
}
|
|
56
|
+
const cfg = s.config;
|
|
57
|
+
const flags = s.flags;
|
|
58
|
+
const suggestedHome = s.home()?.value ?? s.suggestedHome();
|
|
59
|
+
if (!flags.home && !ctx.yes && !ctx.interactive && !cfg.home)
|
|
60
|
+
throw new NeedsInputError([
|
|
61
|
+
{
|
|
62
|
+
key: "home",
|
|
63
|
+
kind: "config",
|
|
64
|
+
question: "Where should Strom keep the research (trees and shared data)?",
|
|
65
|
+
default: ctx.display(suggestedHome),
|
|
66
|
+
scopes: ["user"],
|
|
67
|
+
set: `strom setup --home "${ctx.display(suggestedHome)}" --yes`,
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
let home = flags.home ? ctx.resolvePath(flags.home) : suggestedHome;
|
|
71
|
+
let shared = flags.shared ? ctx.resolvePath(flags.shared) : undefined;
|
|
72
|
+
let trees = flags.trees ? ctx.resolvePath(flags.trees) : undefined;
|
|
73
|
+
let lang = flags.lang ?? cfg.lang ?? s.lang().value;
|
|
74
|
+
const langDetected = !flags.lang && !cfg.lang && s.lang().source === "detected" && !ctx.interactive;
|
|
75
|
+
// Suggest the configured agent, else the first one installed, else Claude Code.
|
|
76
|
+
const installed = agentsHere(ctx.env).map((a) => a.id);
|
|
77
|
+
// Set up by an agent: that one is the user's (unless they chose already); else the first one here.
|
|
78
|
+
const running = detectAgent(ctx.env);
|
|
79
|
+
let agent = (flags.agent ?? cfg.agent ?? (running && PROFILES[running] ? running : undefined) ?? installed[0] ?? "claude").toLowerCase();
|
|
80
|
+
if (ctx.interactive) {
|
|
81
|
+
if (!flags.home)
|
|
82
|
+
home = ctx.resolvePath(await ctx.ask("Where should Strom keep your research?", ctx.display(home)));
|
|
83
|
+
if (!flags.shared && !flags.trees && (await ctx.confirm("Keep scans and the archive catalog in a separate folder (e.g. a bigger disk)?", false)))
|
|
84
|
+
shared = ctx.resolvePath(await ctx.ask("Folder for shared data:", ctx.display(path.join(home, "shared"))));
|
|
85
|
+
if (!flags.lang)
|
|
86
|
+
lang = (await ctx.ask(`Research language (the agent talks to you in it):`, lang)).toLowerCase();
|
|
87
|
+
if (!flags.agent)
|
|
88
|
+
agent = (await ctx.ask(`Which AI agent does the research? (${Object.keys(PROFILES).join(", ")})`, agent)).toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
if (!PROFILES[agent])
|
|
91
|
+
throw new UsageError(`unknown agent "${agent}"`, { hint: Object.keys(PROFILES).join(", ") });
|
|
92
|
+
if (!isValidLang(lang))
|
|
93
|
+
throw new UsageError(`invalid language code "${lang}"`, { hint: "use a code like cs, en, de" });
|
|
94
|
+
cfg.home = home;
|
|
95
|
+
cfg.lang = lang;
|
|
96
|
+
cfg.agent = agent;
|
|
97
|
+
// Set up by an agent working in its desktop app: that is where this person talks with it.
|
|
98
|
+
if (!cfg.agentWhere && inDesktopApp(ctx.env))
|
|
99
|
+
cfg.agentWhere = "app";
|
|
100
|
+
if (shared)
|
|
101
|
+
cfg.shared = shared;
|
|
102
|
+
if (trees)
|
|
103
|
+
cfg.trees = trees;
|
|
104
|
+
s.save();
|
|
105
|
+
fs.mkdirSync(home, { recursive: true });
|
|
106
|
+
const sharedDir = s.shared().value;
|
|
107
|
+
const treesDir = s.trees().value;
|
|
108
|
+
fs.mkdirSync(treesDir, { recursive: true });
|
|
109
|
+
ensureShared(sharedDir);
|
|
110
|
+
// The installed agents learn about strom in any folder.
|
|
111
|
+
const taught = [...new Set(globalTargets(ctx.env).filter((t) => installed.includes(t.agent) && installGlobal(t)).map((t) => t.agent))];
|
|
112
|
+
const text = lines("Strom is set up.", table([
|
|
113
|
+
[" home", ctx.display(home)],
|
|
114
|
+
[" trees", ctx.display(treesDir)],
|
|
115
|
+
[" shared", ctx.display(sharedDir)],
|
|
116
|
+
[" language", `${langName(lang)} (${lang})${langDetected ? " — detected from the system; the user speaks another? strom config set lang <code>" : ""}`],
|
|
117
|
+
[" agent", `${PROFILES[agent].name}${installed.includes(agent) ? "" : " — not installed yet"}`],
|
|
118
|
+
...taught.map((a) => [" knows strom", `${PROFILES[a].name}, in any folder`]),
|
|
119
|
+
[" config", ctx.display(configFile(ctx.env))],
|
|
120
|
+
]), "", 'next strom init "<tree name, e.g. the family surname>"');
|
|
121
|
+
return { text, data: { home, trees: treesDir, shared: sharedDir, lang, agent, config: configFile(ctx.env) } };
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
function nodeOk(version) {
|
|
125
|
+
const [maj = 0, min = 0] = version.replace(/^v/, "").split(".").map(Number);
|
|
126
|
+
return maj > 22 || (maj === 22 && min >= 18);
|
|
127
|
+
}
|
|
128
|
+
const FIX = "strom doctor --fix";
|
|
129
|
+
/** Everything strom needs and has on this computer, in the user's language. */
|
|
130
|
+
function diagnose(ctx) {
|
|
131
|
+
const lang = ctx.uiLang();
|
|
132
|
+
const t = (key, values = {}) => ui(lang, key, values);
|
|
133
|
+
const checks = [];
|
|
134
|
+
const add = (name, status, detail, fix, repair) => checks.push({ name, label: t(`ui.doc.${name}`), status, detail, ...(fix ? { fix } : {}), ...(repair ? { repair } : {}) });
|
|
135
|
+
// The program itself: the installer's (its own Node), or npm's or the sources' on the Node of the computer.
|
|
136
|
+
if (installation().kind === "installed")
|
|
137
|
+
add("program", "ok", t("ui.doc.installed", { version: VERSION, node: process.version }));
|
|
138
|
+
else if (nodeOk(process.version))
|
|
139
|
+
add("program", "ok", t("ui.doc.innode", { version: VERSION, node: process.version }));
|
|
140
|
+
else
|
|
141
|
+
add("program", "fail", t("ui.doc.oldnode", { node: process.version }), "https://nodejs.org");
|
|
142
|
+
const newer = knownNewerVersion(ctx.settings, ctx.env);
|
|
143
|
+
if (newer)
|
|
144
|
+
add("update", "warn", t("ui.doc.update.new", { version: newer }), "strom update");
|
|
145
|
+
const g = gitVersion(ctx.env);
|
|
146
|
+
if (g)
|
|
147
|
+
add("git", "ok", g);
|
|
148
|
+
else
|
|
149
|
+
add("git", "fail", t("ui.doc.missing"), process.platform === "linux" ? linuxGitCommand() : FIX, "git");
|
|
150
|
+
const home = ctx.settings.home();
|
|
151
|
+
if (!home)
|
|
152
|
+
add("home", "fail", t("ui.doc.notsetup"), "strom setup");
|
|
153
|
+
else {
|
|
154
|
+
const exists = fs.existsSync(home.value);
|
|
155
|
+
add("home", exists ? "ok" : "fail", ctx.display(home.value), exists ? undefined : "strom setup");
|
|
156
|
+
const shared = ctx.settings.shared();
|
|
157
|
+
const sharedOk = SHARED_DIRS.every((d) => fs.existsSync(path.join(shared.value, d)));
|
|
158
|
+
add("shared", sharedOk ? "ok" : "warn", ctx.display(shared.value), sharedOk ? undefined : "strom setup --yes");
|
|
159
|
+
const trees = ctx.knownTrees();
|
|
160
|
+
add("trees", "ok", trees.length ? trees.map((k) => k.name).join(", ") : t("ui.doc.none"));
|
|
161
|
+
}
|
|
162
|
+
// Agents: their CLI or their desktop app — found, never started here.
|
|
163
|
+
const here = agentsHere(ctx.env);
|
|
164
|
+
const found = here.map((a) => a.id);
|
|
165
|
+
const tree = ctx.hasTree() ? ctx.tree().config : undefined;
|
|
166
|
+
const chosen = ctx.settings.agent(tree).value;
|
|
167
|
+
if (!found.length)
|
|
168
|
+
add("agent", "fail", t("ui.doc.noagent"), `${FIX} (${researchUrl(lang, "agents")})`, "agent");
|
|
169
|
+
else {
|
|
170
|
+
const names = found.map((id) => `${PROFILES[id]?.name ?? id}${id === chosen ? ` (${t("ui.doc.default")})` : ""}`).join(", ");
|
|
171
|
+
add("agent", found.includes(chosen) ? "ok" : "warn", names, found.includes(chosen) ? undefined : `strom agents use ${found[0]}`);
|
|
172
|
+
// Do the agents here know strom in any folder, and may they run it?
|
|
173
|
+
const missing = globalTargets(ctx.env).filter((x) => found.includes(x.agent) && !isInstalled(x));
|
|
174
|
+
const agentsMissing = [...new Set(missing.map((x) => PROFILES[x.agent]?.name ?? x.agent))];
|
|
175
|
+
if (agentsMissing.length)
|
|
176
|
+
add("knows", "warn", t("ui.doc.knows.no", { agents: agentsMissing.join(", ") }), FIX, "knows");
|
|
177
|
+
else
|
|
178
|
+
add("knows", "ok", t("ui.doc.knows.yes"));
|
|
179
|
+
const level = ctx.settings.agentPermissions();
|
|
180
|
+
if (found.includes(chosen)) {
|
|
181
|
+
const where = whereToTalk(chosen, ctx.settings.agentWhere(), ctx.env);
|
|
182
|
+
const other = here.find((a) => a.id === chosen);
|
|
183
|
+
add("where", "ok", where === "app" ? t("ui.doc.where.app", { app: DESKTOP_APPS[chosen].name }) : t("ui.doc.where.terminal", { agent: PROFILES[chosen].name }), other.app && other.cli ? `strom config set agent.where ${where === "app" ? "terminal" : "app"}` : undefined);
|
|
184
|
+
}
|
|
185
|
+
add("level", "ok", t(`ui.setup.level.${level}`));
|
|
186
|
+
const model = ctx.settings.models(chosen, tree).lead;
|
|
187
|
+
if (model)
|
|
188
|
+
add("model", "ok", model);
|
|
189
|
+
}
|
|
190
|
+
// For a person: the shortcut on the desktop, the Strom app.
|
|
191
|
+
const desktop = desktopDir(ctx.env);
|
|
192
|
+
const shortcut = [`${shortcutName(lang)}.command`, `${shortcutName(lang)}.cmd`, "strom-research.desktop"].some((f) => fs.existsSync(path.join(desktop, f)));
|
|
193
|
+
if (shortcut)
|
|
194
|
+
add("shortcut", "ok", t("ui.doc.yes"));
|
|
195
|
+
else
|
|
196
|
+
add("shortcut", "warn", t("ui.doc.none"), FIX, "shortcut");
|
|
197
|
+
const app = stromAppState(ctx.settings);
|
|
198
|
+
add("app", "ok", t(`ui.doc.app.${app}`), app === "unknown" ? FIX : undefined, app === "unknown" ? "app" : undefined);
|
|
199
|
+
if (ctx.hasTree()) {
|
|
200
|
+
const tr = ctx.tree();
|
|
201
|
+
const errs = check(tr).filter((f) => f.level === "error").length + verifyFull(tr).findings.filter((f) => f.level === "error").length;
|
|
202
|
+
if (errs === 0)
|
|
203
|
+
add("tree", "ok", t("ui.doc.tree.ok", { name: tr.config.name }));
|
|
204
|
+
else
|
|
205
|
+
add("tree", "fail", t("ui.doc.tree.bad", { name: tr.config.name, n: errs }), "strom verify ; strom check");
|
|
206
|
+
}
|
|
207
|
+
return checks;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* What strom can put right itself, each with the user's yes: git, an agent
|
|
211
|
+
* (a person here only), what the agents know, the shortcut and the Strom app
|
|
212
|
+
* (a person here only — an agent's user does not need them from it).
|
|
213
|
+
*/
|
|
214
|
+
async function repair(ctx, checks, out) {
|
|
215
|
+
const lang = ctx.uiLang();
|
|
216
|
+
const person = ctx.interactive && !isAgent(ctx.env);
|
|
217
|
+
const todo = new Set(checks.filter((c) => c.status !== "ok" || c.repair === "app").map((c) => c.repair).filter(Boolean));
|
|
218
|
+
if (todo.has("git"))
|
|
219
|
+
await fixGit(ctx, out);
|
|
220
|
+
if (todo.has("agent") && person)
|
|
221
|
+
await offerAgent(ctx, out);
|
|
222
|
+
if (todo.has("knows") || todo.has("agent")) {
|
|
223
|
+
const found = agentsHere(ctx.env).map((a) => a.id);
|
|
224
|
+
for (const a of new Set(globalTargets(ctx.env).filter((x) => found.includes(x.agent) && installGlobal(x)).map((x) => x.agent)))
|
|
225
|
+
out(ui(lang, "ui.setup.skill", { agent: PROFILES[a].name }));
|
|
226
|
+
}
|
|
227
|
+
if (todo.has("shortcut") && person && (await ctx.confirm(ui(lang, "ui.setup.shortcut", {}), true))) {
|
|
228
|
+
for (const f of createShortcut(shortcutName(lang), ctx.env))
|
|
229
|
+
out(ui(lang, "ui.setup.shortcut.done", { file: ctx.display(f) }));
|
|
230
|
+
}
|
|
231
|
+
if (todo.has("app") && person && (await ctx.confirm(ui(lang, "ui.fix.app"), false))) {
|
|
232
|
+
openForUser(stromAppUrl(ctx.env), ctx.env);
|
|
233
|
+
out(ui(lang, "ui.app.install"));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
register({
|
|
237
|
+
path: ["doctor"],
|
|
238
|
+
summary: "Check everything strom needs: the program, git, folders, AI agents, what they know and may do, the Strom app, the current tree",
|
|
239
|
+
group: "setup",
|
|
240
|
+
description: "In the user's language; each problem says what to do. --fix puts right what strom can itself, each with the user's yes\n" +
|
|
241
|
+
"(in the terminal, or in a window of the system when an agent runs it): git (Windows: strom's own; macOS: Apple's tools),\n" +
|
|
242
|
+
"an AI agent, what the agents know, the shortcut on the desktop, the Strom app.",
|
|
243
|
+
options: [{ name: "fix", type: "boolean", description: "put right what strom can, with the user's yes" }],
|
|
244
|
+
examples: ["strom doctor", "strom doctor --fix", "strom doctor --json"],
|
|
245
|
+
async run(ctx, { opts }) {
|
|
246
|
+
const lang = ctx.uiLang();
|
|
247
|
+
const t = (key, values = {}) => ui(lang, key, values);
|
|
248
|
+
await newerVersion(ctx.settings, ctx.env, { fresh: true, timeoutMs: 3000 });
|
|
249
|
+
let checks = diagnose(ctx);
|
|
250
|
+
const said = [];
|
|
251
|
+
if (opts.fix) {
|
|
252
|
+
const fixable = checks.some((c) => c.repair && (c.status !== "ok" || c.repair === "app"));
|
|
253
|
+
if (fixable)
|
|
254
|
+
await repair(ctx, checks, (line) => (ctx.interactive ? ctx.io.stdout(line + "\n") : said.push(line)));
|
|
255
|
+
checks = diagnose(ctx);
|
|
256
|
+
}
|
|
257
|
+
const bad = checks.filter((c) => c.status === "fail").length;
|
|
258
|
+
const mark = { ok: "✓", warn: "!", fail: "✗" };
|
|
259
|
+
const text = lines(...said, ...(said.length ? [""] : []), table(checks.map((c) => [` ${mark[c.status]}`, c.label, c.detail, c.fix ? `→ ${c.fix}` : ""])), "", bad === 0 ? t("ui.doc.allok") : t("ui.doc.problems", { n: bad }));
|
|
260
|
+
return { text, data: { ok: bad === 0, checks: checks.map(({ repair: _, ...c }) => c) }, exitCode: bad === 0 ? 0 : 1 };
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
register({
|
|
264
|
+
path: ["update"],
|
|
265
|
+
summary: "Install the newest version of strom (from the project's releases, checked like the installer does) — with the user's yes",
|
|
266
|
+
group: "setup",
|
|
267
|
+
description: "strom looks for a new version at most once a day and says so (the menu, strom, strom doctor); the setting updates off\n" +
|
|
268
|
+
"stops it. A person at their terminal is asked there (--yes: no question); asked by an agent, strom asks in a window of the\n" +
|
|
269
|
+
"system. Installed with npm: npm install -g strom-research@latest.",
|
|
270
|
+
options: [{ name: "check", type: "boolean", description: "only say whether there is a new version" }],
|
|
271
|
+
examples: ["strom update --check", "strom update"],
|
|
272
|
+
async run(ctx, { opts }) {
|
|
273
|
+
const lang = ctx.uiLang();
|
|
274
|
+
const t = (key, values = {}) => ui(lang, key, values);
|
|
275
|
+
const newest = await latestVersion(ctx.env, 8000);
|
|
276
|
+
if (!newest)
|
|
277
|
+
throw new StromError(t("ui.update.unknown"), { hint: "check the network — or run the installer again" });
|
|
278
|
+
ctx.settings.config.updateCheck = { at: new Date().toISOString(), latest: newest };
|
|
279
|
+
ctx.settings.save();
|
|
280
|
+
if (!isNewer(newest, VERSION))
|
|
281
|
+
return { text: t("ui.update.current", { version: VERSION }), data: { current: VERSION, latest: newest, newer: false } };
|
|
282
|
+
if (opts.check)
|
|
283
|
+
return { text: t("ui.update.available", { version: newest, current: VERSION }), data: { current: VERSION, latest: newest, newer: true } };
|
|
284
|
+
const inst = installation();
|
|
285
|
+
if (inst.kind !== "installed")
|
|
286
|
+
return { text: t(inst.kind === "npm" ? "ui.update.npm" : "ui.update.source", { version: newest }), data: { current: VERSION, latest: newest, newer: true, npm: inst.kind === "npm" } };
|
|
287
|
+
const question = t("ui.update.sure", { version: newest, current: VERSION });
|
|
288
|
+
if (isAgent(ctx.env) || !ctx.yes) {
|
|
289
|
+
if (!isAgent(ctx.env) && ctx.interactive) {
|
|
290
|
+
if (!(await ctx.confirm(question, true)))
|
|
291
|
+
return { text: t("ui.update.later"), data: { current: VERSION, latest: newest, updated: false } };
|
|
292
|
+
}
|
|
293
|
+
else
|
|
294
|
+
ctx.requireHuman(`update strom to ${newest}`, "strom update", "update", question);
|
|
295
|
+
}
|
|
296
|
+
if (ctx.interactive)
|
|
297
|
+
ctx.io.stdout(`${t("ui.update.downloading", { version: newest })}\n`);
|
|
298
|
+
let version;
|
|
299
|
+
try {
|
|
300
|
+
version = await installUpdate(ctx.env, inst.root);
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
throw new StromError(t("ui.update.failed", { detail: e.message }), { hint: "strom update — or run the installer again" });
|
|
304
|
+
}
|
|
305
|
+
// The agents learn what the new version tells them.
|
|
306
|
+
const { command, args } = stromLauncher();
|
|
307
|
+
spawnSync(command, [...args, "agents", "install"], { stdio: "ignore", env: ctx.env, windowsHide: true });
|
|
308
|
+
return { text: t("ui.update.done", { version, previous: VERSION }), data: { previous: VERSION, version, updated: true } };
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
function display(ctx, def, value) {
|
|
312
|
+
if (value === undefined)
|
|
313
|
+
return "(unset)";
|
|
314
|
+
return def.kind === "path" ? ctx.display(String(value)) : String(value);
|
|
315
|
+
}
|
|
316
|
+
/** Effective value of a setting, with the default filled in. */
|
|
317
|
+
function effective(ctx, def) {
|
|
318
|
+
const s = ctx.settings;
|
|
319
|
+
const tree = ctx.hasTree() ? ctx.tree().config : undefined;
|
|
320
|
+
if (def.key === "home")
|
|
321
|
+
return s.home() ?? { value: undefined, source: "unset" };
|
|
322
|
+
if (def.key === "shared")
|
|
323
|
+
return s.shared() ?? { value: undefined, source: "unset" };
|
|
324
|
+
if (def.key === "trees")
|
|
325
|
+
return s.trees() ?? { value: undefined, source: "unset" };
|
|
326
|
+
if (def.key === "lang")
|
|
327
|
+
return s.lang(tree);
|
|
328
|
+
if (def.key === "agent")
|
|
329
|
+
return s.agent(tree);
|
|
330
|
+
const r = s.resolve(def.key, tree);
|
|
331
|
+
if (r)
|
|
332
|
+
return r;
|
|
333
|
+
if (def.kind === "model") {
|
|
334
|
+
const v = PROFILES[s.agent(tree).value]?.models[def.key.slice(6)];
|
|
335
|
+
return { value: v ?? "(the agent's own)", source: "default" };
|
|
336
|
+
}
|
|
337
|
+
if (def.key === "brief.budget")
|
|
338
|
+
return { value: DEFAULT_BUDGET, source: "default" };
|
|
339
|
+
if (def.key === "run.minutes")
|
|
340
|
+
return { value: DEFAULT_RUN_MINUTES, source: "default" };
|
|
341
|
+
if (def.key === "connectors.consent")
|
|
342
|
+
return { value: "off", source: "default" };
|
|
343
|
+
if (def.key === "browser.downloads")
|
|
344
|
+
return { value: downloadsDir(ctx.env), source: "detected" };
|
|
345
|
+
if (def.key === "agent.permissions")
|
|
346
|
+
return { value: ctx.settings.agentPermissions(), source: ctx.settings.config.agentPermissions ? "config" : "default" };
|
|
347
|
+
return { value: undefined, source: "unset" };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Change a setting of one tree (strom.json): a logged operation, committed
|
|
351
|
+
* together with the agent files that depend on it.
|
|
352
|
+
*/
|
|
353
|
+
export function setTreeSetting(ctx, key, value) {
|
|
354
|
+
const def = settingDef(key);
|
|
355
|
+
if (!def.tree)
|
|
356
|
+
throw new UsageError(`${key} is a setting of your computer, not of a tree`, { hint: `strom config set ${key} <value>` });
|
|
357
|
+
const tree = ctx.tree();
|
|
358
|
+
assertIntact(tree);
|
|
359
|
+
const agent = ctx.settings.agent(tree.config).value;
|
|
360
|
+
tree.updateConfig((c) => writeStored(c, key, agent, value), {
|
|
361
|
+
op: "config.set",
|
|
362
|
+
summary: value === undefined ? `tree setting ${key} removed` : `tree setting ${key} = ${value}`,
|
|
363
|
+
});
|
|
364
|
+
const files = syncAgentFiles(tree);
|
|
365
|
+
tree.withTreeLock(() => tree.commit(`Setting ${key}${value === undefined ? " removed" : `: ${value}`}`, ["strom.json", ...tree.opsFilesTouched(), ...files]));
|
|
366
|
+
tree.settle();
|
|
367
|
+
return tree;
|
|
368
|
+
}
|
|
369
|
+
/** What the user reads before they let the agent do everything but what is denied. */
|
|
370
|
+
export const BYPASS_WARNING = [
|
|
371
|
+
"Full: the agent does everything but what the tree's permissions deny, without asking.",
|
|
372
|
+
" · The allow list no longer counts. The agent may run any program on this computer, read and change",
|
|
373
|
+
" any file you can, and reach any web site — strom denies only a few (the evidence, its own settings",
|
|
374
|
+
" and instructions, git, curl and wget, your consents and logins), and cannot foresee every way round.",
|
|
375
|
+
" · A web page or a document the agent reads may carry instructions meant for it (prompt injection).",
|
|
376
|
+
" · strom still guards the evidence: a change in data/ that did not go through strom blocks every commit.",
|
|
377
|
+
" · Meant for a computer or an account that holds nothing else of value. Back: strom config set agent.permissions auto",
|
|
378
|
+
].join("\n");
|
|
379
|
+
/** Does this value let the agent do more than now? */
|
|
380
|
+
function raises(now, value) {
|
|
381
|
+
const next = (value ?? "auto");
|
|
382
|
+
return PERMISSION_LEVELS.indexOf(next) > PERMISSION_LEVELS.indexOf(now);
|
|
383
|
+
}
|
|
384
|
+
/** Change a user setting (the config file on this computer). */
|
|
385
|
+
function setUserSetting(ctx, key, value) {
|
|
386
|
+
const s = ctx.settings;
|
|
387
|
+
// Loosening the agent's permissions is the user's decision alone.
|
|
388
|
+
if (key === "agent.permissions" && raises(s.agentPermissions(), value))
|
|
389
|
+
ctx.requireHuman(`Let the agent ${value === "full" ? "do everything but what the tree's permissions deny" : "work on its own, asking only about risky steps"}, without asking?`, `strom config set agent.permissions ${value}`, "agent.permissions", ui(ctx.uiLang(), value === "full" ? "ui.consent.level.full" : "ui.consent.level.auto"));
|
|
390
|
+
// Asking before a connector runs is the user's safeguard: only they take it away.
|
|
391
|
+
if (key === "connectors.consent" && value !== "on" && s.connectorsConsent())
|
|
392
|
+
ctx.requireHuman("Let connectors run without asking you first?", `strom config set connectors.consent ${value ?? "off"}`, "connectors.consent", ui(ctx.uiLang(), "ui.consent.connectors.off"));
|
|
393
|
+
writeStored(s.config, key, s.agent(ctx.hasTree() ? ctx.tree().config : undefined).value, value);
|
|
394
|
+
s.save();
|
|
395
|
+
if (key === "shared" && typeof value === "string")
|
|
396
|
+
ensureShared(value);
|
|
397
|
+
}
|
|
398
|
+
register({
|
|
399
|
+
path: ["config", "where"],
|
|
400
|
+
summary: "Show every setting, its value and where it comes from (flag, env, tree, config, default)",
|
|
401
|
+
group: "setup",
|
|
402
|
+
run(ctx) {
|
|
403
|
+
const rows = SETTINGS.map((def) => {
|
|
404
|
+
const r = effective(ctx, def);
|
|
405
|
+
return { key: def.key, value: r.value, source: r.source, env: def.env, tree: def.tree, description: def.description };
|
|
406
|
+
});
|
|
407
|
+
const text = lines(table(rows.map((r) => [r.key, display(ctx, settingDef(r.key), r.value), r.source, r.env])), "", "order: flag > env > tree (strom.json) > config > default", `change: strom config set <key> <value> [--for-tree] · strom config unset <key> [--for-tree]`, `other env: ${OTHER_ENV.map((e) => e.env).join(" ")} (strom help config where)`, `config file: ${ctx.display(configFile(ctx.env))}`);
|
|
408
|
+
return { text, data: { settings: rows, otherEnv: OTHER_ENV, file: configFile(ctx.env) } };
|
|
409
|
+
},
|
|
410
|
+
description: "Settings a tree can carry (lang, agent, model.*, brief.budget, run.minutes) are set per tree with --for-tree.\n" +
|
|
411
|
+
OTHER_ENV.map((e) => `${e.env}: ${e.description}`).join("\n"),
|
|
412
|
+
}, {
|
|
413
|
+
path: ["config", "get"],
|
|
414
|
+
summary: "Print one setting (as it applies here)",
|
|
415
|
+
group: "setup",
|
|
416
|
+
args: [{ name: "key", description: `one of ${SETTINGS.map((s) => s.key).join(", ")}`, required: true }],
|
|
417
|
+
run(ctx, { args }) {
|
|
418
|
+
const def = settingDef(args[0]);
|
|
419
|
+
const r = effective(ctx, def);
|
|
420
|
+
return { text: r.value === undefined ? "" : display(ctx, def, r.value), data: { key: def.key, value: r.value, source: r.source } };
|
|
421
|
+
},
|
|
422
|
+
}, {
|
|
423
|
+
path: ["config", "set"],
|
|
424
|
+
summary: "Change a setting for you, or for the current tree only (--for-tree)",
|
|
425
|
+
group: "setup",
|
|
426
|
+
args: [
|
|
427
|
+
{ name: "key", description: SETTINGS.map((s) => s.key).join(", "), required: true },
|
|
428
|
+
{ name: "value", description: "a folder, a language code, an agent, a model name or a number", required: true },
|
|
429
|
+
],
|
|
430
|
+
options: [{ name: "for-tree", type: "boolean", description: "only for the current tree (saved in its strom.json)" }],
|
|
431
|
+
examples: ["strom config set lang cs", 'strom config set shared "/Volumes/Big/strom-shared"', "strom config set model.vision opus --for-tree", "strom config set run.minutes 45"],
|
|
432
|
+
run: async (ctx, { args, opts }) => {
|
|
433
|
+
const def = settingDef(args[0]);
|
|
434
|
+
const value = checkValue(def, args[1], (p) => ctx.resolvePath(p));
|
|
435
|
+
// In their own terminal the user reads what full means and says yes once more.
|
|
436
|
+
if (def.key === "agent.permissions" && value === "full" && ctx.settings.agentPermissions() !== "full" && !opts["for-tree"] && ctx.interactive && !isAgent(ctx.env)) {
|
|
437
|
+
ctx.io.stderr(BYPASS_WARNING + "\n");
|
|
438
|
+
const now = ctx.settings.agentPermissions();
|
|
439
|
+
if (!(await ctx.confirm("Turn full on?", false)))
|
|
440
|
+
return { text: `agent.permissions unchanged: ${now}`, data: { key: def.key, value: now, scope: "user" }, exitCode: 1 };
|
|
441
|
+
}
|
|
442
|
+
if (opts["for-tree"]) {
|
|
443
|
+
const tree = setTreeSetting(ctx, def.key, value);
|
|
444
|
+
return { text: `${def.key} = ${display(ctx, def, value)} for tree "${tree.config.name}"`, data: { key: def.key, value, scope: "tree" } };
|
|
445
|
+
}
|
|
446
|
+
setUserSetting(ctx, def.key, value);
|
|
447
|
+
return { text: `${def.key} = ${display(ctx, def, value)}`, data: { key: def.key, value, scope: "user" } };
|
|
448
|
+
},
|
|
449
|
+
}, {
|
|
450
|
+
path: ["config", "unset"],
|
|
451
|
+
summary: "Remove a setting (back to the next source: the user config, then the default)",
|
|
452
|
+
group: "setup",
|
|
453
|
+
args: [{ name: "key", description: SETTINGS.map((s) => s.key).join(", "), required: true }],
|
|
454
|
+
options: [{ name: "for-tree", type: "boolean", description: "remove the tree's own value" }],
|
|
455
|
+
examples: ["strom config unset shared", "strom config unset agent --for-tree"],
|
|
456
|
+
run(ctx, { args, opts }) {
|
|
457
|
+
const def = settingDef(args[0]);
|
|
458
|
+
if (opts["for-tree"])
|
|
459
|
+
setTreeSetting(ctx, def.key, undefined);
|
|
460
|
+
else
|
|
461
|
+
setUserSetting(ctx, def.key, undefined);
|
|
462
|
+
const r = effective(ctx, def);
|
|
463
|
+
return { text: `${def.key} unset — now ${display(ctx, def, r.value)} (${r.source})`, data: { key: def.key, value: r.value, source: r.source } };
|
|
464
|
+
},
|
|
465
|
+
});
|