dsh-trivium 0.4.9 → 0.4.13

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/lib/purge.js ADDED
@@ -0,0 +1,122 @@
1
+ /** Wipe plugin files on real uninstall. Never run this on dsh web restart. */
2
+
3
+ import { existsSync, readdirSync, unlinkSync } from "node:fs";
4
+ import { basename, join, resolve } from "node:path";
5
+ import { listedWorkspaces, settingsFilePath } from "./settings.js";
6
+ import { closeAll, listOpenWorkspaces } from "./store.js";
7
+
8
+ export function isTriviumArtifactName(name) {
9
+ const n = String(name || "");
10
+ return (
11
+ n === "trivium-pending.json" ||
12
+ n === "trivium.jsonl" ||
13
+ n.startsWith("trivium.jsonl") ||
14
+ n === "trivium.tdb" ||
15
+ n.startsWith("trivium.tdb")
16
+ );
17
+ }
18
+
19
+ /**
20
+ * `dsh plugin remove` / `pnpm remove` should wipe.
21
+ * `pnpm add` / update / ci reinstall the same package and must keep memory.
22
+ */
23
+ export function shouldWipeOnUninstall(env = process.env, argv = process.argv) {
24
+ if (argv.includes("--force-purge")) return true;
25
+ const event = String(env.npm_lifecycle_event || "").toLowerCase();
26
+ if (event && event !== "preuninstall" && event !== "uninstall") return false;
27
+
28
+ const cmd = String(env.npm_command || "").toLowerCase();
29
+ if (cmd === "remove" || cmd === "uninstall") return true;
30
+ if (cmd === "install" || cmd === "add" || cmd === "update" || cmd === "upgrade" || cmd === "ci" || cmd === "dedupe") {
31
+ return false;
32
+ }
33
+
34
+ const cooked = String(env.npm_config_argv || "").toLowerCase();
35
+ if (/\b(remove|uninstall)\b/.test(cooked) && !/\b(install|add|update|upgrade)\b/.test(cooked)) return true;
36
+ if (/\b(install|add|update|upgrade|ci)\b/.test(cooked)) return false;
37
+
38
+ // Unknown command: keep memory. Upgrades must never delete the store.
39
+ return false;
40
+ }
41
+
42
+ export function collectPurgeCwds(extraCwds = []) {
43
+ const seen = new Set();
44
+ const out = [];
45
+ const add = (cwd) => {
46
+ const key = String(cwd || "").trim();
47
+ if (!key) return;
48
+ const resolved = resolve(key);
49
+ if (seen.has(resolved)) return;
50
+ seen.add(resolved);
51
+ out.push(resolved);
52
+ };
53
+ for (const cwd of listedWorkspaces()) add(cwd);
54
+ for (const row of listOpenWorkspaces()) add(row.cwd);
55
+ for (const cwd of extraCwds) add(cwd);
56
+ return out;
57
+ }
58
+
59
+ function unlinkQuiet(path, files, errors) {
60
+ try {
61
+ unlinkSync(path);
62
+ files.push(path);
63
+ } catch (err) {
64
+ if (err && err.code !== "ENOENT") errors.push({ path, message: String(err.message || err) });
65
+ }
66
+ }
67
+
68
+ export function purgeWorkspaceArtifacts(cwd, files, errors) {
69
+ const dir = join(resolve(cwd), ".dsh");
70
+ if (!existsSync(dir)) return;
71
+ let names = [];
72
+ try {
73
+ names = readdirSync(dir);
74
+ } catch (err) {
75
+ errors.push({ path: dir, message: String(err.message || err) });
76
+ return;
77
+ }
78
+ for (const name of names) {
79
+ if (!isTriviumArtifactName(name)) continue;
80
+ unlinkQuiet(join(dir, name), files, errors);
81
+ }
82
+ }
83
+
84
+ /** Delete known workspace traces and ~/.dsh/trivium.json. Leaves other .dsh files alone. */
85
+ export function purgeAllTraces({ extraCwds = [], close = closeAll } = {}) {
86
+ const settingsFile = settingsFilePath();
87
+ const workspaces = collectPurgeCwds(extraCwds);
88
+ try {
89
+ close();
90
+ } catch {
91
+ // ignore
92
+ }
93
+ const files = [];
94
+ const errors = [];
95
+ for (const cwd of workspaces) purgeWorkspaceArtifacts(cwd, files, errors);
96
+ unlinkQuiet(settingsFile, files, errors);
97
+ return { ok: errors.length === 0, workspaces, files, errors, settingsFile };
98
+ }
99
+
100
+ function shouldRunCli() {
101
+ return basename(String(process.argv[1] || "")) === "purge.js";
102
+ }
103
+
104
+ export function runUninstallPurge(env = process.env, argv = process.argv, log = console) {
105
+ if (!shouldWipeOnUninstall(env, argv)) {
106
+ log.info?.("[dsh-trivium] skip data wipe (install/update, not remove)");
107
+ return { skipped: true };
108
+ }
109
+ const result = purgeAllTraces();
110
+ const n = result.files.length;
111
+ log.info?.(`[dsh-trivium] removed ${n} plugin file${n === 1 ? "" : "s"} from ${result.workspaces.length} workspace(s)`);
112
+ if (result.errors.length) {
113
+ log.warn?.("[dsh-trivium] some files could not be deleted (stop dsh web and delete leftovers):");
114
+ for (const err of result.errors) log.warn?.(` ${err.path}: ${err.message}`);
115
+ }
116
+ return result;
117
+ }
118
+
119
+ if (shouldRunCli()) {
120
+ const result = runUninstallPurge();
121
+ if (result?.errors?.length) process.exitCode = 1;
122
+ }
package/lib/server.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import {
3
+ generateSidecarFromDb,
3
4
  archiveNode,
4
5
  backfillEmbeddings,
5
6
  dbPathFor,
7
+ deleteGitSidecars,
6
8
  deleteNode,
7
9
  exportGraph,
8
10
  getActiveCwd,
@@ -15,7 +17,7 @@ import {
15
17
  updateNode,
16
18
  } from "./store.js";
17
19
  import { embeddingPublicStatus } from "./embed.js";
18
- import { chipsEnabledOf, recallModeOf, sessionLayerEnabledOf, writeUiSettings } from "./settings.js";
20
+ import { chipsEnabledOf, configPanelOpenOf, gitSidecarEnabledOf, pluginLocaleOf, recallModeOf, sessionLayerEnabledOf, writeUiSettings } from "./settings.js";
19
21
  import { discoverWorkbuddyFiles, discoverExternalFiles, exportMarkdown, importWorkbuddy, importExternal } from "./markdown.js";
20
22
  import { checkNpmUpdate, installedVersion } from "./update.js";
21
23
  import { writeGateMessage } from "./hygiene.js";
@@ -116,6 +118,9 @@ async function handle(req, res, { liveOptions, status }) {
116
118
  extractEnabled: opts.extractEnabled !== false,
117
119
  chipsEnabled: chipsEnabledOf(opts),
118
120
  sessionLayerEnabled: opts.sessionLayerEnabled === true,
121
+ gitSidecarEnabled: gitSidecarEnabledOf(opts),
122
+ pluginLocale: pluginLocaleOf(opts),
123
+ configPanelOpen: configPanelOpenOf(opts),
119
124
  writeApproval: !!opts.writeApproval,
120
125
  mapTokenBudget: opts.mapTokenBudget || 400,
121
126
  workspaces: listOpenWorkspaces(),
@@ -138,6 +143,29 @@ async function handle(req, res, { liveOptions, status }) {
138
143
  return;
139
144
  }
140
145
 
146
+ if (path === "/api/dsh-trivium/sidecar/generate" && method === "POST") {
147
+ if (!gitSidecarEnabledOf(liveOptions())) {
148
+ sendJson(res, 400, { ok: false, message: "git sidecar off" });
149
+ return;
150
+ }
151
+ const { cwd, db } = await dbForRequest(url);
152
+ if (!db) {
153
+ sendJson(res, 400, { ok: false, message: "no workspace" });
154
+ return;
155
+ }
156
+ sendJson(res, 200, generateSidecarFromDb(cwd, db));
157
+ return;
158
+ }
159
+
160
+ if (path === "/api/dsh-trivium/sidecar/delete" && method === "POST") {
161
+ if (gitSidecarEnabledOf(liveOptions())) {
162
+ sendJson(res, 400, { ok: false, message: "turn sidecar off first" });
163
+ return;
164
+ }
165
+ sendJson(res, 200, deleteGitSidecars());
166
+ return;
167
+ }
168
+
141
169
  if (path === "/api/dsh-trivium/export" && method === "GET") {
142
170
  const { cwd, db } = await dbForRequest(url);
143
171
  if (!db) {
package/lib/settings.js CHANGED
@@ -1,19 +1,68 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
2
+ import { homedir, tmpdir } from "node:os";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { looksHttpUrl } from "./embed.js";
5
5
 
6
6
  export const SETTINGS_FILE = join(homedir(), ".dsh", "trivium.json");
7
7
 
8
+ /** Tests may set DSH_TRIVIUM_SETTINGS to a temp file so we never touch the live home config. */
9
+ export function settingsFilePath() {
10
+ const override = String(process.env.DSH_TRIVIUM_SETTINGS || "").trim();
11
+ return override ? resolve(override) : SETTINGS_FILE;
12
+ }
13
+
14
+ function persistSettings(next) {
15
+ const file = settingsFilePath();
16
+ mkdirSync(dirname(file), { recursive: true });
17
+ writeFileSync(file, JSON.stringify(next, null, 2), "utf8");
18
+ return next;
19
+ }
20
+
8
21
  export function readUiSettings() {
9
22
  try {
10
- const raw = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
23
+ const raw = JSON.parse(readFileSync(settingsFilePath(), "utf8"));
11
24
  return raw && typeof raw === "object" ? raw : {};
12
25
  } catch {
13
26
  return {};
14
27
  }
15
28
  }
16
29
 
30
+ export function listedWorkspaces() {
31
+ const raw = readUiSettings().workspaces;
32
+ if (!Array.isArray(raw)) return [];
33
+ const seen = new Set();
34
+ const out = [];
35
+ for (const item of raw) {
36
+ const cwd = String(item || "").trim();
37
+ if (!cwd) continue;
38
+ const key = resolve(cwd);
39
+ if (seen.has(key)) continue;
40
+ seen.add(key);
41
+ out.push(key);
42
+ }
43
+ return out;
44
+ }
45
+
46
+ function isInsideDir(parent, child) {
47
+ const rel = relative(resolve(parent), resolve(child));
48
+ return rel === "" || (rel !== ".." && !rel.startsWith(".." + sep));
49
+ }
50
+
51
+ /** Remember a workspace so uninstall can delete its .tdb. No-op if already listed. */
52
+ export function rememberWorkspace(cwd) {
53
+ const key = String(cwd || "").trim();
54
+ if (!key) return listedWorkspaces();
55
+ const resolved = resolve(key);
56
+ if (!process.env.DSH_TRIVIUM_SETTINGS && isInsideDir(tmpdir(), resolved)) {
57
+ return listedWorkspaces();
58
+ }
59
+ const prev = readUiSettings();
60
+ const workspaces = listedWorkspaces();
61
+ if (workspaces.includes(resolved)) return workspaces;
62
+ persistSettings({ ...prev, workspaces: [...workspaces, resolved] });
63
+ return [...workspaces, resolved];
64
+ }
65
+
17
66
  export function recallModeOf(opts = {}) {
18
67
  const mode = opts.recallMode;
19
68
  if (mode === "auto" || mode === "anchor" || mode === "off") return mode;
@@ -33,6 +82,44 @@ export function sessionLayerEnabledOf(opts = {}) {
33
82
  return chipsEnabledOf(opts) && opts.sessionLayerEnabled === true;
34
83
  }
35
84
 
85
+ /** Git JSONL sidecar write. Missing key = off. Import still runs when this is off. */
86
+ export function gitSidecarEnabledOf(opts = {}) {
87
+ return opts.gitSidecarEnabled === true;
88
+ }
89
+
90
+ /** Plugin UI language. Missing key = follow the DSH host. */
91
+ export function pluginLocaleOf(opts = {}) {
92
+ const v = String(opts.pluginLocale || "").toLowerCase();
93
+ if (v === "zh" || v === "en" || v === "host") return v;
94
+ return "host";
95
+ }
96
+
97
+ /** Settings fold. Missing key = collapsed. */
98
+ export function configPanelOpenOf(opts = {}) {
99
+ return opts.configPanelOpen === true;
100
+ }
101
+
102
+ export function sidecarHashOf(cwd) {
103
+ const raw = readUiSettings().sidecarHashByCwd;
104
+ if (!raw || typeof raw !== "object") return "";
105
+ const key = resolve(String(cwd || ""));
106
+ return String(raw[key] || "");
107
+ }
108
+
109
+ export function writeSidecarHash(cwd, hash) {
110
+ const key = resolve(String(cwd || ""));
111
+ if (!process.env.DSH_TRIVIUM_SETTINGS && isInsideDir(tmpdir(), key)) {
112
+ return readUiSettings();
113
+ }
114
+ const prev = readUiSettings();
115
+ const sidecarHashByCwd = {
116
+ ...(prev.sidecarHashByCwd && typeof prev.sidecarHashByCwd === "object" ? prev.sidecarHashByCwd : {}),
117
+ };
118
+ if (!hash) delete sidecarHashByCwd[key];
119
+ else sidecarHashByCwd[key] = String(hash);
120
+ return persistSettings({ ...prev, sidecarHashByCwd });
121
+ }
122
+
36
123
  export function applyRecallMode(target, mode) {
37
124
  const next = mode === "auto" || mode === "anchor" ? mode : "off";
38
125
  target.recallMode = next;
@@ -42,7 +129,6 @@ export function applyRecallMode(target, mode) {
42
129
  }
43
130
 
44
131
  export function writeUiSettings(patch) {
45
- mkdirSync(join(homedir(), ".dsh"), { recursive: true });
46
132
  const prev = readUiSettings();
47
133
  const next = { ...prev };
48
134
  let mode = recallModeOf(prev);
@@ -62,6 +148,12 @@ export function writeUiSettings(patch) {
62
148
  if (typeof patch.extractEnabled === "boolean") next.extractEnabled = patch.extractEnabled;
63
149
  if (typeof patch.chipsEnabled === "boolean") next.chipsEnabled = patch.chipsEnabled;
64
150
  if (typeof patch.sessionLayerEnabled === "boolean") next.sessionLayerEnabled = patch.sessionLayerEnabled;
151
+ if (typeof patch.gitSidecarEnabled === "boolean") next.gitSidecarEnabled = patch.gitSidecarEnabled;
152
+ if (typeof patch.configPanelOpen === "boolean") next.configPanelOpen = patch.configPanelOpen;
153
+ if (typeof patch.pluginLocale === "string") {
154
+ const loc = String(patch.pluginLocale || "").toLowerCase();
155
+ next.pluginLocale = loc === "zh" || loc === "en" ? loc : "host";
156
+ }
65
157
  if (typeof patch.embeddingEnabled === "boolean") next.embeddingEnabled = patch.embeddingEnabled;
66
158
  if (typeof patch.embeddingUrl === "string") {
67
159
  const url = patch.embeddingUrl.trim();
@@ -76,8 +168,14 @@ export function writeUiSettings(patch) {
76
168
  } else if (prev.pinsBySession && typeof prev.pinsBySession === "object") {
77
169
  next.pinsBySession = prev.pinsBySession;
78
170
  }
79
- writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2), "utf8");
80
- return next;
171
+ if (Array.isArray(patch.workspaces)) next.workspaces = patch.workspaces;
172
+ else if (Array.isArray(prev.workspaces)) next.workspaces = prev.workspaces;
173
+ if (patch.sidecarHashByCwd && typeof patch.sidecarHashByCwd === "object") {
174
+ next.sidecarHashByCwd = patch.sidecarHashByCwd;
175
+ } else if (prev.sidecarHashByCwd && typeof prev.sidecarHashByCwd === "object") {
176
+ next.sidecarHashByCwd = prev.sidecarHashByCwd;
177
+ }
178
+ return persistSettings(next);
81
179
  }
82
180
 
83
181
  export function pinsOf(sessionId) {
@@ -96,11 +194,10 @@ export function writePins(sessionId, ids) {
96
194
  if (!key) return nextIds;
97
195
  if (!nextIds.length) delete pinsBySession[key];
98
196
  else pinsBySession[key] = nextIds;
99
- mkdirSync(join(homedir(), ".dsh"), { recursive: true });
100
- writeFileSync(SETTINGS_FILE, JSON.stringify({ ...prev, pinsBySession }, null, 2), "utf8");
197
+ persistSettings({ ...prev, pinsBySession });
101
198
  return nextIds;
102
199
  }
103
200
 
104
201
  export function settingsFileExists() {
105
- return existsSync(SETTINGS_FILE);
202
+ return existsSync(settingsFilePath());
106
203
  }