strom-research 1.2.0 → 1.4.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/README.md +4 -1
- package/assets/lang/cs.json +164 -7
- package/assets/lang/de.json +164 -7
- package/assets/plugins/README.md +8 -1
- package/assets/plugins/gates/README.md +1 -1
- package/assets/plugins/hooks/README.md +91 -0
- package/dist/agents/files.js +142 -99
- package/dist/agents/global.js +83 -6
- package/dist/agents/launch.js +12 -5
- package/dist/agents/profiles.js +13 -2
- package/dist/cli/main.js +17 -1
- package/dist/cli/menu-parts.js +140 -0
- package/dist/cli/menu-research.js +194 -0
- package/dist/cli/menu-settings.js +273 -0
- package/dist/cli/menu-waiting.js +79 -0
- package/dist/cli/menu.js +91 -97
- package/dist/cli/registry.js +1 -1
- package/dist/cli/ui.js +162 -6
- package/dist/cli/wizard.js +48 -2
- package/dist/commands/browse.js +2 -2
- package/dist/commands/connectors.js +25 -2
- package/dist/commands/hooks.js +141 -0
- package/dist/commands/index.js +1 -0
- package/dist/commands/intake.js +2 -2
- package/dist/commands/meta.js +6 -2
- package/dist/commands/research.js +55 -16
- package/dist/commands/session.js +15 -7
- package/dist/commands/setup.js +51 -2
- package/dist/commands/start.js +9 -4
- package/dist/commands/tasks.js +41 -4
- package/dist/core/browser.js +50 -0
- package/dist/core/config.js +7 -1
- package/dist/core/hooks.js +210 -0
- package/dist/core/install.js +3 -2
- package/dist/core/media.js +21 -7
- package/dist/core/phrases.js +1 -0
- package/dist/core/relocate.js +111 -0
- package/dist/core/shortcut.js +1 -4
- package/dist/core/tree.js +19 -2
- package/dist/core/which.js +18 -0
- package/dist/runners/claude.js +3 -0
- package/dist/runners/codex.js +14 -5
- package/dist/runners/grok.js +108 -0
- package/dist/runners/index.js +2 -0
- package/dist/runners/opencode.js +8 -4
- package/package.json +1 -1
package/dist/cli/wizard.js
CHANGED
|
@@ -18,6 +18,7 @@ import { openForUser } from "../core/open.js";
|
|
|
18
18
|
import { createShortcut } from "../core/shortcut.js";
|
|
19
19
|
import { desktopDir } from "../core/paths.js";
|
|
20
20
|
import { ensureShared } from "../commands/setup.js";
|
|
21
|
+
import { moveHome, planMove, repointSettings, sameFolder } from "../core/relocate.js";
|
|
21
22
|
/** The shortcut's name, in the user's language. */
|
|
22
23
|
export function shortcutName(lang) {
|
|
23
24
|
return ui(lang, "ui.dialog.title");
|
|
@@ -46,9 +47,18 @@ export async function setupWizard(ctx) {
|
|
|
46
47
|
// Run again: each choice can be left as it is (0), the current one suggested.
|
|
47
48
|
const keep = first ? {} : { back: ui(lang, "ui.keep") };
|
|
48
49
|
// 2. Where the research lives.
|
|
49
|
-
const
|
|
50
|
+
const was = s.home()?.value;
|
|
51
|
+
const typed = (await ctx.ask(ui(lang, first ? "ui.setup.home" : "ui.setup.home.again"), ctx.display(was ?? s.suggestedHome()))).trim();
|
|
52
|
+
// Run again: 0 (or nothing) keeps the folder, like every other answer here.
|
|
53
|
+
let home = was && (typed === "0" || !typed) ? was : ctx.resolvePath(typed || ctx.display(s.suggestedHome()));
|
|
54
|
+
// Another folder while the research is in the old one: it moves along (trees, the shared folder) — or stays where it is.
|
|
55
|
+
if (was && !sameFolder(home, was))
|
|
56
|
+
home = await moveResearch(ctx, lang, was, home);
|
|
50
57
|
cfg.lang = lang;
|
|
51
|
-
|
|
58
|
+
if (was && !sameFolder(home, was))
|
|
59
|
+
repointSettings(s, ctx.env, was, home);
|
|
60
|
+
else if (!was)
|
|
61
|
+
cfg.home = home;
|
|
52
62
|
s.save();
|
|
53
63
|
fs.mkdirSync(home, { recursive: true });
|
|
54
64
|
fs.mkdirSync(s.trees().value, { recursive: true });
|
|
@@ -167,3 +177,39 @@ export async function askStromApp(ctx, lang, back = ui(lang, "ui.browse.back"))
|
|
|
167
177
|
}
|
|
168
178
|
return answer;
|
|
169
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* The research to another folder: what moves is said (the trees, the shared folder with the plugins and images), and it
|
|
182
|
+
* moves on the person's yes. Where it cannot (somebody at work, a folder with something in it, into itself) it stays.
|
|
183
|
+
* Returns the folder the research is in afterwards.
|
|
184
|
+
*/
|
|
185
|
+
async function moveResearch(ctx, lang, from, to) {
|
|
186
|
+
const out = (line) => ctx.io.stdout(line + "\n");
|
|
187
|
+
// Set from outside (STROM_HOME): the folder is not the wizard's to change.
|
|
188
|
+
if (ctx.settings.home()?.source !== "config") {
|
|
189
|
+
out(ui(lang, "ui.home.env", { from: ctx.display(from) }));
|
|
190
|
+
return from;
|
|
191
|
+
}
|
|
192
|
+
const plan = planMove(from, to, ctx.knownTrees().map((k) => k.root));
|
|
193
|
+
if (!plan.content)
|
|
194
|
+
return to;
|
|
195
|
+
const where = { from: ctx.display(from), to: ctx.display(plan.to) };
|
|
196
|
+
if (plan.problem) {
|
|
197
|
+
out(ui(lang, `ui.home.${plan.problem}`, { ...where, trees: plan.busy.join(", ") }));
|
|
198
|
+
out(ui(lang, "ui.home.stays", where));
|
|
199
|
+
return from;
|
|
200
|
+
}
|
|
201
|
+
out(ui(lang, "ui.home.what", { ...where, trees: plan.trees.length ? plan.trees.join(", ") : "–" }));
|
|
202
|
+
if (!(await ctx.confirm(ui(lang, "ui.home.sure"), true))) {
|
|
203
|
+
out(ui(lang, "ui.home.stays", where));
|
|
204
|
+
return from;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const how = moveHome(plan);
|
|
208
|
+
out(ui(lang, how === "moved" ? "ui.home.moved" : "ui.home.copied", where));
|
|
209
|
+
return plan.to;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
out(ui(lang, "ui.home.failed", { ...where, reason: err.message }));
|
|
213
|
+
return from;
|
|
214
|
+
}
|
|
215
|
+
}
|
package/dist/commands/browse.js
CHANGED
|
@@ -115,7 +115,7 @@ register({
|
|
|
115
115
|
(s.sessions.costUsd ? ui(lang, "ui.stats.cost", { cost: humanCost(s.sessions.costUsd, lang) }) : "") +
|
|
116
116
|
(s.sessions.costUsd && s.sessions.costPartial ? ui(lang, "ui.stats.cost.partial", { n: s.sessions.costPartial }) : ""));
|
|
117
117
|
if (s.stories.written)
|
|
118
|
-
out.push(ui(lang, "ui.stats.stories", { n: s.stories.written
|
|
118
|
+
out.push(ui(lang, "ui.stats.stories", { n: s.stories.written }));
|
|
119
119
|
return { text: lines(...out), data: s };
|
|
120
120
|
},
|
|
121
121
|
}, {
|
|
@@ -164,7 +164,7 @@ register({
|
|
|
164
164
|
}
|
|
165
165
|
if (c.story) {
|
|
166
166
|
const text = c.story.text.replace(/\*\*/g, "").trim();
|
|
167
|
-
out.push("", ui(lang, "ui.card.story"
|
|
167
|
+
out.push("", ui(lang, "ui.card.story"));
|
|
168
168
|
// markdown headings as plain lines
|
|
169
169
|
out.push(text.split("\n").map((l) => (l.trim() ? ` ${l.trim().replace(/^#+\s*/, "")}` : "")).join("\n"));
|
|
170
170
|
}
|
|
@@ -26,7 +26,8 @@ import { imageSizeOfFile } from "../image/index.js";
|
|
|
26
26
|
import { Tree } from "../core/tree.js";
|
|
27
27
|
import { syncAgentFiles } from "../agents/files.js";
|
|
28
28
|
import { clearBlock, CookieJar, DEFAULT_PACE, hostAllowed, hostPace, hostState, knownHosts, MIN_INTERVAL_MS, NetError, paceText, politeRequest, refusedBy, reserveSlots, setOwnPace } from "../core/net.js";
|
|
29
|
-
import { botCheck, fileBase, findDownload, loadPage, loadPlans, pageKey, pageScript, parseResult, PLAN_TTL_MS, planScript, readSavedPage, removePlan, savePage, savePlan, SCRIPT_MS, } from "../core/browser.js";
|
|
29
|
+
import { botCheck, fileBase, findDownload, loadPage, loadPlans, pageKey, pageScript, parseResult, PLAN_TTL_MS, planScript, readSavedPage, removePlan, savePage, savePlan, SCRIPT_MS, claudeInChrome, CLAUDE_IN_CHROME_URL, } from "../core/browser.js";
|
|
30
|
+
import { PROFILES } from "../agents/profiles.js";
|
|
30
31
|
import { bareHost, changedSinceConsent, codeWarning, consentRequired, connectorHash, connectorsDir, directNetwork, ensurePluginsDir, findConnector, hostWarning, imageProblem, INTERFACE, listConnectors, loadConsents, MANIFEST, missingConsents, NAME_RE, readManifest, routeOf, treeBrowserConnectors, routesOf, ROUTES, runConnector, saveConsents, scanConnectors, suggestName, } from "../core/connector.js";
|
|
31
32
|
import { registerImages } from "./media.js";
|
|
32
33
|
function shared(ctx) {
|
|
@@ -304,7 +305,7 @@ register({
|
|
|
304
305
|
const hosts = c.manifest.hosts.map(bareHost);
|
|
305
306
|
return {
|
|
306
307
|
text: via === "browser"
|
|
307
|
-
? lines(`${c.name}: images through your browser from now on (${hosts.join(", ")})`, ` · the agent gets browser tools (Claude in Chrome) for these sites only, in the family trees that work with this`, ` archive (images fetched through it, or an archive, book or record of theirs on its site), from its next session${synced.length ? ` (permissions of ${synced.join(", ")} updated)` : ""}. Chrome with the`, " Claude extension must be running then, signed in to the account Claude Code uses; browser tools need a model", " that can review its actions (Sonnet or Opus, not Haiku)", ` · strom plans and paces every request, and takes the files over from ${ctx.display(s.downloads())}`, " (another folder: strom config set browser.downloads <folder>)", ` · the first time, Chrome asks whether ${hosts[0]} may download several files: allow it once`, ` · back: strom connector use ${c.name} --via direct
|
|
308
|
+
? lines(`${c.name}: images through your browser from now on (${hosts.join(", ")})`, ` · the agent gets browser tools (Claude in Chrome) for these sites only, in the family trees that work with this`, ` archive (images fetched through it, or an archive, book or record of theirs on its site), from its next session${synced.length ? ` (permissions of ${synced.join(", ")} updated)` : ""}. Chrome with the`, " Claude extension must be running then, signed in to the account Claude Code uses; browser tools need a model", " that can review its actions (Sonnet or Opus, not Haiku)", ` · strom plans and paces every request, and takes the files over from ${ctx.display(s.downloads())}`, " (another folder: strom config set browser.downloads <folder>)", ` · the first time, Chrome asks whether ${hosts[0]} may download several files: allow it once`, ` · back: strom connector use ${c.name} --via direct`, ...browserWarnings(ctx))
|
|
308
309
|
: lines(`${c.name}: images directly through strom from now on, paced (${hosts.join(", ")})`, routesOf(c).includes("browser") ? ` · through your browser again: strom connector use ${c.name} --via browser` : undefined),
|
|
309
310
|
data: { connector: c.name, via, hosts, synced },
|
|
310
311
|
};
|
|
@@ -1440,3 +1441,25 @@ function textFiles(dir) {
|
|
|
1440
1441
|
walk(dir);
|
|
1441
1442
|
return out;
|
|
1442
1443
|
}
|
|
1444
|
+
/**
|
|
1445
|
+
* What the person is told of the archives this tree reaches through their browser, for the agent that works: only
|
|
1446
|
+
* Claude Code has browser tools (Claude in Chrome), and the extension must be in a browser here. Nothing to say: undefined.
|
|
1447
|
+
*/
|
|
1448
|
+
export function browserNote(ctx, tree, agent) {
|
|
1449
|
+
const via = treeBrowserConnectors(tree, ctx.settings.shared()?.value);
|
|
1450
|
+
if (!via.length)
|
|
1451
|
+
return undefined;
|
|
1452
|
+
const lang = ctx.uiLang();
|
|
1453
|
+
const names = via.map((c) => c.manifest.title ?? c.name).join(", ");
|
|
1454
|
+
if (agent !== "claude")
|
|
1455
|
+
return ui(lang, "ui.browser.agent", { names, agent: PROFILES[agent]?.name ?? agent });
|
|
1456
|
+
const found = claudeInChrome(ctx.env).extension;
|
|
1457
|
+
return found.length ? undefined : ui(lang, "ui.browser.noext", { names, url: CLAUDE_IN_CHROME_URL });
|
|
1458
|
+
}
|
|
1459
|
+
/** Through the browser: what stands in the way here (English, for the agent — which tells the user). */
|
|
1460
|
+
function browserWarnings(ctx) {
|
|
1461
|
+
const agent = ctx.settings.agent(ctx.hasTree() ? ctx.tree().config : undefined).value;
|
|
1462
|
+
if (agent !== "claude")
|
|
1463
|
+
return [` ⚠ only Claude Code has browser tools (Claude in Chrome); this research's agent is ${PROFILES[agent]?.name ?? agent} — tell the user: they save these images by hand, or choose Claude Code (strom agents use claude)`];
|
|
1464
|
+
return claudeInChrome(ctx.env).extension.length ? [] : [` ⚠ the Claude in Chrome extension is in no browser here — tell the user to install it and sign in: ${CLAUDE_IN_CHROME_URL}`];
|
|
1465
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Hooks: the user's programs told of what is saved into a research (core/hooks.ts).
|
|
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 { ui } from "../cli/ui.js";
|
|
7
|
+
import { StromError, UsageError } from "../core/errors.js";
|
|
8
|
+
import { readJsonLines } from "../core/json.js";
|
|
9
|
+
import { prependPath } from "../runners/runner.js";
|
|
10
|
+
import { shimDir } from "./session.js";
|
|
11
|
+
import { ensureHooksDir, forHook, HOOK_INTERFACE, hooksDir, listHooks, loadHook, runHookNow } from "../core/hooks.js";
|
|
12
|
+
function shared(ctx) {
|
|
13
|
+
const s = ctx.settings.shared()?.value;
|
|
14
|
+
if (!s)
|
|
15
|
+
throw new StromError("strom is not set up yet", { hint: "strom setup" });
|
|
16
|
+
ensureHooksDir(s);
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
/** Turning a hook on or off is the user's decision: an agent asks them (a window of the system). */
|
|
20
|
+
function setHook(ctx, name, on) {
|
|
21
|
+
const s = shared(ctx);
|
|
22
|
+
const now = ctx.settings.config.hooks ?? [];
|
|
23
|
+
if (now.includes(name) === on)
|
|
24
|
+
return { text: `${name}: ${on ? "on" : "off"} already`, data: { hook: name, on, hooks: now } };
|
|
25
|
+
// off: also a hook whose folder is gone or broken — it goes from the list all the same
|
|
26
|
+
let hook;
|
|
27
|
+
try {
|
|
28
|
+
hook = loadHook(s, name);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (on)
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
const title = hook?.manifest.title ?? name;
|
|
35
|
+
ctx.requireHuman(on ? `Let the hook "${name}" be told of everything saved into your researches?` : `Stop telling the hook "${name}"?`, `strom hook ${on ? "on" : "off"} ${name}`, "hooks", ui(ctx.uiLang(), on ? "ui.consent.hook.on" : "ui.consent.hook.off", { name: title }));
|
|
36
|
+
ctx.settings.reload();
|
|
37
|
+
const next = on ? [...(ctx.settings.config.hooks ?? []).filter((h) => h !== name), name] : (ctx.settings.config.hooks ?? []).filter((h) => h !== name);
|
|
38
|
+
ctx.settings.config.hooks = next.length ? next : undefined;
|
|
39
|
+
ctx.settings.save();
|
|
40
|
+
return {
|
|
41
|
+
text: on && hook ? `${name}: on — told of what is saved (${(hook.manifest.events ?? ["*"]).join(", ")}); its output: ${ctx.display(path.join(hook.dir, "hook.log"))}` : `${name}: off`,
|
|
42
|
+
data: { hook: name, on, hooks: next },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** The last operations saved into this research, newest last. */
|
|
46
|
+
function lastOps(root, n) {
|
|
47
|
+
const dir = path.join(root, "data", "ops");
|
|
48
|
+
let files = [];
|
|
49
|
+
try {
|
|
50
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return files
|
|
56
|
+
.flatMap((f) => readJsonLines(path.join(dir, f)))
|
|
57
|
+
.sort((a, b) => a.at.localeCompare(b.at))
|
|
58
|
+
.slice(-n)
|
|
59
|
+
.map((o) => ({ op: o.op, targets: o.targets, summary: o.summary, at: o.at, by: o.by, ...(o.reason ? { reason: o.reason } : {}) }));
|
|
60
|
+
}
|
|
61
|
+
register({
|
|
62
|
+
path: ["hook", "list"],
|
|
63
|
+
summary: "Hooks: your programs told of what is saved into a research (a message to your phone, a log…) — which there are, which are on",
|
|
64
|
+
group: "setup",
|
|
65
|
+
run(ctx) {
|
|
66
|
+
const s = shared(ctx);
|
|
67
|
+
const on = ctx.settings.config.hooks ?? [];
|
|
68
|
+
const hooks = listHooks(s);
|
|
69
|
+
return {
|
|
70
|
+
text: lines(hooks.length
|
|
71
|
+
? table(hooks.map((h) => [on.includes(h.name) ? `${h.name} ◀ on` : h.name, h.manifest.title ?? "", (h.manifest.events ?? ["*"]).join(" "), h.manifest.command.join(" ")]))
|
|
72
|
+
: "no hooks", on.length ? `on: ${on.join(", ")} (strom hook off <name>)` : "none is on: strom hook on <name> (only you)", `folder: ${ctx.display(hooksDir(s))} — copy a hook's folder in to install it (the interface: README.md there)`),
|
|
73
|
+
data: {
|
|
74
|
+
folder: hooksDir(s),
|
|
75
|
+
on,
|
|
76
|
+
hooks: hooks.map((h) => ({ name: h.name, title: h.manifest.title, events: h.manifest.events ?? ["*"], command: h.manifest.command, dir: h.dir, on: on.includes(h.name) })),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
}, {
|
|
81
|
+
path: ["hook", "on"],
|
|
82
|
+
summary: "Turn a hook on: from now on it is told of what is saved into your researches (only you)",
|
|
83
|
+
group: "setup",
|
|
84
|
+
args: [{ name: "name", description: "the hook's folder name", required: true }],
|
|
85
|
+
examples: ["strom hook on telegram"],
|
|
86
|
+
run: (ctx, { args }) => setHook(ctx, args[0], true),
|
|
87
|
+
}, {
|
|
88
|
+
path: ["hook", "off"],
|
|
89
|
+
summary: "Turn a hook off (only you)",
|
|
90
|
+
group: "setup",
|
|
91
|
+
args: [{ name: "name", description: "the hook's folder name", required: true }],
|
|
92
|
+
run: (ctx, { args }) => setHook(ctx, args[0], false),
|
|
93
|
+
}, {
|
|
94
|
+
path: ["hook", "test"],
|
|
95
|
+
summary: "Run a hook now on what was saved last in this research (or one operation you name), wait for it and show what it printed",
|
|
96
|
+
group: "setup",
|
|
97
|
+
tree: true,
|
|
98
|
+
args: [{ name: "name", description: "the hook's folder name", required: true }],
|
|
99
|
+
options: [
|
|
100
|
+
{ name: "last", type: "string", value: "<n>", description: "the last n operations saved (default 5)" },
|
|
101
|
+
{ name: "op", type: "string", value: "<op>", description: "one operation instead, e.g. person.add (with --target)" },
|
|
102
|
+
{ name: "target", type: "string", value: "<ID>", description: "the record it concerns, e.g. P0001" },
|
|
103
|
+
],
|
|
104
|
+
examples: ["strom hook test telegram", "strom hook test telegram --op person.add --target P0001"],
|
|
105
|
+
run(ctx, { args, opts }) {
|
|
106
|
+
const s = shared(ctx);
|
|
107
|
+
const hook = loadHook(s, args[0]);
|
|
108
|
+
const tree = ctx.tree();
|
|
109
|
+
const n = opts.last === undefined ? 5 : Number(opts.last);
|
|
110
|
+
if (!Number.isInteger(n) || n < 1)
|
|
111
|
+
throw new UsageError("--last must be a positive number");
|
|
112
|
+
const all = typeof opts.op === "string"
|
|
113
|
+
? [{ op: opts.op, targets: typeof opts.target === "string" ? [opts.target] : [], summary: `(a test of ${opts.op})`, at: new Date().toISOString(), by: "user" }]
|
|
114
|
+
: lastOps(tree.root, n);
|
|
115
|
+
const events = forHook(hook, all);
|
|
116
|
+
if (!events.length)
|
|
117
|
+
return {
|
|
118
|
+
text: `${hook.name} wants none of these: ${[...new Set(all.map((e) => e.op))].join(", ") || "nothing saved yet"} (its events: ${(hook.manifest.events ?? ["*"]).join(", ")})`,
|
|
119
|
+
data: { hook: hook.name, events: [] },
|
|
120
|
+
};
|
|
121
|
+
const r = runHookNow(hook, prependPath(ctx.env, shimDir(tree)), {
|
|
122
|
+
interface: HOOK_INTERFACE,
|
|
123
|
+
hook: hook.name,
|
|
124
|
+
tree: { name: tree.config.name, id: tree.config.id, root: tree.root, lang: tree.lang },
|
|
125
|
+
commit: "test",
|
|
126
|
+
at: new Date().toISOString(),
|
|
127
|
+
events,
|
|
128
|
+
});
|
|
129
|
+
const ok = !r.error && r.status === 0;
|
|
130
|
+
return {
|
|
131
|
+
text: lines(`${hook.name}: ${events.length} operation(s) — ${ok ? "ended well" : `failed: ${r.error ?? `exit status ${r.status}`}`}`, r.output
|
|
132
|
+
? r.output
|
|
133
|
+
.split("\n")
|
|
134
|
+
.map((l) => ` ${l}`)
|
|
135
|
+
.join("\n")
|
|
136
|
+
: undefined),
|
|
137
|
+
data: { hook: hook.name, events, status: r.status, ...(r.error ? { error: r.error } : {}), output: r.output },
|
|
138
|
+
exitCode: ok ? 0 : 1,
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
});
|
package/dist/commands/index.js
CHANGED
package/dist/commands/intake.js
CHANGED
|
@@ -67,8 +67,8 @@ function groupTask(tree, inputs, research, imported, folder) {
|
|
|
67
67
|
return create(tree, "task", { level: "intake", priority: 4, what, where: ids, why, doneWhen, subject: ids, research, state: "open", origin: tree.actor }, (id) => `+${id} task "${truncate(what, 60)}"`);
|
|
68
68
|
}
|
|
69
69
|
/** More images than this in one folder look like the scans of a book, not family documents. */
|
|
70
|
-
const BOOK_OF_SCANS = 20;
|
|
71
|
-
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".tif", ".tiff", ".jp2", ".gif", ".webp", ".heic"]);
|
|
70
|
+
export const BOOK_OF_SCANS = 20;
|
|
71
|
+
export const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".tif", ".tiff", ".jp2", ".gif", ".webp", ".heic"]);
|
|
72
72
|
register({
|
|
73
73
|
path: ["intake"],
|
|
74
74
|
summary: "Take in material to research from: folders, files, a family tree (GEDCOM / Strom JSON), or text",
|
package/dist/commands/meta.js
CHANGED
|
@@ -20,6 +20,7 @@ import { currentSession, openSessions } from "../core/session.js";
|
|
|
20
20
|
import { readByOtherModels } from "../core/review.js";
|
|
21
21
|
import { liveHolder } from "../core/lock.js";
|
|
22
22
|
import { isAgent } from "../core/which.js";
|
|
23
|
+
import { agentsHere } from "../core/apps.js";
|
|
23
24
|
import { taskQueue, waitingForUser, waitingLines } from "./tasks.js";
|
|
24
25
|
import { liveWorkers, runAlive } from "../core/workers.js";
|
|
25
26
|
import { label } from "../core/people.js";
|
|
@@ -114,10 +115,13 @@ function orientation(ctx) {
|
|
|
114
115
|
const open = currentSession(tree, ctx.env) ?? sessions.find((s) => !busy(s) && !(s.worker && s.worker !== ctx.env.STROM_WORKER && !human));
|
|
115
116
|
const queued = taskQueue(tree).length;
|
|
116
117
|
// An agent outside the tree that strom did not start (it set strom up from the web page, or was
|
|
117
|
-
// opened in some folder): the research goes on in its own conversation, where the tree lives
|
|
118
|
+
// opened in some folder): the research goes on in its own conversation, where the tree lives — when
|
|
119
|
+
// strom can start one here (the research's agent is on this computer; not so for a bot on its own
|
|
120
|
+
// server, e.g. Grok Bot: it goes on where it is).
|
|
118
121
|
const rel = path.relative(tree.root, ctx.cwd);
|
|
119
122
|
const inTree = rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
120
|
-
const
|
|
123
|
+
const chosen = ctx.settings.agent(tree.config).value;
|
|
124
|
+
const elsewhere = isAgent(ctx.env) && !ctx.env.STROM_WORKER && !ctx.env.STROM_SESSION && !inTree && agentsHere(ctx.env).some((a) => a.id === chosen);
|
|
121
125
|
if (base.tree.errors > 0)
|
|
122
126
|
base.next = { why: t("ui.why.check"), command: "strom check" };
|
|
123
127
|
else if (elsewhere)
|
|
@@ -3,12 +3,16 @@ import { register } from "../cli/registry.js";
|
|
|
3
3
|
import { lines, table } from "../cli/format.js";
|
|
4
4
|
import { addPerson, addResearch } from "../core/actions.js";
|
|
5
5
|
import { applyFrontier, researchProposals } from "./session.js";
|
|
6
|
+
import { frontier } from "../core/frontier.js";
|
|
7
|
+
import { parkedWhy } from "./tasks.js";
|
|
8
|
+
import { ui } from "../cli/ui.js";
|
|
6
9
|
import { phrase } from "../core/phrases.js";
|
|
7
10
|
import { UsageError } from "../core/errors.js";
|
|
8
11
|
import { REVIEW_SCOPES } from "../core/model.js";
|
|
9
12
|
import { ancestorGenerations, displayName, label, lifespan, parentsOf, resolvePerson } from "../core/people.js";
|
|
10
13
|
import { foldText } from "../core/text.js";
|
|
11
14
|
import { update } from "../core/records.js";
|
|
15
|
+
import { isAgent } from "../core/which.js";
|
|
12
16
|
function int(v, name) {
|
|
13
17
|
if (v === undefined)
|
|
14
18
|
return undefined;
|
|
@@ -175,22 +179,31 @@ register({
|
|
|
175
179
|
else if (opts.model)
|
|
176
180
|
throw new UsageError("--model goes with --reread");
|
|
177
181
|
const review = { scope: scope, ...(reread ? { reread } : {}) };
|
|
182
|
+
const lang = tree.lang;
|
|
178
183
|
// one review research per person: again later, it goes on where it was
|
|
179
184
|
const existing = tree.list("research").find((r) => r.direction === "person" && r.focus === p.id && r.review);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
// What it would propose, asked before anything is written: with nothing to do, no research is made for it.
|
|
186
|
+
const draft = existing
|
|
187
|
+
? { ...existing, state: "active", review }
|
|
188
|
+
: { id: "G?", type: "research", name: phrase(lang, "review.research", { name: displayName(p) }), focus: p.id, direction: "person", state: "active", review, notes: [] };
|
|
189
|
+
const planned = researchProposals(tree, draft).map((x) => x.proposal);
|
|
190
|
+
let research = draft;
|
|
191
|
+
let tasks = [];
|
|
192
|
+
if (planned.length && !tree.dryRun) {
|
|
193
|
+
if (existing)
|
|
194
|
+
research = update(tree, existing.id, "research", (r) => ({ ...r, state: "active", review }), {
|
|
195
|
+
op: "research.edit",
|
|
196
|
+
summary: `${existing.id} review again: ${scope}${reread ? `, second reading with ${reread}` : ""}`,
|
|
197
|
+
});
|
|
198
|
+
else {
|
|
199
|
+
const r = addResearch(tree, { name: draft.name, focus: p.id, direction: "person" });
|
|
200
|
+
research = update(tree, r.id, "research", (x) => ({ ...x, review }), { op: "research.edit", summary: `${r.id} review: ${scope}${reread ? `, second reading with ${reread}` : ""}` });
|
|
201
|
+
}
|
|
202
|
+
tasks = applyFrontier(tree, research)
|
|
203
|
+
.map((id) => tree.get(id))
|
|
204
|
+
.filter((t) => !!t);
|
|
189
205
|
}
|
|
190
|
-
const
|
|
191
|
-
const created = tree.dryRun ? [] : applyFrontier(tree, research);
|
|
192
|
-
const tasks = created.map((id) => tree.get(id)).filter((t) => !!t);
|
|
193
|
-
const open = tree.list("task").filter((t) => t.research === research.id && ["open", "doing"].includes(t.state));
|
|
206
|
+
const open = existing ? tree.list("task").filter((t) => t.research === existing.id && ["open", "doing"].includes(t.state)) : [];
|
|
194
207
|
// what a session costs here, from those so far: the user decides with the price in view
|
|
195
208
|
const costs = tree
|
|
196
209
|
.list("session")
|
|
@@ -198,8 +211,34 @@ register({
|
|
|
198
211
|
.map((s) => s.metrics?.costUsd)
|
|
199
212
|
.filter((c) => typeof c === "number");
|
|
200
213
|
const avg = costs.length ? costs.reduce((a, b) => a + b, 0) / costs.length : undefined;
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
214
|
+
// A person at a terminal (the menu) reads the tasks and what comes next, not the operations and commands.
|
|
215
|
+
const human = ctx.io.tty && !isAgent(ctx.env);
|
|
216
|
+
const list = tree.dryRun
|
|
217
|
+
? planned.map((t) => ui(lang, "ui.review.would", { level: t.level, what: t.what }))
|
|
218
|
+
: tasks.map((t) => (human ? ` • ${t.what}` : ` ${t.id} ${t.level} · ${t.what}`));
|
|
219
|
+
// Nothing new: why — the work that covers them already (a parked task says how to go on), a search done in vain.
|
|
220
|
+
const why = list.length ? [] : coveredLines(tree, draft, lang, human);
|
|
221
|
+
const inReview = existing ?? (planned.length && !tree.dryRun ? research : undefined);
|
|
222
|
+
const text = lines(...(human ? [] : tree.written.map((o) => o.summary)), tree.dryRun ? ui(lang, "ui.review.dry") : undefined, (tree.written.length && !human) || tree.dryRun ? "" : undefined, list.length ? ui(lang, "ui.review.new", { name: label(p), n: list.length }) : ui(lang, "ui.review.nothing", { name: label(p) }), ...list, ...why, open.length || tasks.length
|
|
223
|
+
? ui(lang, "ui.review.open", { n: open.length || tasks.length, research: inReview.id }) + (avg !== undefined ? ui(lang, "ui.review.cost", { cost: avg.toFixed(2) }) : "")
|
|
224
|
+
: undefined, (open.length || tasks.length) && inReview ? ui(lang, human ? "ui.review.next.human" : "ui.review.next", { research: inReview.id }) : undefined);
|
|
225
|
+
return { text, data: { research: inReview ?? null, created: tasks, planned, open: open.map((t) => t.id), ...(why.length ? { why } : {}), ...(avg !== undefined ? { avgCostUsd: avg } : {}) } };
|
|
204
226
|
},
|
|
205
227
|
});
|
|
228
|
+
/** Why a review found nothing new, person by person: the work that covers them already, or a search done in vain. */
|
|
229
|
+
function coveredLines(tree, research, lang, human = false) {
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const item of frontier(tree, research)) {
|
|
232
|
+
const name = label(item.person);
|
|
233
|
+
const t = item.coveredBy ? tree.get(item.coveredBy) : undefined;
|
|
234
|
+
if (t) {
|
|
235
|
+
const why = parkedWhy(tree, t);
|
|
236
|
+
out.push(ui(lang, "ui.review.covered", { name, task: t.id, state: ui(lang, `ui.review.state.${t.state}`) + (why ? `: ${why}` : ""), what: t.what }));
|
|
237
|
+
if (t.state === "parked")
|
|
238
|
+
out.push(ui(lang, human ? "ui.review.wake.human" : "ui.review.wake", { task: t.id }));
|
|
239
|
+
}
|
|
240
|
+
else if (item.exhausted?.length)
|
|
241
|
+
out.push(ui(lang, "ui.review.exhausted", { name, tasks: item.exhausted.join(", ") }));
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
package/dist/commands/session.js
CHANGED
|
@@ -25,7 +25,7 @@ import { syncAgentFiles } from "../agents/files.js";
|
|
|
25
25
|
import { setTreeSetting } from "./setup.js";
|
|
26
26
|
import { RUNNERS } from "../runners/index.js";
|
|
27
27
|
import { PROFILES } from "../agents/profiles.js";
|
|
28
|
-
import { AGENTS, detectAgent, isAgent, which } from "../core/which.js";
|
|
28
|
+
import { AGENTS, detectAgent, isAgent, which, withoutAgentMarks } from "../core/which.js";
|
|
29
29
|
import { askGate, ensureGatesDir, loadGate } from "../core/gate.js";
|
|
30
30
|
import { keepAwake } from "../core/awake.js";
|
|
31
31
|
import { deadlineOf, WRAP_UP_MS } from "../core/clock.js";
|
|
@@ -37,6 +37,7 @@ import { guard } from "../core/guard.js";
|
|
|
37
37
|
import { hasErrors } from "../core/check.js";
|
|
38
38
|
import { Tree } from "../core/tree.js";
|
|
39
39
|
import { treeBrowserConnectors } from "../core/connector.js";
|
|
40
|
+
import { browserNote } from "./connectors.js";
|
|
40
41
|
import { reviewProposals } from "../core/review.js";
|
|
41
42
|
function written(tree) {
|
|
42
43
|
return lines(...tree.written.map((o) => o.summary), tree.dryRun ? "(dry run — nothing written)" : undefined);
|
|
@@ -384,7 +385,7 @@ register({
|
|
|
384
385
|
"the tree's permissions (anything else is denied), or with the terminal (--interactive). Stops at the\n" +
|
|
385
386
|
"subscription limit, when the queue is empty, or at --until (a session already running finishes, within\n" +
|
|
386
387
|
"--minutes). The agent knows when its session is stopped (the brief; strom's output counts down its last\n" +
|
|
387
|
-
"10 minutes, a short session its last quarter); at --minutes it is stopped, and Claude Code, Codex and
|
|
388
|
+
"10 minutes, a short session its last quarter); at --minutes it is stopped, and Claude Code, Codex, OpenCode and Grok\n" +
|
|
388
389
|
"get 5 minutes more to write down what they found and close (an unfinished task goes back to the queue); a task\n" +
|
|
389
390
|
"that comes back twice in a row with nothing recorded is parked. --task picks the tasks (one session each, in that order;\n" +
|
|
390
391
|
"one done or held by another agent meanwhile is left out). --loop: session after session for as long as there\n" +
|
|
@@ -479,6 +480,10 @@ register({
|
|
|
479
480
|
out(ui(lang, "ui.run.browser", { name: c.manifest.title ?? c.name, hosts: c.manifest.hosts.join(", ") }));
|
|
480
481
|
if (browser.length && /haiku/i.test(models.lead ?? ""))
|
|
481
482
|
out(ui(lang, "ui.run.browser.model", { model: models.lead }));
|
|
483
|
+
// Archives through the browser with an agent that has no browser tools, or without the extension: said before it starts.
|
|
484
|
+
const browserSays = browserNote(ctx, Tree.open(root, runEnv), runnerId);
|
|
485
|
+
if (browserSays)
|
|
486
|
+
out(browserSays);
|
|
482
487
|
if (permissions === "full")
|
|
483
488
|
out(ui(lang, "ui.run.full"));
|
|
484
489
|
// The gate holds a run that goes on by itself (--loop, --until). Tasks the user starts themselves (one, --max n,
|
|
@@ -573,7 +578,7 @@ register({
|
|
|
573
578
|
brief.text;
|
|
574
579
|
const kickoff = `You are the researcher in strom session ${session.id}. Run \`strom brief\` and follow it; work only through strom; finish with \`strom session close\`.`;
|
|
575
580
|
const env = {
|
|
576
|
-
...prependPath(runEnv, bin),
|
|
581
|
+
...prependPath(withoutAgentMarks(runEnv), bin),
|
|
577
582
|
STROM_SESSION: session.id,
|
|
578
583
|
STROM_DEADLINE: new Date(deadline).toISOString(),
|
|
579
584
|
STROM_MINUTES: String(minutes),
|
|
@@ -598,7 +603,9 @@ register({
|
|
|
598
603
|
shared: ctx.settings.shared()?.value,
|
|
599
604
|
...(models.lead ? { model: models.lead } : {}),
|
|
600
605
|
...(opts.interactive ? { interactive: true } : {}),
|
|
601
|
-
|
|
606
|
+
// the level for every agent (each maps it to its own switches); the browser and Remote Control are Claude Code's
|
|
607
|
+
permissions,
|
|
608
|
+
...(runnerId === "claude" ? { chrome: browser.length > 0, remote: ctx.settings.agentRemote() } : {}),
|
|
602
609
|
...(extra?.length ? { extraArgs: extra } : {}),
|
|
603
610
|
onProgress: (l) => out(` · ${l}`),
|
|
604
611
|
signal: stop.signal,
|
|
@@ -624,10 +631,11 @@ register({
|
|
|
624
631
|
const back = after.get(task.id);
|
|
625
632
|
const idle = back && back.state === "open" ? idleSessions(after, task.id) : 0;
|
|
626
633
|
if (idle >= MAX_IDLE_SESSIONS) {
|
|
627
|
-
|
|
634
|
+
const why = `${idle} sessions in a row recorded nothing for it — needs a look (strom task show ${task.id})`;
|
|
635
|
+
update(after, task.id, "task", (t) => ({ ...t, state: "parked", parkedReason: why }), {
|
|
628
636
|
op: "task.park",
|
|
629
637
|
summary: `${task.id} park: ${idle} sessions without a result`,
|
|
630
|
-
reason:
|
|
638
|
+
reason: why,
|
|
631
639
|
});
|
|
632
640
|
out(ui(lang, "ui.run.parked", { task: task.id, n: idle }));
|
|
633
641
|
}
|
|
@@ -754,7 +762,7 @@ function costText(m) {
|
|
|
754
762
|
return m?.costUsd !== undefined ? `$${m.costUsd.toFixed(2)}` : "";
|
|
755
763
|
}
|
|
756
764
|
/** How the user clears the agent's context, in the agent's own words. */
|
|
757
|
-
const CLEAR = { claude: "/clear", codex: "/new", opencode: "/new" };
|
|
765
|
+
const CLEAR = { claude: "/clear", codex: "/new", opencode: "/new", grok: "/new" };
|
|
758
766
|
/** The hint after a session in a conversation: the next task in a fresh context (images stay in a context and are paid on every turn). */
|
|
759
767
|
function freshContext(ctx, tree, s) {
|
|
760
768
|
const since = Date.parse(s.started);
|
package/dist/commands/setup.js
CHANGED
|
@@ -19,6 +19,7 @@ import { installation, stromLauncher } from "../core/self.js";
|
|
|
19
19
|
import { VERSION } from "../core/tree.js";
|
|
20
20
|
import { installUpdate, isNewer, knownNewerVersion, latestVersion, newerNode, newerVersion } from "../core/update.js";
|
|
21
21
|
import { desktopDir } from "../core/paths.js";
|
|
22
|
+
import { planMove } from "../core/relocate.js";
|
|
22
23
|
import { researchUrl, stromAppUrl, stromAppState } from "../core/stromapp.js";
|
|
23
24
|
import { isInstalled } from "../agents/global.js";
|
|
24
25
|
import { shortcutName } from "../cli/wizard.js";
|
|
@@ -31,13 +32,16 @@ import { check } from "../core/check.js";
|
|
|
31
32
|
import { assertIntact, verifyFull } from "../core/integrity.js";
|
|
32
33
|
import { ensurePluginsDir } from "../core/connector.js";
|
|
33
34
|
import { ensureGatesDir, loadGate } from "../core/gate.js";
|
|
34
|
-
import {
|
|
35
|
+
import { ensureHooksDir } from "../core/hooks.js";
|
|
36
|
+
import { claudeInChrome, CLAUDE_IN_CHROME_URL, downloadsDir } from "../core/browser.js";
|
|
37
|
+
import { browserConnectors } from "../core/connector.js";
|
|
35
38
|
export const SHARED_DIRS = ["media", "catalog", "tools", "cache", "inbox"];
|
|
36
39
|
export function ensureShared(dir) {
|
|
37
40
|
for (const d of SHARED_DIRS)
|
|
38
41
|
fs.mkdirSync(path.join(dir, d), { recursive: true });
|
|
39
42
|
ensurePluginsDir(dir);
|
|
40
43
|
ensureGatesDir(dir);
|
|
44
|
+
ensureHooksDir(dir);
|
|
41
45
|
}
|
|
42
46
|
register({
|
|
43
47
|
path: ["setup"],
|
|
@@ -69,6 +73,12 @@ register({
|
|
|
69
73
|
set: `strom setup --home "${ctx.display(suggestedHome)}" --yes`,
|
|
70
74
|
},
|
|
71
75
|
]);
|
|
76
|
+
if (cfg.home && flags.home)
|
|
77
|
+
guardResearchFolder(ctx, "home", flags.home);
|
|
78
|
+
if (flags.shared)
|
|
79
|
+
guardResearchFolder(ctx, "shared", flags.shared);
|
|
80
|
+
if (flags.trees)
|
|
81
|
+
guardResearchFolder(ctx, "trees", flags.trees);
|
|
72
82
|
let home = flags.home ? ctx.resolvePath(flags.home) : suggestedHome;
|
|
73
83
|
let shared = flags.shared ? ctx.resolvePath(flags.shared) : undefined;
|
|
74
84
|
let trees = flags.trees ? ctx.resolvePath(flags.trees) : undefined;
|
|
@@ -166,7 +176,11 @@ function diagnose(ctx) {
|
|
|
166
176
|
const found = here.map((a) => a.id);
|
|
167
177
|
const tree = ctx.hasTree() ? ctx.tree().config : undefined;
|
|
168
178
|
const chosen = ctx.settings.agent(tree).value;
|
|
169
|
-
|
|
179
|
+
// None strom knows, and no person at a terminal: another agent or program runs strom (a bot on its own
|
|
180
|
+
// server, found live: Grok Bot) — it does the research through strom, nothing is missing for it.
|
|
181
|
+
if (!found.length && (isAgent(ctx.env) || !ctx.io.tty))
|
|
182
|
+
add("agent", "ok", t("ui.doc.agent.other"));
|
|
183
|
+
else if (!found.length)
|
|
170
184
|
add("agent", "fail", t("ui.doc.noagent"), `${FIX} (${researchUrl(lang, "agents")})`, "agent");
|
|
171
185
|
else {
|
|
172
186
|
const names = found.map((id) => `${PROFILES[id]?.name ?? id}${id === chosen ? ` (${t("ui.doc.default")})` : ""}`).join(", ");
|
|
@@ -189,6 +203,19 @@ function diagnose(ctx) {
|
|
|
189
203
|
if (model)
|
|
190
204
|
add("model", "ok", model);
|
|
191
205
|
}
|
|
206
|
+
// Archives through the browser: only Claude Code has browser tools, and they work through the Claude in Chrome extension.
|
|
207
|
+
const shared = ctx.settings.shared()?.value;
|
|
208
|
+
const viaBrowser = shared ? browserConnectors(ctx.env, shared) : [];
|
|
209
|
+
if (viaBrowser.length) {
|
|
210
|
+
const names = viaBrowser.map((c) => c.manifest.title ?? c.name).join(", ");
|
|
211
|
+
const ext = claudeInChrome(ctx.env).extension;
|
|
212
|
+
if (chosen !== "claude")
|
|
213
|
+
add("browser", "warn", t("ui.doc.browser.agent", { names, agent: PROFILES[chosen]?.name ?? chosen }), found.includes("claude") ? "strom agents use claude" : undefined);
|
|
214
|
+
else if (ext.length)
|
|
215
|
+
add("browser", "ok", t("ui.doc.browser.ok", { browsers: ext.join(", ") }));
|
|
216
|
+
else
|
|
217
|
+
add("browser", "warn", t("ui.doc.browser.noext", { names }), CLAUDE_IN_CHROME_URL);
|
|
218
|
+
}
|
|
192
219
|
// For a person: the shortcut on the desktop, the Strom app.
|
|
193
220
|
const desktop = desktopDir(ctx.env);
|
|
194
221
|
const shortcut = [`${shortcutName(lang)}.command`, `${shortcutName(lang)}.lnk`, `${shortcutName(lang)}.cmd`, "strom-research.desktop"].some((f) => fs.existsSync(path.join(desktop, f)));
|
|
@@ -391,11 +418,33 @@ function raises(now, value) {
|
|
|
391
418
|
return PERMISSION_LEVELS.indexOf(next) > PERMISSION_LEVELS.indexOf(now);
|
|
392
419
|
}
|
|
393
420
|
/** Change a user setting (the config file on this computer). */
|
|
421
|
+
/**
|
|
422
|
+
* Where the research lives (home, trees, shared) changes only with the research moving along — which the person does in
|
|
423
|
+
* the wizard (strom setup in a terminal, the menu's settings). A folder set to another place with the research left in
|
|
424
|
+
* the old one would lose the trees from the list and the plugins and images from sight.
|
|
425
|
+
*/
|
|
426
|
+
function guardResearchFolder(ctx, key, next) {
|
|
427
|
+
const now = key === "home" ? ctx.settings.home()?.value : key === "trees" ? ctx.settings.trees()?.value : key === "shared" ? ctx.settings.shared()?.value : undefined;
|
|
428
|
+
// unset: back to the default under home (none for home itself)
|
|
429
|
+
const home = ctx.settings.home()?.value;
|
|
430
|
+
const target = next !== undefined ? ctx.resolvePath(next) : key === "shared" && home ? path.join(home, "shared") : key === "trees" ? home : undefined;
|
|
431
|
+
if (!now || (target && path.resolve(target) === path.resolve(now)))
|
|
432
|
+
return;
|
|
433
|
+
if (!planMove(now, target ?? now, []).content)
|
|
434
|
+
return;
|
|
435
|
+
throw new UsageError(`the research is in ${ctx.display(now)} — changing ${key} would leave it behind`, {
|
|
436
|
+
hint: "the user moves it: strom setup in their terminal (or the menu: Settings → the setup), which moves the trees and the shared folder along",
|
|
437
|
+
});
|
|
438
|
+
}
|
|
394
439
|
function setUserSetting(ctx, key, value) {
|
|
395
440
|
const s = ctx.settings;
|
|
441
|
+
guardResearchFolder(ctx, key, value === undefined ? undefined : String(value));
|
|
396
442
|
// Loosening the agent's permissions is the user's decision alone.
|
|
397
443
|
if (key === "agent.permissions" && raises(s.agentPermissions(), value))
|
|
398
444
|
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"));
|
|
445
|
+
// Sessions steered from elsewhere (Remote Control): the user's decision alone.
|
|
446
|
+
if (key === "agent.remote" && value === "on" && !s.agentRemote())
|
|
447
|
+
ctx.requireHuman("Start the Claude Code sessions with Remote Control (followed and steered from claude.ai or your phone)?", "strom config set agent.remote on", "agent.remote", ui(ctx.uiLang(), "ui.consent.remote"));
|
|
399
448
|
// Asking before a connector runs is the user's safeguard: only they take it away.
|
|
400
449
|
if (key === "connectors.consent" && value !== "on" && s.connectorsConsent())
|
|
401
450
|
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"));
|