@tiagojct/subsub 0.5.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.
Files changed (54) hide show
  1. package/README.md +90 -0
  2. package/bin/subsub-bench.js +19 -0
  3. package/bin/subsub.js +14 -0
  4. package/dist/args.js +67 -0
  5. package/dist/bench.js +534 -0
  6. package/dist/bridge.js +107 -0
  7. package/dist/cli.js +209 -0
  8. package/dist/config.js +144 -0
  9. package/dist/doctor.js +131 -0
  10. package/dist/init.js +215 -0
  11. package/dist/look.js +107 -0
  12. package/dist/paths.js +86 -0
  13. package/dist/preview.js +94 -0
  14. package/dist/profiles.js +79 -0
  15. package/dist/prompt.js +72 -0
  16. package/dist/roles.js +73 -0
  17. package/dist/subsub.js +414 -0
  18. package/docs/Subsub.md +126 -0
  19. package/extensions/subsub.ts +6 -0
  20. package/package.json +73 -0
  21. package/prompts/alert.md +4 -0
  22. package/prompts/clean-tags.md +4 -0
  23. package/prompts/gaps.md +5 -0
  24. package/prompts/import-queue.md +5 -0
  25. package/prompts/lit-note.md +5 -0
  26. package/prompts/manuscript.md +5 -0
  27. package/prompts/synthesis.md +5 -0
  28. package/prompts/tag-batch.md +5 -0
  29. package/roles/librarian.md +46 -0
  30. package/roles/researcher.md +37 -0
  31. package/scripts/build.mjs +14 -0
  32. package/scripts/make-themes.py +116 -0
  33. package/src/args.ts +64 -0
  34. package/src/bench.ts +580 -0
  35. package/src/bridge.ts +134 -0
  36. package/src/cli.ts +201 -0
  37. package/src/config.ts +191 -0
  38. package/src/doctor.ts +171 -0
  39. package/src/init.ts +252 -0
  40. package/src/look.ts +141 -0
  41. package/src/paths.ts +100 -0
  42. package/src/preview.ts +107 -0
  43. package/src/profiles.ts +99 -0
  44. package/src/prompt.ts +81 -0
  45. package/src/roles.ts +83 -0
  46. package/src/subsub.ts +434 -0
  47. package/templates/Zotero agent.md +87 -0
  48. package/templates/vocabularies/any-field.md +52 -0
  49. package/templates/vocabularies/health-informatics.md +88 -0
  50. package/templates/vocabularies/health-sciences.md +95 -0
  51. package/themes/subsub-glauca-dark.json +91 -0
  52. package/themes/subsub-glauca-light.json +91 -0
  53. package/themes/subsub-try-works-dark.json +91 -0
  54. package/themes/subsub-try-works-light.json +91 -0
package/dist/cli.js ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * The `subsub` command: pi, set up as Sub-Sub.
3
+ *
4
+ * - Own pi folder (~/.subsub/agent, or SUBSUB_AGENT_DIR): settings, sessions
5
+ * and packages are separate from plain pi.
6
+ * - The Sub-Sub package is declared in that folder's settings.json, so pi
7
+ * loads the extension and the prompt templates in the normal way.
8
+ * - Credentials: if pi already has an auth.json and Sub-Sub has none, Sub-Sub
9
+ * links to it, so one /login serves both.
10
+ * - Folder: started from the home folder, Sub-Sub moves to the vault.
11
+ * `--here` keeps the current folder.
12
+ * - `subsub review preview|apply NOTE...` runs zotero-review (tag review notes,
13
+ * several at a time) with Sub-Sub's server folder and settings.
14
+ * - `subsub init` sets Sub-Sub up; `subsub doctor` checks the setup.
15
+ */
16
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
17
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
18
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
19
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
20
+ });
21
+ }
22
+ return path;
23
+ };
24
+ import { spawnSync } from "node:child_process";
25
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, symlinkSync, writeFileSync } from "node:fs";
26
+ import { homedir } from "node:os";
27
+ import { dirname, join, resolve } from "node:path";
28
+ import { fileURLToPath, pathToFileURL } from "node:url";
29
+ import { expand, loadConfig, serverCommand } from "./config.js";
30
+ export const PACKAGE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
+ export function packageVersion(dir = PACKAGE_DIR) {
32
+ try {
33
+ return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version ?? "0.0.0";
34
+ }
35
+ catch {
36
+ return "0.0.0";
37
+ }
38
+ }
39
+ export function agentDirFor(env, home = homedir()) {
40
+ return env.SUBSUB_AGENT_DIR ? expand(env.SUBSUB_AGENT_DIR) : join(home, ".subsub", "agent");
41
+ }
42
+ function real(p) {
43
+ try {
44
+ return realpathSync(p);
45
+ }
46
+ catch {
47
+ return resolve(p);
48
+ }
49
+ }
50
+ function sourceOf(entry) {
51
+ if (typeof entry === "string")
52
+ return entry;
53
+ if (entry && typeof entry === "object" && typeof entry.source === "string") {
54
+ return entry.source;
55
+ }
56
+ return undefined;
57
+ }
58
+ /**
59
+ * Make sure settings.json in the agent folder declares the Sub-Sub package.
60
+ * Other settings are kept. Returns what happened, for tests and messages.
61
+ */
62
+ export function ensureSettings(agentDir, packageDir = PACKAGE_DIR) {
63
+ mkdirSync(agentDir, { recursive: true });
64
+ const file = join(agentDir, "settings.json");
65
+ let settings = {};
66
+ let existed = false;
67
+ if (existsSync(file)) {
68
+ existed = true;
69
+ try {
70
+ settings = JSON.parse(readFileSync(file, "utf8"));
71
+ }
72
+ catch {
73
+ return "unreadable";
74
+ }
75
+ }
76
+ const packages = Array.isArray(settings.packages) ? [...settings.packages] : [];
77
+ const want = real(packageDir);
78
+ const has = packages.some((p) => {
79
+ const src = sourceOf(p);
80
+ return src !== undefined && real(resolve(agentDir, expand(src))) === want;
81
+ });
82
+ // Sub-Sub draws its own header; pi's list of loaded resources is noise here. Set once, so it can be changed.
83
+ const quiet = settings.quietStartup === undefined;
84
+ if (has && !quiet)
85
+ return "present";
86
+ if (!has)
87
+ packages.push(want);
88
+ settings.packages = packages;
89
+ if (quiet)
90
+ settings.quietStartup = true;
91
+ writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
92
+ return existed ? "added" : "created";
93
+ }
94
+ function isEmptyAuth(file) {
95
+ try {
96
+ const data = JSON.parse(readFileSync(file, "utf8"));
97
+ return data && typeof data === "object" && Object.keys(data).length === 0;
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ /** Link Sub-Sub's auth.json to pi's, when pi has one and Sub-Sub has none. */
104
+ export function shareAuth(agentDir, piAgentDir) {
105
+ const mine = join(agentDir, "auth.json");
106
+ const theirs = join(piAgentDir, "auth.json");
107
+ if (!existsSync(theirs))
108
+ return false;
109
+ try {
110
+ const st = lstatSync(mine);
111
+ // pi writes an empty "{}" on first start; that one may be replaced.
112
+ if (st.isSymbolicLink() || !isEmptyAuth(mine))
113
+ return false;
114
+ renameSync(mine, `${mine}.empty`);
115
+ }
116
+ catch (err) {
117
+ if (err.code !== "ENOENT")
118
+ throw err;
119
+ }
120
+ mkdirSync(agentDir, { recursive: true });
121
+ symlinkSync(theirs, mine);
122
+ return true;
123
+ }
124
+ /** Folder to start in: the vault when started from the home folder, unless --here. */
125
+ export function chooseCwd(cwd, home, vault, here) {
126
+ if (here || !vault || !existsSync(vault))
127
+ return cwd;
128
+ return real(cwd) === real(home) ? vault : cwd;
129
+ }
130
+ /** `pi update` with no package source updates pi itself; here that must not happen. */
131
+ export function isSelfUpdate(args) {
132
+ if (args[0] !== "update")
133
+ return false;
134
+ const rest = args.slice(1).filter((a) => a !== "--force" && a !== "-a" && a !== "--approve");
135
+ return rest.length === 0 || rest.some((a) => ["--self", "self", "pi", "--all"].includes(a));
136
+ }
137
+ /** The command line for `subsub review ...`: zotero-review in the server folder, with the same settings file. */
138
+ export function reviewCommand(args, cfg, env = process.env) {
139
+ return {
140
+ ...serverCommand(cfg, "zotero-review", args, env),
141
+ env: { ...env, ...(cfg.envFile ? { ZOTERO_MCP_ENV: cfg.envFile } : {}) },
142
+ };
143
+ }
144
+ export async function run(argv = process.argv.slice(2)) {
145
+ const env = process.env;
146
+ const home = homedir();
147
+ const here = argv.includes("--here");
148
+ const args = argv.filter((a) => a !== "--here");
149
+ if (args[0] === "--version" || args[0] === "-v") {
150
+ const piPkg = join(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "..");
151
+ console.log(`subsub ${packageVersion()} (pi ${packageVersion(piPkg)})`);
152
+ return;
153
+ }
154
+ if (args[0] === "init") {
155
+ const { initMain } = await import("./init.js");
156
+ process.exitCode = await initMain(args.slice(1), env);
157
+ return;
158
+ }
159
+ if (args[0] === "doctor") {
160
+ const { doctorMain } = await import("./doctor.js");
161
+ process.exitCode = await doctorMain(env);
162
+ return;
163
+ }
164
+ if (args[0] === "review") {
165
+ const r = reviewCommand(args.slice(1), loadConfig(env), env);
166
+ const res = spawnSync(r.command, r.args, { env: r.env, stdio: "inherit" });
167
+ if (res.error)
168
+ console.error(`Sub-Sub: cannot run ${r.command}: ${res.error.message}`);
169
+ process.exitCode = res.status ?? 1;
170
+ return;
171
+ }
172
+ if (isSelfUpdate(args)) {
173
+ console.log(`Sub-Sub is not updated with "update". Its code is in ${PACKAGE_DIR}; after new code arrives, run "npm install" there.`);
174
+ console.log('To update packages added to Sub-Sub, use "subsub update --extensions".');
175
+ process.exitCode = 1;
176
+ return;
177
+ }
178
+ const agentDir = agentDirFor(env, home);
179
+ const piAgentDir = env.PI_CODING_AGENT_DIR ? expand(env.PI_CODING_AGENT_DIR) : join(home, ".pi", "agent");
180
+ const settings = ensureSettings(agentDir);
181
+ if (settings === "unreadable") {
182
+ console.error(`Sub-Sub: ${join(agentDir, "settings.json")} is not valid JSON. Fix it, or move it away and start again.`);
183
+ process.exitCode = 1;
184
+ return;
185
+ }
186
+ try {
187
+ shareAuth(agentDir, piAgentDir);
188
+ }
189
+ catch {
190
+ /* not important: /login in Sub-Sub also works */
191
+ }
192
+ let vault;
193
+ try {
194
+ vault = loadConfig(env).vault;
195
+ }
196
+ catch {
197
+ vault = undefined; // the extension reports config errors when it starts
198
+ }
199
+ const cwd = chooseCwd(process.cwd(), home, vault, here);
200
+ if (cwd !== process.cwd())
201
+ process.chdir(cwd);
202
+ env.PI_CODING_AGENT_DIR = agentDir;
203
+ env.SUBSUB_CLI = "1";
204
+ // pi's update notice would point to the global pi, not to this copy.
205
+ env.PI_SKIP_VERSION_CHECK ??= "1";
206
+ process.argv = [process.argv[0], process.argv[1], ...args];
207
+ const index = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
208
+ await import(__rewriteRelativeImportExtension(pathToFileURL(join(dirname(index), "bundle", "cli.js")).href));
209
+ }
package/dist/config.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Settings for Sub-Sub. Everything has a default; an optional JSON file
3
+ * (~/.config/subsub/config.json, or SUBSUB_CONFIG) overrides it.
4
+ *
5
+ * {
6
+ * "profile": "scholar",
7
+ * "userName": "Ana", "about": "a master's student in health informatics", "language": "English",
8
+ * "vault": "~/Notes",
9
+ * "models": { "librarian": "opencode-go/glm-5.3-flash", "researcher": "opencode-go/mimo-v2.6-pro" },
10
+ * "defaultMode": "researcher"
11
+ * }
12
+ *
13
+ * `subsub init` writes this file. Without "serverDir" (or when that folder does
14
+ * not exist), the Zotero servers run from PyPI with uv.
15
+ */
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { dirname, join, resolve } from "node:path";
19
+ export const PROFILES = ["reader", "scholar", "author", "editor"];
20
+ /** The Zotero server release that this Sub-Sub version runs when there is no local server folder. */
21
+ export const SERVER_VERSION = "0.4.1";
22
+ export function expand(p) {
23
+ return resolve(p.replace(/^~(?=$|\/)/, homedir()));
24
+ }
25
+ export function parseEnvFile(path) {
26
+ const out = {};
27
+ let text;
28
+ try {
29
+ text = readFileSync(path, "utf8");
30
+ }
31
+ catch {
32
+ return out;
33
+ }
34
+ for (const raw of text.split("\n")) {
35
+ const line = raw.trim();
36
+ if (!line || line.startsWith("#") || !line.includes("="))
37
+ continue;
38
+ const i = line.indexOf("=");
39
+ out[line.slice(0, i).trim()] = line.slice(i + 1).trim().replace(/^['"]|['"]$/g, "");
40
+ }
41
+ return out;
42
+ }
43
+ export function configPath(env = process.env) {
44
+ return expand(env.SUBSUB_CONFIG ?? "~/.config/subsub/config.json");
45
+ }
46
+ export function readConfigFile(file) {
47
+ if (!existsSync(file))
48
+ return {};
49
+ try {
50
+ return JSON.parse(readFileSync(file, "utf8"));
51
+ }
52
+ catch (err) {
53
+ throw new Error(`Sub-Sub: cannot read ${file}: ${err.message}`);
54
+ }
55
+ }
56
+ /** Merge settings into the settings file; other settings stay. */
57
+ export function saveConfig(patch, env = process.env) {
58
+ const file = configPath(env);
59
+ const merged = { ...readConfigFile(file), ...patch };
60
+ for (const [k, v] of Object.entries(merged))
61
+ if (v === undefined)
62
+ delete merged[k];
63
+ mkdirSync(dirname(file), { recursive: true });
64
+ writeFileSync(file, `${JSON.stringify(merged, null, 2)}\n`);
65
+ return file;
66
+ }
67
+ export function isProfile(x) {
68
+ return typeof x === "string" && PROFILES.includes(x);
69
+ }
70
+ export function loadConfig(env = process.env) {
71
+ const file = configPath(env);
72
+ let user = {};
73
+ if (existsSync(file)) {
74
+ try {
75
+ user = JSON.parse(readFileSync(file, "utf8"));
76
+ }
77
+ catch (err) {
78
+ throw new Error(`Sub-Sub: cannot read ${file}: ${err.message}`);
79
+ }
80
+ }
81
+ const envCandidates = [
82
+ user.envFile,
83
+ env.ZOTERO_MCP_ENV,
84
+ "~/.config/zotero-local-mcp/env",
85
+ "~/.config/opencode/zotero.env",
86
+ ].filter((x) => Boolean(x));
87
+ const envFile = envCandidates.map(expand).find((p) => existsSync(p));
88
+ const envValues = envFile ? parseEnvFile(envFile) : {};
89
+ const vaultRaw = user.vault ?? env.ZOTERO_VAULT ?? envValues.ZOTERO_VAULT;
90
+ const vault = vaultRaw ? expand(vaultRaw) : undefined;
91
+ const mode = user.defaultMode === "librarian" ? "librarian" : "researcher";
92
+ return {
93
+ configFile: file,
94
+ profile: isProfile(user.profile) ? user.profile : "scholar",
95
+ userName: typeof user.userName === "string" && user.userName.trim() ? user.userName.trim() : undefined,
96
+ about: typeof user.about === "string" && user.about.trim() ? user.about.trim() : undefined,
97
+ language: typeof user.language === "string" && user.language.trim() ? user.language.trim() : "English",
98
+ setup: user.setup,
99
+ serverDir: expand(user.serverDir ?? "~/Projects/zotero-local-mcp"),
100
+ envFile,
101
+ vault,
102
+ models: user.models ?? {
103
+ librarian: "opencode-go/glm-5.3-flash",
104
+ researcher: "opencode-go/mimo-v2.6-pro",
105
+ },
106
+ defaultMode: mode,
107
+ vaultContext: (user.vaultContext ?? (vault ? [join(vault, ".claude", "CLAUDE.md")] : [])).map(expand),
108
+ sharedRules: user.sharedRules
109
+ ? expand(user.sharedRules)
110
+ : vault
111
+ ? join(vault, "Systems", "Zotero agent.md")
112
+ : undefined,
113
+ startTimeout: user.startTimeout ?? 45,
114
+ uvPath: user.uvPath ? expand(user.uvPath) : undefined,
115
+ look: user.look,
116
+ quotes: user.quotes,
117
+ themes: user.themes,
118
+ };
119
+ }
120
+ /** uv is often missing from PATH when pi is started outside a login shell. */
121
+ export function findUv(cfg, env = process.env, platform = process.platform) {
122
+ const unix = ["~/.local/bin/uv", "/opt/homebrew/bin/uv", "/usr/local/bin/uv", "~/.cargo/bin/uv"];
123
+ const windows = ["~/.local/bin/uv.exe", "~/.cargo/bin/uv.exe", env.LOCALAPPDATA ? join(env.LOCALAPPDATA, "uv", "uv.exe") : ""];
124
+ const candidates = [cfg.uvPath, env.SUBSUB_UV, ...(platform === "win32" ? windows : unix)]
125
+ .filter((x) => Boolean(x))
126
+ .map(expand);
127
+ return candidates.find((p) => existsSync(p)) ?? "uv";
128
+ }
129
+ /**
130
+ * How to start one of the Python programs (zotero-local-mcp, zotero-scholar-mcp,
131
+ * zotero-review): from the local server folder when it exists, otherwise the
132
+ * pinned release from PyPI through `uv tool run`.
133
+ */
134
+ export function serverCommand(cfg, program, args = [], env = process.env) {
135
+ const uv = findUv(cfg, env);
136
+ if (existsSync(join(cfg.serverDir, "pyproject.toml"))) {
137
+ return { command: uv, args: ["run", "--quiet", "--directory", cfg.serverDir, program, ...args] };
138
+ }
139
+ return { command: uv, args: ["tool", "run", "--quiet", "--from", `zotero-local-mcp==${SERVER_VERSION}`, program, ...args] };
140
+ }
141
+ /** How prompts name the user. */
142
+ export function who(cfg) {
143
+ return cfg.userName ?? "the user";
144
+ }
package/dist/doctor.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `subsub doctor`: check the setup and say how to fix each problem.
3
+ *
4
+ * Checks Node, the settings file, uv, the Zotero server (it runs
5
+ * `zotero-local-mcp --check`), Zotero itself, the tag list, the notes folder,
6
+ * the contact email and the model login. Exit code 1 when something needs a fix.
7
+ */
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { findUv, loadConfig, SERVER_VERSION, serverCommand } from "./config.js";
13
+ import { profileSpec } from "./profiles.js";
14
+ export const defaultRunner = (command, args, env) => {
15
+ const r = spawnSync(command, args, { env, encoding: "utf8", timeout: 120_000 });
16
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", error: r.error?.message };
17
+ };
18
+ const ZOTERO_SETTING = 'In Zotero, open Settings > Advanced and turn on "Allow other applications on this computer to communicate with Zotero".';
19
+ function nodeOk(version) {
20
+ const [major, minor] = version.split(".").map(Number);
21
+ return major > 22 || (major === 22 && minor >= 19);
22
+ }
23
+ function hasLogin(agentDir) {
24
+ try {
25
+ const data = JSON.parse(readFileSync(join(agentDir, "auth.json"), "utf8"));
26
+ return Boolean(data) && typeof data === "object" && Object.keys(data).length > 0;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ export function runChecks(cfg, env, deps = {}) {
33
+ const run = deps.run ?? defaultRunner;
34
+ const out = [];
35
+ const node = deps.nodeVersion ?? process.versions.node;
36
+ out.push(nodeOk(node)
37
+ ? { name: "Node", ok: true, detail: node }
38
+ : { name: "Node", ok: false, detail: node, fix: "Install Node.js 22.19 or later (https://nodejs.org), or run the Sub-Sub installer again." });
39
+ out.push(existsSync(cfg.configFile)
40
+ ? { name: "Settings", ok: true, detail: `${cfg.configFile}, profile ${profileSpec(cfg.profile).label}, replies in ${cfg.language}` }
41
+ : { name: "Settings", ok: false, detail: `no ${cfg.configFile}`, fix: "Type subsub init." });
42
+ const uv = findUv(cfg, env);
43
+ const uvRun = run(uv, ["--version"], env);
44
+ if (uvRun.status !== 0) {
45
+ out.push({ name: "uv", ok: false, detail: uvRun.error ?? uvRun.stderr.trim(), fix: "Install uv (https://docs.astral.sh/uv/), or add \"uvPath\" to the settings file." });
46
+ return out;
47
+ }
48
+ out.push({ name: "uv", ok: true, detail: uvRun.stdout.trim() });
49
+ const cmd = serverCommand(cfg, "zotero-local-mcp", ["--check"], env);
50
+ const local = cmd.args.includes("--directory");
51
+ const res = run(cmd.command, cmd.args, { ...env, ...(cfg.envFile ? { ZOTERO_MCP_ENV: cfg.envFile } : {}) });
52
+ let st;
53
+ try {
54
+ st = JSON.parse(res.stdout);
55
+ }
56
+ catch {
57
+ st = undefined;
58
+ }
59
+ if (res.status !== 0 || !st) {
60
+ const text = (res.error ?? res.stderr).replace(/[^\x20-\x7e\n]/g, " ");
61
+ const first = text.trim().split("\n").slice(-3).join(" ").replace(/\s+/g, " ").trim();
62
+ const notPublished = /not found in the package registry/.test(text);
63
+ out.push({
64
+ name: "Zotero server",
65
+ ok: false,
66
+ detail: notPublished ? `zotero-local-mcp ${SERVER_VERSION} is not on PyPI` : first || "no answer",
67
+ fix: local
68
+ ? `Type: cd "${cfg.serverDir}" && uv sync`
69
+ : notPublished
70
+ ? 'Set "serverDir" in the settings file to a copy of the zotero-local-mcp repository.'
71
+ : `uv could not run zotero-local-mcp ${SERVER_VERSION} from PyPI. Check the internet connection and try again.`,
72
+ });
73
+ return out;
74
+ }
75
+ out.push({ name: "Zotero server", ok: true, detail: local ? `from ${cfg.serverDir}` : `zotero-local-mcp ${SERVER_VERSION}` });
76
+ out.push(st.zotero === "reachable"
77
+ ? { name: "Zotero", ok: true, detail: `reachable, write permission ${st.write_key_remembered ? "remembered" : "asked at the first change"}` }
78
+ : { name: "Zotero", ok: false, detail: String(st.error ?? "not reachable"), fix: `Start Zotero 10 or later. ${ZOTERO_SETTING}` });
79
+ const v = st.vocabulary ?? {};
80
+ if (v.loaded === false || !v.tags) {
81
+ out.push({ name: "Tag list", ok: false, detail: v.path ? `cannot read ${v.path}` : "not set", fix: "Type subsub init: it creates a starter tag list." });
82
+ }
83
+ else {
84
+ const noTopics = Array.isArray(v.facets) && !v.facets.includes("topic");
85
+ out.push({
86
+ name: "Tag list",
87
+ ok: !(v.problems?.length),
88
+ warn: noTopics,
89
+ detail: `${v.tags} tags in ${v.path}${noTopics ? "; no topic/ tags yet" : ""}`,
90
+ fix: v.problems?.length
91
+ ? `Fix these lines in the tag list: ${v.problems.slice(0, 3).join("; ")}`
92
+ : noTopics
93
+ ? "In Sub-Sub (librarian), ask: propose topics for my tag list."
94
+ : undefined,
95
+ });
96
+ }
97
+ out.push(cfg.vault && existsSync(cfg.vault)
98
+ ? { name: "Notes folder", ok: true, detail: cfg.vault }
99
+ : { name: "Notes folder", ok: false, detail: cfg.vault ? `${cfg.vault} does not exist` : "not set", fix: "Type subsub init." });
100
+ out.push(st.contact_email_set
101
+ ? { name: "Contact email", ok: true, detail: "set" }
102
+ : { name: "Contact email", ok: true, warn: true, detail: "not set: open-access PDFs cannot be found", fix: "Type subsub init and give an email address." });
103
+ const agentDir = deps.agentDir ?? (env.SUBSUB_AGENT_DIR ? env.SUBSUB_AGENT_DIR : join(homedir(), ".subsub", "agent"));
104
+ out.push(hasLogin(agentDir)
105
+ ? { name: "Model login", ok: true, detail: Object.keys(cfg.models).length ? Object.values(cfg.models).join(", ") : "pi's default model" }
106
+ : { name: "Model login", ok: false, detail: "no model provider yet", fix: "Type subsub, then /login." });
107
+ return out;
108
+ }
109
+ export function formatChecks(checks) {
110
+ return checks
111
+ .map((c) => {
112
+ const tag = !c.ok ? "FIX " : c.warn ? "NOTE" : "ok ";
113
+ return `${tag} ${c.name.padEnd(14)} ${c.detail}${c.fix && (!c.ok || c.warn) ? `\n ${" ".repeat(14)} ${c.fix}` : ""}`;
114
+ })
115
+ .join("\n");
116
+ }
117
+ export async function doctorMain(env = process.env) {
118
+ let cfg;
119
+ try {
120
+ cfg = loadConfig(env);
121
+ }
122
+ catch (err) {
123
+ console.log(`FIX Settings ${err.message}\n Fix the JSON, or move the file away and type subsub init.`);
124
+ return 1;
125
+ }
126
+ const checks = runChecks(cfg, env);
127
+ console.log(formatChecks(checks));
128
+ const bad = checks.filter((c) => !c.ok).length;
129
+ console.log(bad ? `\n${bad} problem(s) to fix.` : "\nEverything needed is in place.");
130
+ return bad ? 1 : 0;
131
+ }