@operatingtag/dsh-unclesam 0.0.0-stage → 1.0.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 (67) hide show
  1. package/CHANGELOG.md +893 -0
  2. package/LICENSE +142 -0
  3. package/README.md +354 -2
  4. package/client.js +1099 -0
  5. package/cordis.patch.yml +48 -0
  6. package/icon.svg +17 -0
  7. package/index.js +1411 -0
  8. package/install.cmd +43 -0
  9. package/install.ps1 +311 -0
  10. package/lib/cdp.mjs +247 -0
  11. package/lib/engine.mjs +1412 -0
  12. package/lib/i18n.mjs +141 -0
  13. package/lib/prefs.mjs +101 -0
  14. package/lib/prompt.mjs +518 -0
  15. package/lib/savings.mjs +131 -0
  16. package/locale/ar.json +98 -0
  17. package/locale/az.json +98 -0
  18. package/locale/bg.json +98 -0
  19. package/locale/bn.json +98 -0
  20. package/locale/cs.json +98 -0
  21. package/locale/da.json +98 -0
  22. package/locale/de.json +98 -0
  23. package/locale/el.json +98 -0
  24. package/locale/en.json +98 -0
  25. package/locale/es.json +98 -0
  26. package/locale/fa.json +98 -0
  27. package/locale/fi.json +98 -0
  28. package/locale/fr.json +98 -0
  29. package/locale/he.json +98 -0
  30. package/locale/hi.json +98 -0
  31. package/locale/hr.json +98 -0
  32. package/locale/hu.json +98 -0
  33. package/locale/id.json +98 -0
  34. package/locale/it.json +98 -0
  35. package/locale/ja.json +98 -0
  36. package/locale/ka.json +98 -0
  37. package/locale/kk.json +98 -0
  38. package/locale/km.json +98 -0
  39. package/locale/ko.json +98 -0
  40. package/locale/lt.json +98 -0
  41. package/locale/mr.json +98 -0
  42. package/locale/ms.json +98 -0
  43. package/locale/my.json +98 -0
  44. package/locale/ne.json +98 -0
  45. package/locale/nl.json +98 -0
  46. package/locale/no.json +98 -0
  47. package/locale/pl.json +98 -0
  48. package/locale/pt.json +98 -0
  49. package/locale/ro.json +98 -0
  50. package/locale/ru.json +98 -0
  51. package/locale/si.json +98 -0
  52. package/locale/sk.json +98 -0
  53. package/locale/sl.json +98 -0
  54. package/locale/sr.json +98 -0
  55. package/locale/sv.json +98 -0
  56. package/locale/sw.json +98 -0
  57. package/locale/ta.json +98 -0
  58. package/locale/te.json +98 -0
  59. package/locale/th.json +98 -0
  60. package/locale/tl.json +98 -0
  61. package/locale/tr.json +98 -0
  62. package/locale/uk.json +98 -0
  63. package/locale/ur.json +98 -0
  64. package/locale/uz.json +98 -0
  65. package/locale/vi.json +98 -0
  66. package/locale/zh.json +98 -0
  67. package/package.json +73 -4
package/lib/i18n.mjs ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Locale loading: which language the panel speaks, and how it falls back.
3
+ *
4
+ * Copyright (c) 2026 Operating Tag. All rights reserved. Released under the CC BY-NC-ND 4.0 licence.
5
+ *
6
+ * @file i18n.mjs
7
+ * @version 3.9.11
8
+ * @author Operating Tag (@operatingtag)
9
+ * @license CC-BY-NC-ND-4.0
10
+ *
11
+ * WHY THIS IS BUILT THE WAY IT IS
12
+ * A person who cannot read English cannot be sold anything in English, so the panel has to speak their
13
+ * language. Three rules follow, and they are the whole design:
14
+ *
15
+ * 1. Ask the harness which language it is in, rather than guessing from an IP address. The harness
16
+ * already knows: it ships its own locale files and has a locale service. An IP lookup would be wrong
17
+ * for anyone on a VPN, wrong for someone living abroad, and would put a network call inside a plugin
18
+ * that promises nothing leaves the machine.
19
+ *
20
+ * 2. Fall back, never fail. A missing language file, a missing key, a file that will not parse - each
21
+ * degrades to English for that one string. The panel must never render a key name or an empty box.
22
+ *
23
+ * 3. Let the reader choose, always. Detection is a good default and a bad decision. A Spanish speaker
24
+ * who prefers English gets English, and the choice sticks.
25
+ *
26
+ * WHAT IS NOT HERE
27
+ * No network requests. Every language is a file in this package, read from disk at load time.
28
+ */
29
+
30
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
31
+ import { join } from 'node:path';
32
+
33
+ /** Shown when a key is absent everywhere. Deliberately visible: it is a bug, not a translation gap. */
34
+ const MISSING = '…';
35
+
36
+ /**
37
+ * Fill %s placeholders in order.
38
+ *
39
+ * Values are inserted as plain text, so a translation can move a placeholder anywhere in the sentence -
40
+ * which many languages require - without any parsing on this side.
41
+ */
42
+ function fill(template, args) {
43
+ let i = 0;
44
+ return String(template).replace(/%s/g, () => (i < args.length ? String(args[i++]) : '%s'));
45
+ }
46
+
47
+ export class Locales {
48
+ #dir;
49
+ #tables = new Map();
50
+ #english;
51
+
52
+ /**
53
+ * @param {string} dir folder holding <code>.json files
54
+ * @param {string} forced a language the reader picked; empty means follow the harness
55
+ */
56
+ constructor(dir, forced = '') {
57
+ this.#dir = dir;
58
+ this.#forced = String(forced || '').trim().toLowerCase();
59
+ this.#english = this.#load('en') ?? {};
60
+ }
61
+
62
+ #forced = '';
63
+
64
+ /** Read one language file. Absolute path or a code; anything unreadable is simply absent. */
65
+ #load(code) {
66
+ const clean = String(code || '').trim().toLowerCase().replace(/[^a-z-]/g, '');
67
+ if (!clean) return null;
68
+ if (this.#tables.has(clean)) return this.#tables.get(clean);
69
+
70
+ // `zh-CN` and `zh` are the same file; try the exact code, then the language part alone.
71
+ const candidates = [clean];
72
+ const base = clean.split('-')[0];
73
+ if (base !== clean) candidates.push(base);
74
+
75
+ for (const candidate of candidates) {
76
+ const file = join(this.#dir, `${candidate}.json`);
77
+ if (!existsSync(file)) continue;
78
+ try {
79
+ const table = JSON.parse(readFileSync(file, 'utf8'));
80
+ this.#tables.set(clean, table);
81
+ return table;
82
+ } catch {
83
+ // A broken file must not take the panel down with it.
84
+ continue;
85
+ }
86
+ }
87
+ this.#tables.set(clean, null);
88
+ return null;
89
+ }
90
+
91
+ /** Every language this package can speak, in menu order. */
92
+ available() {
93
+ const out = [];
94
+ try {
95
+ for (const name of readdirSync(this.#dir)) {
96
+ const m = /^([a-z]{2}(?:-[a-z]{2})?)\.json$/i.exec(name);
97
+ if (!m) continue;
98
+ const table = this.#load(m[1]);
99
+ out.push({ code: m[1].toLowerCase(), name: table?.languageName || m[1] });
100
+ }
101
+ } catch {
102
+ // A missing folder just means English only.
103
+ }
104
+ if (!out.some((entry) => entry.code === 'en')) out.unshift({ code: 'en', name: 'English' });
105
+ return out.sort((a, b) => (a.code === 'en' ? -1 : b.code === 'en' ? 1 : a.name.localeCompare(b.name)));
106
+ }
107
+
108
+ /**
109
+ * The translator for one language.
110
+ *
111
+ * `wanted` is whatever the harness reported. When the reader has picked a language, that wins; when the
112
+ * picked language is missing, the harness value is tried before English.
113
+ */
114
+ pick(wanted) {
115
+ const table =
116
+ this.#load(this.#forced) ?? this.#load(wanted) ?? this.#english;
117
+
118
+ /*
119
+ * The language's own name, for telling the model what to answer in.
120
+ *
121
+ * `languageName` is written in that language - "Español", "日本語", "فارسی" - which is right in the
122
+ * selector and wrong in an instruction, because the web model reads the instruction in English. So the
123
+ * English name is sent instead: "Spanish", "Japanese", "Persian". A model told "answer in Español"
124
+ * usually works it out, but "answer in Spanish" is unambiguous and costs nothing.
125
+ */
126
+ const englishName = table?.languageNameEn || (table === this.#english ? 'English' : '');
127
+
128
+ const t = (key, ...args) => {
129
+ const value = table?.[key] ?? this.#english?.[key];
130
+ if (value === undefined || value === null) return MISSING;
131
+ return args.length ? fill(value, args) : value;
132
+ };
133
+
134
+ t.meta = (key) => table?.meta?.[key] ?? this.#english?.meta?.[key] ?? MISSING;
135
+ t.languageName = table?.languageName || 'English';
136
+ t.languageNameInEnglish = englishName || t.languageName;
137
+ t.code = table && table !== this.#english ? Object.keys(this.#tables).find((k) => this.#tables.get(k) === table) : 'en';
138
+ t.available = () => this.available();
139
+ return t;
140
+ }
141
+ }
package/lib/prefs.mjs ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Durable bits of the plugin's own state, kept in the harness profile.
3
+ *
4
+ * Copyright (c) 2026 Operating Tag. All rights reserved. Released under the CC BY-NC-ND 4.0 licence.
5
+ *
6
+ * @file prefs.mjs
7
+ * @version 3.9.11
8
+ * @author Operating Tag (@operatingtag)
9
+ * @license CC-BY-NC-ND-4.0
10
+ *
11
+ * WHY THIS EXISTS
12
+ * The reader's language choice lived in a plain variable. Restart the harness and it was gone - the
13
+ * panel silently went back to English, and because the page is only told the language when a choice
14
+ * exists, answers came back in English too. Reported exactly that way: "I started harness and it was
15
+ * auto-set to english."
16
+ *
17
+ * A choice a person made should outlive a restart. Two properties matter more than features here:
18
+ *
19
+ * * A failure to read or write must never break the plugin. A missing file, a corrupt file, or a
20
+ * read-only profile all fall back to the defaults rather than throwing.
21
+ * * The file must never be mistaken for the truth when it is absent. `get` returns undefined for a
22
+ * key it has never been told, so the caller can tell "not chosen" from "chosen and empty".
23
+ */
24
+
25
+ import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
26
+ import { dirname, join } from 'node:path';
27
+
28
+ /** Where a plugin may keep its own small state, given the harness profile directory. */
29
+ export function prefsPath(profileDir) {
30
+ return join(profileDir, 'storages', 'deepseek-web-chat.json');
31
+ }
32
+
33
+ export class Prefs {
34
+ #file;
35
+ #data;
36
+
37
+ /**
38
+ * @param {string} file absolute path to the JSON file
39
+ */
40
+ constructor(file) {
41
+ this.#file = file;
42
+ this.#data = this.#read();
43
+ }
44
+
45
+ #read() {
46
+ try {
47
+ const parsed = JSON.parse(readFileSync(this.#file, 'utf8'));
48
+ return parsed && typeof parsed === 'object' ? parsed : {};
49
+ } catch {
50
+ // Absent, unreadable or corrupt: defaults. Never throw on startup for this.
51
+ return {};
52
+ }
53
+ }
54
+
55
+ /** The stored value, or undefined when it was never set - never a guess. */
56
+ get(key) {
57
+ return Object.prototype.hasOwnProperty.call(this.#data, key) ? this.#data[key] : undefined;
58
+ }
59
+
60
+ /**
61
+ * Store one value and write it immediately.
62
+ *
63
+ * Written through a temporary file and renamed, because a half-written JSON file is worse than no file:
64
+ * the next start would silently fall back to defaults and the choice would vanish again, which is the
65
+ * bug this whole module exists to remove.
66
+ */
67
+ set(key, value) {
68
+ if (value === undefined || value === null || value === '') delete this.#data[key];
69
+ else this.#data[key] = value;
70
+ this.#flush();
71
+ return value;
72
+ }
73
+
74
+ #flush() {
75
+ try {
76
+ mkdirSync(dirname(this.#file), { recursive: true });
77
+ const tmp = `${this.#file}.tmp`;
78
+ writeFileSync(tmp, `${JSON.stringify(this.#data, null, 2)}\n`, 'utf8');
79
+ renameSync(tmp, this.#file);
80
+ return true;
81
+ } catch {
82
+ // A read-only profile must not break a working plugin; the value lives on in memory for this run.
83
+ return false;
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * The harness profile directory, from whatever the plugin can see.
90
+ *
91
+ * `DSH_PROFILE_DIR` is honoured first because it is explicit. Failing that the usual location is used,
92
+ * and failing that the plugin runs unpersisted rather than not at all.
93
+ */
94
+ export function profileDir() {
95
+ const fromEnv = process.env.DSH_PROFILE_DIR;
96
+ if (fromEnv) return fromEnv;
97
+ const home = process.env.USERPROFILE || process.env.HOME || '';
98
+ if (!home) return null;
99
+ const name = process.env.DSH_PROFILE || 'desktop';
100
+ return join(home, '.dsh', 'profiles', name);
101
+ }