strom-research 1.7.0 → 1.8.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 +3 -0
- package/assets/lang/cs.json +117 -2
- package/assets/lang/de.json +117 -2
- package/assets/method/narrate.md +5 -1
- package/assets/plugins/gates/claude-usage/gate.ts +2 -1
- package/dist/agents/files.js +6 -4
- package/dist/cli/main.js +21 -3
- package/dist/cli/menu-links.js +35 -1
- package/dist/cli/menu-research.js +41 -2
- package/dist/cli/menu-settings.js +17 -1
- package/dist/cli/menu.js +19 -8
- package/dist/cli/ui.js +57 -1
- package/dist/commands/guide.js +6 -1
- package/dist/commands/links.js +39 -3
- package/dist/commands/pack.js +21 -18
- package/dist/commands/research.js +42 -1
- package/dist/commands/session.js +79 -9
- package/dist/commands/setup.js +3 -0
- package/dist/commands/start.js +6 -3
- package/dist/commands/story.js +47 -6
- package/dist/commands/tasks.js +15 -6
- package/dist/commands/trees.js +82 -2
- package/dist/core/changelog.js +223 -0
- package/dist/core/clock.js +20 -0
- package/dist/core/config.js +2 -0
- package/dist/core/connector.js +25 -1
- package/dist/core/directions.js +41 -0
- package/dist/core/links.js +17 -4
- package/dist/core/live.js +393 -50
- package/dist/core/pack.js +31 -0
- package/dist/core/phrases.js +61 -0
- package/dist/core/queue.js +7 -2
- package/dist/core/relocate.js +4 -4
- package/dist/core/session.js +8 -2
- package/dist/core/trash.js +72 -0
- package/dist/core/tree.js +1 -1
- package/dist/core/workers.js +28 -3
- package/dist/runners/claude.js +2 -1
- package/dist/runners/jsonl.js +2 -2
- package/dist/runners/runner.js +16 -3
- package/dist/runners/script.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// What one commit of a research saved, as a person reads it — in the research
|
|
2
|
+
// language, records by their names: "New person: Jan Novák (*1905) [P0001]",
|
|
3
|
+
// "Downloaded 7 images: Kniha N 1850 (images 16–22)". Made from the operations
|
|
4
|
+
// the commit logged (data/ops/*.jsonl, each line one operation); a commit
|
|
5
|
+
// without them (the readers' notes) by its subject. For the Strom app (the
|
|
6
|
+
// bridge's /log and change events): strom's own commit subjects are English
|
|
7
|
+
// and name records by their IDs.
|
|
8
|
+
import { displayName, lifespan } from "./people.js";
|
|
9
|
+
import { ownerName, cardFact } from "./overview.js";
|
|
10
|
+
import { phrase } from "./phrases.js";
|
|
11
|
+
import { eventName, humanDate, humanPlace, humanTask } from "../cli/human.js";
|
|
12
|
+
/** Operations nobody needs to read: what another line says already, or bookkeeping. */
|
|
13
|
+
const QUIET = new Set(["session.metrics", "task.start", "task.continue", "session.note"]);
|
|
14
|
+
/** The one-line form of an operation, by its kind: {who} a person or family, {name} the record. */
|
|
15
|
+
const SAID = {
|
|
16
|
+
"person.add": "log.person.add",
|
|
17
|
+
"person.name": "log.person.name",
|
|
18
|
+
"person.edit": "log.person.edit",
|
|
19
|
+
"person.cite": "log.person.cite",
|
|
20
|
+
"person.retract": "log.person.retract",
|
|
21
|
+
"family.add": "log.family.add",
|
|
22
|
+
"family.edit": "log.family.edit",
|
|
23
|
+
"family.cite": "log.family.cite",
|
|
24
|
+
"source.add": "log.source.add",
|
|
25
|
+
"source.edit": "log.source.edit",
|
|
26
|
+
"recordset.add": "log.recordset.add",
|
|
27
|
+
"recordset.edit": "log.recordset.edit",
|
|
28
|
+
"recordset.calibrate": "log.recordset.calibrate",
|
|
29
|
+
"task.add": "log.task.add",
|
|
30
|
+
"task.done": "log.task.done",
|
|
31
|
+
"task.park": "log.task.park",
|
|
32
|
+
"task.drop": "log.task.drop",
|
|
33
|
+
"task.wait": "log.task.wait",
|
|
34
|
+
"task.wake": "log.task.wake",
|
|
35
|
+
"task.edit": "log.task.edit",
|
|
36
|
+
"session.start": "log.session.start",
|
|
37
|
+
"session.close": "log.session.close",
|
|
38
|
+
"lesson.add": "log.lesson.add",
|
|
39
|
+
"hypothesis.add": "log.hypothesis.add",
|
|
40
|
+
"hypothesis.argue": "log.hypothesis.argue",
|
|
41
|
+
"hypothesis.decide": "log.hypothesis.decide",
|
|
42
|
+
"conflict.add": "log.conflict.add",
|
|
43
|
+
"conflict.resolve": "log.conflict.resolve",
|
|
44
|
+
"place.add": "log.place.add",
|
|
45
|
+
"place.edit": "log.place.edit",
|
|
46
|
+
"place.jurisdiction": "log.place.edit",
|
|
47
|
+
"repository.add": "log.repository.add",
|
|
48
|
+
"repository.edit": "log.repository.edit",
|
|
49
|
+
"story.set": "log.story.set",
|
|
50
|
+
"story.approve": "log.story.approve",
|
|
51
|
+
"note.add": "log.note.add",
|
|
52
|
+
"input.add": "log.input.add",
|
|
53
|
+
"input.sync": "log.input.sync",
|
|
54
|
+
"input.done": "log.input.done",
|
|
55
|
+
"research.add": "log.research.add",
|
|
56
|
+
"research.edit": "log.research.edit",
|
|
57
|
+
"research.pause": "log.research.pause",
|
|
58
|
+
"research.resume": "log.research.resume",
|
|
59
|
+
"research.done": "log.research.done",
|
|
60
|
+
};
|
|
61
|
+
const EVENT_SAID = {
|
|
62
|
+
"event.add": "log.event.add",
|
|
63
|
+
"event.edit": "log.event.edit",
|
|
64
|
+
"event.cite": "log.event.cite",
|
|
65
|
+
"event.retract": "log.event.retract",
|
|
66
|
+
};
|
|
67
|
+
const SEARCH_SAID = {
|
|
68
|
+
found: "log.search.found",
|
|
69
|
+
negative: "log.search.negative",
|
|
70
|
+
partial: "log.search.partial",
|
|
71
|
+
inconclusive: "log.search.inconclusive",
|
|
72
|
+
};
|
|
73
|
+
function short(text, max = 90) {
|
|
74
|
+
const one = text.replace(/\s+/g, " ").trim();
|
|
75
|
+
return one.length > max ? `${one.slice(0, max - 1).trimEnd()}…` : one;
|
|
76
|
+
}
|
|
77
|
+
/** Image numbers as ranges: 16–22, 30. */
|
|
78
|
+
function ranges(ns) {
|
|
79
|
+
const sorted = [...new Set(ns)].sort((a, b) => a - b);
|
|
80
|
+
const out = [];
|
|
81
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
82
|
+
let j = i;
|
|
83
|
+
while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1)
|
|
84
|
+
j++;
|
|
85
|
+
out.push(j > i ? `${sorted[i]}–${sorted[j]}` : String(sorted[i]));
|
|
86
|
+
i = j;
|
|
87
|
+
}
|
|
88
|
+
return out.join(", ");
|
|
89
|
+
}
|
|
90
|
+
function kindOf(op) {
|
|
91
|
+
const [record] = op.split(".");
|
|
92
|
+
if (record === "person" || record === "family" || record === "event")
|
|
93
|
+
return "person";
|
|
94
|
+
if (record === "source" || record === "media" || record === "recordset" || record === "repository")
|
|
95
|
+
return "source";
|
|
96
|
+
if (record === "story")
|
|
97
|
+
return "story";
|
|
98
|
+
return "other";
|
|
99
|
+
}
|
|
100
|
+
/** Lines of what a commit saved, in the research language, each with what it is about; its subject when it logged no operation. */
|
|
101
|
+
export function changeLines(tree, ops, subject, lang) {
|
|
102
|
+
const get = (id) => (id ? tree.get(id) : undefined);
|
|
103
|
+
const who = (id) => {
|
|
104
|
+
const r = get(id);
|
|
105
|
+
if (!r)
|
|
106
|
+
return id ?? "";
|
|
107
|
+
if (r.type === "person") {
|
|
108
|
+
const span = lifespan(r);
|
|
109
|
+
return `${displayName(r)}${span ? ` (${span})` : ""} [${r.id}]`;
|
|
110
|
+
}
|
|
111
|
+
return r.type === "family" ? ownerName(tree, r.id) : r.id;
|
|
112
|
+
};
|
|
113
|
+
const task = (id) => {
|
|
114
|
+
const t = get(id);
|
|
115
|
+
return t ? short(humanTask(tree, t.what, lang)) : (id ?? "");
|
|
116
|
+
};
|
|
117
|
+
const name = (id) => {
|
|
118
|
+
const r = get(id);
|
|
119
|
+
if (!r)
|
|
120
|
+
return id ?? "";
|
|
121
|
+
switch (r.type) {
|
|
122
|
+
case "person":
|
|
123
|
+
case "family":
|
|
124
|
+
return who(r.id);
|
|
125
|
+
case "task":
|
|
126
|
+
return task(r.id);
|
|
127
|
+
case "session":
|
|
128
|
+
return r.task ? task(r.task) : r.id;
|
|
129
|
+
case "source":
|
|
130
|
+
case "recordset":
|
|
131
|
+
case "conflict":
|
|
132
|
+
return short(r.title);
|
|
133
|
+
case "research":
|
|
134
|
+
case "repository":
|
|
135
|
+
case "input":
|
|
136
|
+
return short(r.name);
|
|
137
|
+
case "search":
|
|
138
|
+
return short(r.question);
|
|
139
|
+
case "lesson":
|
|
140
|
+
return short(r.rule);
|
|
141
|
+
case "hypothesis":
|
|
142
|
+
return short(r.question);
|
|
143
|
+
case "place":
|
|
144
|
+
return r.names[0]?.name ?? r.id;
|
|
145
|
+
default:
|
|
146
|
+
return r.id;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const fact = (owner, eventId) => {
|
|
150
|
+
const e = get(owner)?.events.find((x) => x.id === eventId);
|
|
151
|
+
if (!e)
|
|
152
|
+
return undefined;
|
|
153
|
+
const f = cardFact(tree, e);
|
|
154
|
+
const what = [f.value, humanDate(f.date, lang), humanPlace(f.place, f.house, lang)].filter(Boolean).join(", ");
|
|
155
|
+
return `${eventName(f.kind, lang, f.label)}${what ? `: ${what}` : ""}`;
|
|
156
|
+
};
|
|
157
|
+
const out = [];
|
|
158
|
+
let kind = "other";
|
|
159
|
+
const say = (line) => {
|
|
160
|
+
if (line && !out.some((l) => l.text === line))
|
|
161
|
+
out.push({ text: line, kind });
|
|
162
|
+
};
|
|
163
|
+
// Images downloaded: one line for each book, whatever their order.
|
|
164
|
+
const images = new Map();
|
|
165
|
+
for (const o of ops) {
|
|
166
|
+
if (QUIET.has(o.op))
|
|
167
|
+
continue;
|
|
168
|
+
kind = kindOf(o.op);
|
|
169
|
+
const [first, second] = o.targets;
|
|
170
|
+
if (o.op === "media.add") {
|
|
171
|
+
const m = get(first);
|
|
172
|
+
const key = m?.recordset ?? "";
|
|
173
|
+
if (!images.has(key)) {
|
|
174
|
+
images.set(key, []);
|
|
175
|
+
out.push({ text: `\u0000media:${key}`, kind: "source" }); // its place in the order, filled in below
|
|
176
|
+
}
|
|
177
|
+
if (m?.image !== undefined)
|
|
178
|
+
images.get(key).push(m.image);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (EVENT_SAID[o.op]) {
|
|
182
|
+
const f = fact(first, second);
|
|
183
|
+
say(f ? phrase(lang, EVENT_SAID[o.op], { who: who(first), fact: f }) : o.summary);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (o.op === "search.add" || o.op === "search.edit") {
|
|
187
|
+
const s = get(first);
|
|
188
|
+
say(s ? phrase(lang, SEARCH_SAID[s.result] ?? "log.search.found", { name: short(s.question) }) : o.summary);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (o.op === "family.child") {
|
|
192
|
+
say(phrase(lang, "log.family.child", { who: who(first), child: who(second) }));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (o.op === "person.merge" || o.op === "family.merge") {
|
|
196
|
+
say(phrase(lang, "log.merge", { who: who(second) }));
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const key = SAID[o.op];
|
|
200
|
+
say(key ? phrase(lang, key, { who: who(first), name: name(first) }) : o.summary);
|
|
201
|
+
}
|
|
202
|
+
const lines = out.map(({ text: line, kind }) => {
|
|
203
|
+
if (!line.startsWith("\u0000media:"))
|
|
204
|
+
return { text: line, kind };
|
|
205
|
+
const book = line.slice("\u0000media:".length);
|
|
206
|
+
const ns = images.get(book) ?? [];
|
|
207
|
+
const title = get(book)?.title;
|
|
208
|
+
const n = Math.max(ns.length, 1);
|
|
209
|
+
const text = !title
|
|
210
|
+
? phrase(lang, "log.media.loose", { n })
|
|
211
|
+
: ns.length === 1
|
|
212
|
+
? phrase(lang, "log.media.one", { name: title, image: ns[0] })
|
|
213
|
+
: phrase(lang, "log.media", { name: title, n, images: ns.length ? ranges(ns) : "" });
|
|
214
|
+
return { text, kind };
|
|
215
|
+
});
|
|
216
|
+
if (lines.length)
|
|
217
|
+
return lines;
|
|
218
|
+
// no operations: the readers' notes of a book, else the subject as strom wrote it
|
|
219
|
+
const reading = /^Reading (B\d+)-(\d+)-(\d+): (\d+) images?/.exec(subject);
|
|
220
|
+
if (reading)
|
|
221
|
+
return [{ text: phrase(lang, "log.reading", { name: get(reading[1])?.title ?? reading[1], n: reading[4], images: reading[2] === reading[3] ? reading[2] : `${reading[2]}–${reading[3]}` }), kind: "source" }];
|
|
222
|
+
return ops.length ? [] : [{ text: subject, kind: "other" }];
|
|
223
|
+
}
|
package/dist/core/clock.js
CHANGED
|
@@ -5,12 +5,32 @@
|
|
|
5
5
|
// brief says it), near the end every strom command it runs reminds it, and at
|
|
6
6
|
// the limit an agent that can be resumed gets a few minutes more to write down
|
|
7
7
|
// what it found and close the session (runners: wrapUp).
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
8
10
|
/** From how long before the end strom's output reminds the agent (a short session: its last quarter). */
|
|
9
11
|
export const REMIND_MS = 10 * 60_000;
|
|
10
12
|
/** From how long before the end the reminder says to write down and close (a short session: its last eighth). */
|
|
11
13
|
export const CLOSE_MS = 3 * 60_000;
|
|
12
14
|
/** The time an agent stopped at its limit gets to write down what it found. */
|
|
13
15
|
export const WRAP_UP_MS = 5 * 60_000;
|
|
16
|
+
/**
|
|
17
|
+
* The user asked a session to finish now (strom session finish): a file of the tree's own, read by every strom
|
|
18
|
+
* command the session's agent runs — a run's session cannot be told otherwise (it reads no input).
|
|
19
|
+
*/
|
|
20
|
+
export function finishFile(root, session) {
|
|
21
|
+
return path.join(root, ".strom", "finish", `${session}.json`);
|
|
22
|
+
}
|
|
23
|
+
export function askFinish(root, session, by) {
|
|
24
|
+
fs.mkdirSync(path.dirname(finishFile(root, session)), { recursive: true });
|
|
25
|
+
fs.writeFileSync(finishFile(root, session), JSON.stringify({ at: new Date().toISOString(), by }) + "\n");
|
|
26
|
+
}
|
|
27
|
+
export function finishAsked(root, session) {
|
|
28
|
+
return fs.existsSync(finishFile(root, session));
|
|
29
|
+
}
|
|
30
|
+
/** What every strom command says to the agent of a session the user asked to finish. */
|
|
31
|
+
export const FINISH_LINE = "⏳ the user asks you to finish this session now: start nothing new; record in strom what you found and have not recorded yet " +
|
|
32
|
+
'(facts, sources, the images searched, in vain too), then strom session close --continue --summary "…" --next "exactly where you stopped" ' +
|
|
33
|
+
"(or finish the task, if it is done).";
|
|
14
34
|
/** When this session is stopped (ms since the epoch), if a run set a limit. */
|
|
15
35
|
export function deadlineOf(env) {
|
|
16
36
|
const t = Date.parse(env.STROM_DEADLINE ?? "");
|
package/dist/core/config.js
CHANGED
|
@@ -55,6 +55,7 @@ export const SETTINGS = [
|
|
|
55
55
|
// Read from the config file only — no variable, no flag, no tree: nothing an agent can set.
|
|
56
56
|
{ key: "agent.permissions", env: "", tree: false, kind: "choice", choices: PERMISSION_LEVELS, description: "what the agent may do without asking you: ask (what the tree allows; anything else it asks), auto (default: what the tree allows; anything else the agent's own review decides, asking only when risky), full (everything but what the tree denies) — only you raise it" },
|
|
57
57
|
{ key: "agent.where", env: "STROM_AGENT_WHERE", tree: false, kind: "choice", choices: ["app", "terminal"], description: "where you talk with the agent: app (its desktop app — the easiest), terminal (its CLI) — unset: the app when it is installed" },
|
|
58
|
+
{ key: "agent.browser", env: "", tree: true, kind: "choice", choices: ["archives", "always"], description: "browser tools (Claude in Chrome) for the Claude Code sessions strom starts: archives (default) — only in a research whose connectors fetch through the browser, for their sites; always — in every session, for the sites of the research's archives (a conversation asks about others) — only you turn it on" },
|
|
58
59
|
{ key: "agent.remote", env: "", tree: false, kind: "choice", choices: ["on", "off"], description: "the Claude Code sessions strom starts (strom run, strom chat in the terminal) with Remote Control: on — follow and steer them from claude.ai or the Claude app on your phone; off (default) — only you turn it on" },
|
|
59
60
|
{ key: "updates", env: "STROM_UPDATES", tree: false, kind: "choice", choices: ["check", "off"], description: "look for new versions of strom: check (default — at most once a day, one small file from the project's releases; strom says so, strom update installs it) or off" },
|
|
60
61
|
{ key: "strom.app", env: "", tree: false, kind: "choice", choices: ["yes", "no"], description: "you use the Strom app: yes (strom says which file to import into it), no (strom never mentions it) — unset: strom notices it itself" },
|
|
@@ -72,6 +73,7 @@ const FIELDS = {
|
|
|
72
73
|
"agent.permissions": "agentPermissions",
|
|
73
74
|
"agent.where": "agentWhere",
|
|
74
75
|
"agent.remote": "agentRemote",
|
|
76
|
+
"agent.browser": "agentBrowser",
|
|
75
77
|
"strom.app": "stromApp",
|
|
76
78
|
"strom.app.url": "stromAppUrl",
|
|
77
79
|
"main.person": "mainPerson",
|
package/dist/core/connector.js
CHANGED
|
@@ -236,6 +236,11 @@ export function treeBrowserConnectors(tree, shared) {
|
|
|
236
236
|
if (!all.length)
|
|
237
237
|
return [];
|
|
238
238
|
const fetchedBy = new Set(tree.list("media").flatMap((m) => (m.fetched ? [m.fetched.connector] : [])));
|
|
239
|
+
const hosts = new Set(treeSites(tree));
|
|
240
|
+
return all.filter((c) => fetchedBy.has(c.name) || c.manifest.hosts.some((h) => hosts.has(siteOf(h))));
|
|
241
|
+
}
|
|
242
|
+
/** The sites of the archives a tree's records name (repositories, record sets, sources with an address). */
|
|
243
|
+
function treeSites(tree) {
|
|
239
244
|
const hosts = new Set();
|
|
240
245
|
for (const r of [...tree.list("repository"), ...tree.list("recordset"), ...tree.list("source")])
|
|
241
246
|
if (r.url && !r.retracted)
|
|
@@ -245,7 +250,26 @@ export function treeBrowserConnectors(tree, shared) {
|
|
|
245
250
|
catch {
|
|
246
251
|
// not an address
|
|
247
252
|
}
|
|
248
|
-
return
|
|
253
|
+
return [...hosts];
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Browser tools for the tree's agent (Claude in Chrome): whether its sessions get them, and for which sites.
|
|
257
|
+
* `agent.browser` archives (default): only where a connector of the tree fetches through the browser, for its sites;
|
|
258
|
+
* always (the user's choice): every session, for the sites of the tree's archives and of the connectors — a
|
|
259
|
+
* conversation asks about any other, a run nobody watches uses none other.
|
|
260
|
+
*/
|
|
261
|
+
export function agentBrowser(tree, shared) {
|
|
262
|
+
const via = treeBrowserConnectors(tree, shared);
|
|
263
|
+
const always = new Settings(tree.env, {}).resolve("agent.browser", tree.config)?.value === "always";
|
|
264
|
+
const hosts = new Set(via.flatMap((c) => c.manifest.hosts));
|
|
265
|
+
if (always) {
|
|
266
|
+
for (const c of shared ? scanConnectors(shared).connectors : [])
|
|
267
|
+
for (const h of c.manifest.hosts)
|
|
268
|
+
hosts.add(h);
|
|
269
|
+
for (const h of treeSites(tree))
|
|
270
|
+
hosts.add(h);
|
|
271
|
+
}
|
|
272
|
+
return { on: always || via.length > 0, hosts: [...hosts], always };
|
|
249
273
|
}
|
|
250
274
|
export function findConnector(shared, name) {
|
|
251
275
|
if (!shared)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Which direction of a research (G…) a task belongs to. A task names its
|
|
2
|
+
// research when it was added in a session of one (or named it); a task added
|
|
3
|
+
// elsewhere belongs to the direction its people are in — the narrowest one when
|
|
4
|
+
// several reach them (the ancestors of a grandmother before those of her
|
|
5
|
+
// grandson). The queue, the menu and the Strom app go by the same answer.
|
|
6
|
+
import { researchPeople } from "./frontier.js";
|
|
7
|
+
import { subjectPeople } from "./records.js";
|
|
8
|
+
import { typeOfId } from "./tree.js";
|
|
9
|
+
export function scopes(tree) {
|
|
10
|
+
return tree.list("research").map((research) => ({ research, people: researchPeople(tree, research) }));
|
|
11
|
+
}
|
|
12
|
+
/** The people a task is about: its persons, the partners of its families, the people of its conflicts and hypotheses. */
|
|
13
|
+
export function aboutPeople(tree, subject) {
|
|
14
|
+
const partners = subject.filter((id) => typeOfId(id) === "family").flatMap((id) => tree.get(id)?.partners ?? []);
|
|
15
|
+
return [...new Set([...subjectPeople(tree, subject), ...partners])];
|
|
16
|
+
}
|
|
17
|
+
/** The direction a task belongs to: its own, else the narrowest one its people are in; none for a task about nobody. */
|
|
18
|
+
export function directionOf(tree, t, all) {
|
|
19
|
+
if (t.research)
|
|
20
|
+
return t.research;
|
|
21
|
+
const about = aboutPeople(tree, t.subject);
|
|
22
|
+
if (!about.length)
|
|
23
|
+
return undefined;
|
|
24
|
+
return all
|
|
25
|
+
.filter((s) => about.some((p) => s.people.has(p)))
|
|
26
|
+
.sort((a, b) => a.people.size - b.people.size || a.research.id.localeCompare(b.research.id))[0]?.research.id;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether a task is work of a research (strom session start --research, strom
|
|
30
|
+
* run --research): its own, or about the people it is about (a task added
|
|
31
|
+
* elsewhere or before it began); a task about nobody and of no research, any
|
|
32
|
+
* research's.
|
|
33
|
+
*/
|
|
34
|
+
export function ofResearch(tree, t, scope) {
|
|
35
|
+
if (t.research === scope.research.id)
|
|
36
|
+
return true;
|
|
37
|
+
const about = aboutPeople(tree, t.subject);
|
|
38
|
+
if (!about.length)
|
|
39
|
+
return !t.research;
|
|
40
|
+
return about.some((p) => scope.people.has(p)) || (!t.research && !scope.people.size);
|
|
41
|
+
}
|
package/dist/core/links.js
CHANGED
|
@@ -29,15 +29,19 @@ export const LINK_SCHEME = "strom-research";
|
|
|
29
29
|
* as it is now into the app's window, open the research's menu, a conversation with the agent (of one person),
|
|
30
30
|
* a task (answered, put aside, given up, back in the queue), a person's review, a new direction from a person —
|
|
31
31
|
* and (the app's second wave) a new research with a tree of the app, strom's update, the agent's sessions and
|
|
32
|
-
* what they cost, a conflict decided (by the user or the agent), a story approved, a sync taken back, the setup
|
|
32
|
+
* what they cost, a conflict decided (by the user or the agent), a story approved, a sync taken back, the setup —
|
|
33
|
+
* and (the app's 3.5.0) the research followed live in the app's window; a direction of the research paused, ended or
|
|
34
|
+
* taken up again, a conversation about one direction, a session at work asked to finish.
|
|
33
35
|
*/
|
|
34
|
-
export const LINK_ACTIONS = ["send", "excerpt", "app", "open", "chat", "task", "review", "research", "new", "update", "sessions", "conflict", "story", "sync-undo", "setup"];
|
|
36
|
+
export const LINK_ACTIONS = ["send", "excerpt", "app", "open", "chat", "task", "review", "research", "new", "update", "sessions", "conflict", "story", "sync-undo", "setup", "live", "direction", "finish"];
|
|
35
37
|
/** A person's review: the person, with the family, with the ancestors (strom review --scope). */
|
|
36
38
|
export const LINK_SCOPES = ["person", "family", "line"];
|
|
37
39
|
/** A new direction from a person (strom research new --direction). */
|
|
38
40
|
export const LINK_DIRECTIONS = ["ancestors", "descendants"];
|
|
39
41
|
/** What a link does with a task: put it aside, give it up, back into the queue (none: answer it). */
|
|
40
42
|
export const LINK_TASK_DOS = ["park", "drop", "wake"];
|
|
43
|
+
/** What a link does with a direction of the research (strom research pause|done|resume). */
|
|
44
|
+
export const LINK_DIRECTION_DOS = ["pause", "done", "resume"];
|
|
41
45
|
/** What a link does with a conflict: the user decides it, or leaves it to the agent. */
|
|
42
46
|
export const LINK_CONFLICT_DOS = ["decide", "agent"];
|
|
43
47
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -46,6 +50,8 @@ const PERSON = /^P\d{1,9}$/;
|
|
|
46
50
|
const TASK = /^T\d{1,9}$/;
|
|
47
51
|
const CONFLICT = /^X\d{1,9}$/;
|
|
48
52
|
const INPUT = /^I\d{1,9}$/;
|
|
53
|
+
const RESEARCH = /^G\d{1,9}$/;
|
|
54
|
+
const SESSION = /^N\d{1,9}$/;
|
|
49
55
|
/** The app's own mark of a tree it hands over (32 random bytes, base64url). */
|
|
50
56
|
export const APP_TOKEN = /^[A-Za-z0-9_-]{22,43}$/;
|
|
51
57
|
/** An excerpt's mark (_STROM_CLIP): letters, digits and "-", at most 32. */
|
|
@@ -103,6 +109,7 @@ export function parseLink(text) {
|
|
|
103
109
|
switch (action) {
|
|
104
110
|
case "send":
|
|
105
111
|
case "app":
|
|
112
|
+
case "live":
|
|
106
113
|
case "open":
|
|
107
114
|
case "update":
|
|
108
115
|
case "sessions":
|
|
@@ -115,8 +122,10 @@ export function parseLink(text) {
|
|
|
115
122
|
throw new LinkError("no excerpt named");
|
|
116
123
|
return { action, tree: id, source, clip };
|
|
117
124
|
}
|
|
118
|
-
case "chat":
|
|
119
|
-
|
|
125
|
+
case "chat": {
|
|
126
|
+
const research = idOf(q, "research", RESEARCH, false);
|
|
127
|
+
return { action, tree: id, ...(person ? { person } : {}), ...(research ? { research } : {}) };
|
|
128
|
+
}
|
|
120
129
|
case "task": {
|
|
121
130
|
const task = idOf(q, "task", TASK, true);
|
|
122
131
|
return { action, tree: id, task, ...(q.has("do") ? { do: oneOf(q, "do", LINK_TASK_DOS) } : {}) };
|
|
@@ -136,6 +145,10 @@ export function parseLink(text) {
|
|
|
136
145
|
return { action, tree: id, id: idOf(q, "id", CONFLICT, true), do: oneOf(q, "do", LINK_CONFLICT_DOS, "decide") };
|
|
137
146
|
case "sync-undo":
|
|
138
147
|
return { action, tree: id, intake: idOf(q, "intake", INPUT, true) };
|
|
148
|
+
case "direction":
|
|
149
|
+
return { action, tree: id, id: idOf(q, "id", RESEARCH, true), do: oneOf(q, "do", LINK_DIRECTION_DOS) };
|
|
150
|
+
case "finish":
|
|
151
|
+
return { action, tree: id, session: idOf(q, "session", SESSION, true) };
|
|
139
152
|
}
|
|
140
153
|
throw new LinkError(`unknown action "${action.slice(0, 32)}"`);
|
|
141
154
|
}
|