strom-research 1.25.1 → 1.25.2

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.
@@ -1,6 +1,6 @@
1
1
  // source · cite · repo · recordset · place — where the evidence comes from.
2
2
  import { OFF_MAP_HOW, offMapLine, placesOffMap } from "../core/places.js";
3
- import { addPlaceName, otherNames, personsHand } from "../core/placeid.js";
3
+ import { addPlaceName, nameWithSyntax, otherNames, parseAltName, personsHand } from "../core/placeid.js";
4
4
  import { isAgent } from "../core/which.js";
5
5
  import fs from "node:fs";
6
6
  import { commands, register } from "../cli/registry.js";
@@ -73,17 +73,7 @@ function clipRefs(tree, specs, shared) {
73
73
  }
74
74
  /** "Teinitz an der Elbe@de:1850-1918" → another name of a place. */
75
75
  export function altName(a) {
76
- const m = /^(.+?)(?:@([a-z]{2,3}))?(?::(\d{3,4})?-(\d{3,4})?)?$/.exec(a);
77
- if (!m)
78
- throw new UsageError(`invalid --alt "${a}"`, { hint: 'e.g. "Teinitz an der Elbe@de:1850-1918"' });
79
- const n = { name: m[1].trim() };
80
- if (m[2])
81
- n.lang = m[2];
82
- if (m[3])
83
- n.from = Number(m[3]);
84
- if (m[4])
85
- n.to = Number(m[4]);
86
- return n;
76
+ return parseAltName(a);
87
77
  }
88
78
  /**
89
79
  * An edit: only the options given change; --note adds a note. Overwriting a
@@ -598,7 +588,9 @@ register({
598
588
  examples: ['strom place add "Týnec nad Labem" --alt "Teinitz an der Elbe@de" --kind town --lat 50.042 --lon 15.358', 'strom place add "Lhota" --kind village --unlocated "which Lhota: the record gives no parish"'],
599
589
  run(ctx, { args, opts }) {
600
590
  const tree = ctx.tree();
601
- const named = { names: [{ name: args[0].trim() }] };
591
+ // the name written with the syntax of another name ("Lhotau@de:1850"): its name, language and years, never kept whole
592
+ const first = { name: args[0].normalize("NFC").trim() };
593
+ const named = { names: [nameWithSyntax(first) ?? first] };
602
594
  for (const n of listOpt(opts.alt).map(altName))
603
595
  addPlaceName(named, n.name, "alt", n);
604
596
  const names = named.names;
@@ -42,7 +42,7 @@ import { phrase } from "../core/phrases.js";
42
42
  import { imageSettings, qualityIn, writeGedcoms, writeTreeGed } from "./output.js";
43
43
  import { shortcutName } from "../cli/wizard.js";
44
44
  import { refuseInArchive } from "../core/state.js";
45
- import { claudeStateFile } from "../core/claudedir.js";
45
+ import { claudeStateFile, saysDefaultClaudeDir } from "../core/claudedir.js";
46
46
  /** Has the user trusted this folder in Claude Code already? (Its own record, read only.) */
47
47
  function claudeTrusts(root, env) {
48
48
  const file = claudeStateFile(env);
@@ -233,7 +233,10 @@ register({
233
233
  const agentEnv = conversationEnv(agent, ctx.env);
234
234
  if (opts.print) {
235
235
  const vars = Object.entries(agentEnv).map(([k, v]) => `${k}=${shellQuote(v)}`);
236
- return { text: [...(vars.length && process.platform !== "win32" ? ["env", ...vars] : []), ...[program ?? profile.command, ...args].map(shellQuote)].join(" "), data: { command: program ?? profile.command, args, cwd: tree.root, ...(vars.length ? { env: agentEnv } : {}) } };
236
+ // Claude Code's default folder said outright: the conversation goes without it, as every `claude` of strom (forClaude)
237
+ const unset = agent === "claude" && saysDefaultClaudeDir(ctx.env) ? ["CLAUDE_CONFIG_DIR"] : [];
238
+ const envLine = (vars.length || unset.length) && process.platform !== "win32" ? ["env", ...unset.flatMap((k) => ["-u", k]), ...vars] : [];
239
+ return { text: [...envLine, ...[program ?? profile.command, ...args].map(shellQuote)].join(" "), data: { command: program ?? profile.command, args, cwd: tree.root, ...(vars.length ? { env: agentEnv } : {}), ...(unset.length ? { unset } : {}) } };
237
240
  }
238
241
  // Other agents may work in this tree too: this one gets a name of its own, is present while it
239
242
  // works, and has its own session. Sessions of conversations that ended without closing are closed.
@@ -16,7 +16,7 @@ import { decodeText, safeFolderName } from "../core/text.js";
16
16
  import { humanAge } from "../core/age.js";
17
17
  import { now, Tree } from "../core/tree.js";
18
18
  import { actsAsArchive } from "../core/state.js";
19
- import { applySync, discardReceived, nothingSince, withoutImages, planSync, readTreeFile, receivedAll, receivedPending, receivedSince, receivedOf, removedByItsDecision, settleReceived, SYNC_INBOX, syncConflicts, undoReceived, undoSync } from "../core/sync.js";
19
+ import { applySync, houseWords, discardReceived, nothingSince, withoutImages, planSync, readTreeFile, receivedAll, receivedPending, receivedSince, receivedOf, removedByItsDecision, settleReceived, SYNC_INBOX, syncConflicts, undoReceived, undoSync } from "../core/sync.js";
20
20
  import { labels } from "../gedcom/labels.js";
21
21
  import { startLive } from "../core/live.js";
22
22
  import { appSendsChanges, sendAppUrl, stromAppUrl } from "../core/stromapp.js";
@@ -67,7 +67,7 @@ function detailText(f, lang, name = (k) => k) {
67
67
  if (!f)
68
68
  return "";
69
69
  const ages = Object.entries(f.ages ?? {}).map(([who, a]) => `${name(who)} ${humanAge(a, lang)}`);
70
- return [f.house && ui(lang, "ui.sync.house", { x: f.house }), f.age && humanAge(f.age, lang), ...ages, f.cause && ui(lang, "ui.sync.cause", { x: f.cause })].filter(Boolean).join(", ");
70
+ return [f.house && houseWords(lang, f.house), f.age && humanAge(f.age, lang), ...ages, f.cause && ui(lang, "ui.sync.cause", { x: f.cause })].filter(Boolean).join(", ");
71
71
  }
72
72
  /** A position on the map as a map shows it: degrees, six decimals at most. */
73
73
  const point = (p) => `${+p.lat.toFixed(6)}, ${+p.lon.toFixed(6)}`;
@@ -4,14 +4,33 @@
4
4
  // asks its usage, a list of its models) gets the folder in its environment (claudeEnv), and what strom reads or writes
5
5
  // of Claude Code's own (transcripts, its settings, its skill, its record of trusted folders) is read there.
6
6
  // Only Claude Code: the other agents keep their own folders.
7
+ //
8
+ // Its default folder said outright (CLAUDE_CONFIG_DIR=~/.claude) is not the default to Claude Code: it looks for its
9
+ // login under another name (macOS: the keychain's "Claude Code-credentials-<hash>") and is not logged in there, its
10
+ // `claude -p /usage` a bare /cost (found 2026-11-03: a run stopped before its first session). So the default said is
11
+ // no folder said: every `claude` strom starts goes without the variable (forClaude), as the plain `claude` the person
12
+ // logged in with.
7
13
  import path from "node:path";
14
+ import { spawnSync } from "node:child_process";
8
15
  import { loadUserConfig, Settings } from "./config.js";
9
- import { userHome } from "./paths.js";
10
- /** The folder said outright — CLAUDE_CONFIG_DIR, else agent.claudeConfigDir (absolute, ~ expanded) — or undefined: Claude Code's own default. */
16
+ import { expandHome, userHome } from "./paths.js";
17
+ import { findAgent } from "./which.js";
18
+ /** Is this folder Claude Code's own default, ~/.claude (with a separator at the end, ~ unexpanded; case aside where the disk ignores it)? */
19
+ export function isDefaultClaudeDir(dir, env) {
20
+ const norm = (p) => {
21
+ const r = path.resolve(p);
22
+ return process.platform === "linux" ? r : r.toLowerCase();
23
+ };
24
+ return norm(expandHome(dir.trim(), env)) === norm(path.join(userHome(env), ".claude"));
25
+ }
26
+ /**
27
+ * The folder said outright — CLAUDE_CONFIG_DIR, else agent.claudeConfigDir (absolute, ~ expanded) — or undefined:
28
+ * Claude Code's own default (also when the default is what is said: isDefaultClaudeDir).
29
+ */
11
30
  export function claudeConfigSet(env, config) {
12
31
  const r = new Settings(env, {}, config ?? loadUserConfig(env)).resolve("agent.claudeConfigDir");
13
32
  const v = r && typeof r.value === "string" ? r.value.trim() : "";
14
- return v || undefined;
33
+ return v && !isDefaultClaudeDir(v, env) ? v : undefined;
15
34
  }
16
35
  /** Claude Code's folder: the one said (claudeConfigSet), else ~/.claude (`home`: another home folder than env's). */
17
36
  export function claudeConfigDir(env, config, home) {
@@ -32,7 +51,44 @@ export function claudeEnv(env, config) {
32
51
  const set = claudeConfigSet(env, config);
33
52
  return set ? { CLAUDE_CONFIG_DIR: set } : {};
34
53
  }
54
+ /** Does env say Claude Code's default folder outright (CLAUDE_CONFIG_DIR=~/.claude), which a `claude` of strom goes without? */
55
+ export function saysDefaultClaudeDir(env) {
56
+ return Boolean(env.CLAUDE_CONFIG_DIR?.trim()) && isDefaultClaudeDir(env.CLAUDE_CONFIG_DIR, env);
57
+ }
58
+ /**
59
+ * The whole environment of a `claude` strom starts: env with the folder of the setting (claudeEnv), and without
60
+ * CLAUDE_CONFIG_DIR when it says the default folder — a plain `claude`, logged in as the person logged in.
61
+ */
62
+ export function forClaude(env, config) {
63
+ const out = { ...env, ...claudeEnv(env, config) };
64
+ if (saysDefaultClaudeDir(env))
65
+ delete out.CLAUDE_CONFIG_DIR;
66
+ return out;
67
+ }
68
+ /** How to log Claude Code in for strom's `claude` (its folder said, when one is): what a person runs once. */
69
+ export function claudeLoginCommand(env, config) {
70
+ const set = claudeConfigSet(env, config);
71
+ if (!set)
72
+ return "claude";
73
+ return process.platform === "win32" ? `claude (CLAUDE_CONFIG_DIR=${set})` : `CLAUDE_CONFIG_DIR='${set.replace(/'/g, "'\\''")}' claude`;
74
+ }
35
75
  /** Is this command Claude Code's (claude, claude.cmd, a path to it)? */
36
76
  export function isClaudeCommand(command) {
37
77
  return /^claude(?:\.(?:cmd|exe|bat))?$/iu.test(path.basename(command));
38
78
  }
79
+ /**
80
+ * Is the `claude` strom starts logged in (`claude auth status`, in its environment: forClaude)? false only when it says
81
+ * so; undefined when it cannot tell (an older Claude Code, a stand-in, no answer in time) — then nothing is decided by it.
82
+ */
83
+ export function claudeLoggedIn(env, command = "claude") {
84
+ const program = findAgent(command, env) ?? command;
85
+ const shell = process.platform === "win32" && /\.(cmd|bat)$/i.test(program);
86
+ try {
87
+ const r = spawnSync(shell ? `"${program}"` : program, ["auth", "status"], { env: forClaude(env), encoding: "utf8", timeout: 20_000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true, shell });
88
+ const said = JSON.parse((r.stdout ?? "").trim());
89
+ return typeof said?.loggedIn === "boolean" ? said.loggedIn : undefined;
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
package/dist/core/gate.js CHANGED
@@ -19,7 +19,7 @@ import { StromError, UsageError } from "./errors.js";
19
19
  import { NAME_RE, pluginsDir } from "./connector.js";
20
20
  import { readAsset } from "./assets.js";
21
21
  import { ui } from "../cli/ui.js";
22
- import { claudeEnv } from "./claudedir.js";
22
+ import { forClaude } from "./claudedir.js";
23
23
  export const GATE_INTERFACE = 1;
24
24
  const MANIFEST = "gate.json";
25
25
  /** How long a gate may think (it may ask a service). */
@@ -168,9 +168,9 @@ export function askGate(gate, env, facts) {
168
168
  timeout,
169
169
  windowsHide: true,
170
170
  env: {
171
- ...inherited,
172
171
  // a gate that asks Claude Code (claude-usage) asks the one of agent.claudeConfigDir, as the runs it decides about
173
- ...claudeEnv(inherited),
172
+ // (and the default folder said outright: none — a plain `claude`, logged in)
173
+ ...forClaude(inherited),
174
174
  STROM_GATE: gate.name,
175
175
  STROM_TREE: facts.tree,
176
176
  STROM_LANG: facts.lang,
@@ -611,7 +611,7 @@ export function claimText(tree, c, claim) {
611
611
  const ofApp = !!(c.edit || c.parents || c.claims.some((x) => x.note === "the user's edit"));
612
612
  // a title cut at its length says one side no more
613
613
  if (ofApp && c.claims.length === 2 && i >= 0 && c.title.length < 200) {
614
- const m = / — (.*) × (.*)$/s.exec(conflictTitle(tree, c));
614
+ const m = / [—–] (.*) × (.*)$/s.exec(conflictTitle(tree, c));
615
615
  if (m && !m[1].includes(" × "))
616
616
  return m[i + 1];
617
617
  }
@@ -89,6 +89,12 @@ export const PHRASES = {
89
89
  "places.identify.why": "The person asked: which place of the name each is, where it lies, its names and jurisdictions — for the map, the history and the houses.",
90
90
  "places.identify.done": "Every place of the queue identified, asked about or found not located.",
91
91
  "places.identify.result": "every place of the queue identified, asked about or found not located",
92
+ "places.count.identified": "identified {n}",
93
+ "places.count.verified": "verified {n} (already identified, checked)",
94
+ "places.count.undecided": "checked, undecided {n}",
95
+ "places.count.asked": "with a question {n}",
96
+ "places.count.unlocated": "not located {n}",
97
+ "places.count.merged": "merged {n}",
92
98
  "history.region.what": "Events of history in {region}: wars, epidemics, censuses, changes of the state, emigration",
93
99
  "history.region.where": "encyclopedias, state institutions, archives, the Wikipedia of the region's language with its references",
94
100
  "history.region.why": "what the ancestors lived through, shown with their lives",