cline-kit 0.1.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/src/detect.js ADDED
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ // Locate cline-app.exe without hardcoding anyone's install path.
3
+ const { execFileSync } = require("child_process");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+
8
+ const EXE = process.platform === "win32" ? "cline-app.exe" : "cline-app";
9
+
10
+ function fromRunningProcess() {
11
+ if (process.platform !== "win32") return null;
12
+ try {
13
+ const out = execFileSync("powershell.exe", [
14
+ "-NoProfile", "-Command",
15
+ "(Get-Process -Name cline-app -ErrorAction SilentlyContinue | Select-Object -First 1).Path"
16
+ ], { encoding: "utf8", timeout: 15000 });
17
+ const p = out.trim().replace(/^"|"$/g, "");
18
+ return p && fs.existsSync(p) ? p : null;
19
+ } catch (e) { return null; }
20
+ }
21
+
22
+ function fromRegistry() {
23
+ if (process.platform !== "win32") return null;
24
+ const roots = [
25
+ "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
26
+ "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
27
+ "HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
28
+ ];
29
+ // PowerShell treats backslash literally, so paths must be single-quoted, not JSON-encoded.
30
+ const q = (s) => "'" + String(s).replace(/'/g, "''") + "'";
31
+ const script = `$roots = @(${roots.map(q).join(",")})
32
+ foreach ($r in $roots) {
33
+ Get-ChildItem $r -ErrorAction SilentlyContinue | ForEach-Object {
34
+ $p = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
35
+ if ($p.DisplayName -match 'cline') {
36
+ foreach ($k in @('InstallLocation','DisplayIcon','UninstallString')) {
37
+ $v = $p.$k
38
+ if ($v) { Write-Output ("$k=$v") }
39
+ }
40
+ }
41
+ }
42
+ }`;
43
+ try {
44
+ const out = execFileSync("powershell.exe", ["-NoProfile", "-Command", script], { encoding: "utf8", timeout: 20000 });
45
+ let installLocation = null;
46
+ for (const line of out.split(/\r?\n/)) {
47
+ const i = line.indexOf("=");
48
+ if (i < 0) continue;
49
+ const k = line.slice(0, i), v = line.slice(i + 1).trim().replace(/^"|"$/g, "");
50
+ if (k === "InstallLocation" && v) installLocation = v;
51
+ if (k === "DisplayIcon" && /cline-app\.exe/i.test(v)) return v.split(",")[0];
52
+ if (k === "UninstallString") {
53
+ const m = v.match(/^(?:"([^"]+)"|[^\s]+)|(.+)/);
54
+ const guess = (v.match(/[A-Za-z]:[^"]*?cline-app\.exe/i) || [])[0];
55
+ if (guess) return guess;
56
+ }
57
+ }
58
+ if (installLocation) {
59
+ const p = path.join(installLocation, EXE);
60
+ if (fs.existsSync(p)) return p;
61
+ }
62
+ } catch (e) { /* registry unreadable */ }
63
+ return null;
64
+ }
65
+
66
+ function fromCommonPaths() {
67
+ const candidates = [];
68
+ const env = process.env;
69
+ if (process.platform === "win32") {
70
+ const roots = [
71
+ env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, "Programs", "Cline"),
72
+ env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, "Programs", "cline-app"),
73
+ env.PROGRAMFILES && path.join(env.PROGRAMFILES, "Cline"),
74
+ env["PROGRAMFILES(X86)"] && path.join(env["PROGRAMFILES(X86)"], "Cline"),
75
+ env.ProgramFiles && path.join(env.ProgramFiles, "cline")
76
+ ];
77
+ candidates.push(...roots.filter(Boolean).map((d) => path.join(d, EXE)));
78
+ // a few fixed-drive variants people commonly use
79
+ for (const drive of ["C:", "D:", "E:"]) {
80
+ candidates.push(path.join(drive + "\\", "Programs", "Cline", EXE));
81
+ candidates.push(path.join(drive + "\\", "Program Files", "Cline", EXE));
82
+ }
83
+ } else {
84
+ candidates.push("/Applications/Cline.app/Contents/MacOS/" + EXE);
85
+ candidates.push(path.join(os.homedir(), ".cline", "bin", EXE));
86
+ candidates.push(path.join("/usr/local/bin", EXE));
87
+ }
88
+ for (const c of candidates) { try { if (fs.existsSync(c)) return c; } catch (e) { } }
89
+ return null;
90
+ }
91
+
92
+ function fromPath() {
93
+ try {
94
+ const cmd = process.platform === "win32" ? "where.exe" : "which";
95
+ const out = execFileSync(cmd, [EXE], { encoding: "utf8", timeout: 8000 });
96
+ const p = out.split(/\r?\n/)[0].trim();
97
+ return p && fs.existsSync(p) ? p : null;
98
+ } catch (e) { return null; }
99
+ }
100
+
101
+ // ordered resolution; each source is labelled so `status` can explain the choice
102
+ function detect(cfg) {
103
+ const sources = [
104
+ ["config", () => (cfg && cfg.clinePath && fs.existsSync(cfg.clinePath) ? cfg.clinePath : null)],
105
+ ["running-process", fromRunningProcess],
106
+ ["registry", fromRegistry],
107
+ ["common-paths", fromCommonPaths],
108
+ ["path-env", fromPath]
109
+ ];
110
+ for (const [name, fn] of sources) {
111
+ let p = null;
112
+ try { p = fn(); } catch (e) { p = null; }
113
+ if (p) return { path: p, source: name };
114
+ }
115
+ return { path: null, source: null };
116
+ }
117
+
118
+ module.exports = { detect, EXE };
package/src/dict.js ADDED
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ // Dictionary loading + optional GitHub-hosted updates.
3
+ // Precedence (lowest -> highest): bundled JSON, cached remote JSON, user local override JSON.
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const cfg = require("./config");
7
+
8
+ function readJson(p) {
9
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (e) { return null; }
10
+ }
11
+
12
+ // Heuristic guard for catastrophic backtracking: a quantified group whose body already ends in a
13
+ // quantifier, or alternates between quantified atoms - (a+)+ / (a+|b)+ / (a{2,})+.
14
+ function nestedQuantifier(pattern) {
15
+ const s = String(pattern).replace(/\\./g, " "); // drop escapes: \( and \) must not confuse the scan
16
+ for (let i = 0; i < s.length - 1; i++) {
17
+ if (s[i] !== ")") continue;
18
+ const q = s[i + 1];
19
+ if (q !== "*" && q !== "+" && q !== "{") continue;
20
+ let depth = 0, j = i;
21
+ for (; j >= 0; j--) {
22
+ if (s[j] === ")") depth++;
23
+ else if (s[j] === "(") { depth--; if (depth === 0) break; }
24
+ }
25
+ if (j < 0) continue;
26
+ const body = s.slice(j + 1, i).replace(/\s+$/, "");
27
+ if (/[*+}]$/.test(body) || /\|[^|]*[*+]/.test(body)) return true;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ function bundledPath(name) {
33
+ return path.join(__dirname, "..", "dictionaries", name + ".json");
34
+ }
35
+
36
+ function validate(d) {
37
+ if (!d || typeof d !== "object") return false;
38
+ if (typeof d.version !== "number") return false;
39
+ if (!d.entries || typeof d.entries !== "object" || Array.isArray(d.entries)) return false;
40
+ if (!Array.isArray(d.rules) || !Array.isArray(d.prefixes)) return false;
41
+
42
+ // Remote dictionaries are data, but they do feed new RegExp(). Keep them anchored and small
43
+ // so a hostile or broken file cannot inject arbitrary behaviour or a pathological regex.
44
+ const keys = Object.keys(d.entries);
45
+ if (keys.length > 20000) return false;
46
+ for (const k of keys) {
47
+ if (typeof k !== "string" || k.length > 400) return false;
48
+ if (typeof d.entries[k] !== "string" || d.entries[k].length > 400) return false;
49
+ }
50
+ for (const r of d.rules) {
51
+ if (!r || typeof r.pattern !== "string" || typeof r.out !== "string") return false;
52
+ if (r.pattern.length > 200 || r.out.length > 200) return false;
53
+ if (r.pattern[0] !== "^" || r.pattern[r.pattern.length - 1] !== "$") return false;
54
+ // Patterns reach new RegExp() in the webview, so a nested quantifier such as (a+)+ is a
55
+ // hang risk, not just a style problem. Reject the shape before it is ever compiled.
56
+ if (nestedQuantifier(r.pattern)) return false;
57
+ try { new RegExp(r.pattern); } catch (e) { return false; }
58
+ }
59
+ for (const p of d.prefixes) {
60
+ if (!p || typeof p.from !== "string" || typeof p.to !== "string") return false;
61
+ if (p.from.length > 100 || p.to.length > 100) return false;
62
+ }
63
+ // featureText is optional, but when present it must be id -> key -> string
64
+ if (d.featureText !== undefined) {
65
+ if (!d.featureText || typeof d.featureText !== "object" || Array.isArray(d.featureText)) return false;
66
+ for (const fid of Object.keys(d.featureText)) {
67
+ const block = d.featureText[fid];
68
+ if (!block || typeof block !== "object" || Array.isArray(block)) return false;
69
+ for (const k of Object.keys(block)) {
70
+ if (typeof block[k] !== "string" || block[k].length > 400) return false;
71
+ }
72
+ if (Object.keys(block).length > 200) return false;
73
+ }
74
+ }
75
+ return true;
76
+ }
77
+
78
+ function merge(base, extra) {
79
+ if (!extra) return base;
80
+ const featureText = Object.assign({}, base.featureText || {}, extra.featureText || {});
81
+ for (const id of Object.keys(featureText)) {
82
+ featureText[id] = Object.assign({}, (base.featureText || {})[id] || {}, (extra.featureText || {})[id] || {});
83
+ }
84
+ const out = {
85
+ version: Math.max(base.version || 0, extra.version || 0),
86
+ language: base.language,
87
+ clineVersion: extra.clineVersion || base.clineVersion,
88
+ updated: extra.updated || base.updated,
89
+ entries: Object.assign({}, base.entries, extra.entries),
90
+ prefixes: (base.prefixes || []).concat((extra.prefixes || []).filter((p) => !(base.prefixes || []).some((b) => b.from === p.from))),
91
+ rules: (base.rules || []).concat((extra.rules || []).filter((r) => !(base.rules || []).some((b) => b.pattern === r.pattern)))
92
+ };
93
+ if (Object.keys(featureText).length) out.featureText = featureText;
94
+ return out;
95
+ }
96
+
97
+ function load(cfgObj) {
98
+ const name = cfgObj.dictionary || "zh-CN";
99
+ let dict = readJson(bundledPath(name));
100
+ if (!dict) throw new Error("bundled dictionary missing: " + name);
101
+ const cached = readJson(cfg.cachedDictFile(name));
102
+ if (cached && validate(cached) && cached.version > dict.version) dict = merge(dict, cached);
103
+ const local = readJson(cfg.localDictFile(name));
104
+ if (local && local.entries) dict = merge(dict, local);
105
+ dict.sourceVersions = {
106
+ bundled: readJson(bundledPath(name))?.version || 0,
107
+ cached: cached?.version || 0,
108
+ local: local?.version || 0
109
+ };
110
+ return dict;
111
+ }
112
+
113
+ async function fetchRemote(url, timeoutMs) {
114
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs || 12000), cache: "no-store" });
115
+ if (!res.ok) throw new Error("HTTP " + res.status);
116
+ return await res.json();
117
+ }
118
+
119
+ // The configured URL points at the reference dictionary; every locale is a sibling file, so swap
120
+ // the file name rather than asking users to configure one URL per language.
121
+ function remoteUrlFor(cfgObj) {
122
+ const name = cfgObj.dictionary || "zh-CN";
123
+ const url = cfgObj.updateUrl || "";
124
+ return /CHANGE_ME/.test(url) ? url : url.replace(/zh-CN\.json(\?.*)?$/i, name + ".json$1");
125
+ }
126
+
127
+ // returns { updated, from, to, error }
128
+ async function update(cfgObj, opts) {
129
+ opts = opts || {};
130
+ const now = Date.now();
131
+ if (!opts.force && !cfgObj.autoUpdate) return { updated: false, reason: "autoUpdate disabled" };
132
+ if (!opts.force && (now - (cfgObj.lastUpdateCheck || 0)) < (cfgObj.updateIntervalMs || 86400000)) {
133
+ return { updated: false, reason: "not due yet" };
134
+ }
135
+ const url = remoteUrlFor(cfgObj);
136
+ if (!url || /CHANGE_ME/.test(url)) {
137
+ return { updated: false, reason: "no update url configured" };
138
+ }
139
+ cfgObj.lastUpdateCheck = now;
140
+ const name = cfgObj.dictionary || "zh-CN";
141
+ try {
142
+ const remote = await fetchRemote(url);
143
+ if (!validate(remote)) return { updated: false, error: "remote dictionary failed validation" };
144
+ if (remote.language && remote.language !== name) {
145
+ // a 404 page or a mis-pointed URL must not overwrite the locale the user selected
146
+ return { updated: false, error: "remote dictionary is for " + remote.language + ", not " + name };
147
+ }
148
+ const current = load(cfgObj);
149
+ cfgObj.remoteVersion = remote.version;
150
+ cfg.write(cfgObj);
151
+ if (remote.version <= current.version && !opts.force) {
152
+ return { updated: false, from: current.version, to: remote.version, reason: "already current" };
153
+ }
154
+ cfg.ensureDirs();
155
+ fs.writeFileSync(cfg.cachedDictFile(name), JSON.stringify(remote, null, 1), "utf8");
156
+ return { updated: true, from: current.version, to: remote.version };
157
+ } catch (e) {
158
+ cfg.write(cfgObj);
159
+ return { updated: false, error: e.message };
160
+ }
161
+ }
162
+
163
+ function available() {
164
+ const dir = path.join(__dirname, "..", "dictionaries");
165
+ try {
166
+ return fs.readdirSync(dir).filter((f) => /^[A-Za-z]{2,3}(-[A-Za-z]{2,4})?\.json$/.test(f))
167
+ .map((f) => f.replace(/\.json$/, ""));
168
+ } catch (e) { return []; }
169
+ }
170
+
171
+ module.exports = { load, update, validate, bundledPath, nestedQuantifier, remoteUrlFor, available };
package/src/doctor.js ADDED
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ // doctor.js - one command that answers "is the overlay actually live in the window right now?".
3
+ // Everything else in the tool reports what it *intends*; this reports what the webview *has*,
4
+ // so a Cline update that changes the DOM shows up as a mismatch instead of silent absence.
5
+ const fs = require("fs");
6
+ const cfg = require("./config");
7
+ const cdp = require("./cdp");
8
+ const payload = require("./payload");
9
+ const launcher = require("./launcher");
10
+ const detect = require("./detect");
11
+
12
+ // Read-only probe. Runs inside the page; returns plain data, never touches the DOM.
13
+ const PROBE = `(function () {
14
+ var st = window.__clineKitFeatureState || {};
15
+ var s = window.__ckitStats || { scans: 0, writes: 0, restored: 0 };
16
+ var out = { engineBuild: window.__ckitEngineBuild || null, stats: { scans: s.scans, writes: s.writes, restored: s.restored }, features: {} };
17
+ var ids = Object.keys(st).filter(function (k) { return /_build$/.test(k); });
18
+ ids.forEach(function (k) {
19
+ var id = k.slice(0, -6);
20
+ out.features[id] = { build: String(st[k]), stats: st[id + "_stats"] || null };
21
+ });
22
+ var rows = document.querySelectorAll("[data-ckit-feat]");
23
+ out.domRows = rows.length;
24
+ out.title = document.title || "";
25
+ return JSON.stringify(out);
26
+ })()`;
27
+
28
+ function evaluate(api, expression) {
29
+ return api.rpc("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: false })
30
+ .then((r) => (r && r.result && r.result.value !== undefined ? r.result.value : null));
31
+ }
32
+
33
+ async function run() {
34
+ const conf = cfg.read();
35
+ const found = detect.detect(conf);
36
+ const report = { ok: true, checks: [] };
37
+ const add = (name, pass, detail) => {
38
+ report.checks.push({ name, pass: !!pass, detail: detail || "" });
39
+ if (!pass) report.ok = false;
40
+ };
41
+
42
+ add("cline executable", !!found.path, (found.path || "not found") + " (via " + found.source + ")");
43
+
44
+ const port = conf.port;
45
+ const alive = port ? await launcher.isPortAlive(port) : false;
46
+ add("debug port", !!alive, port ? "127.0.0.1:" + port + (alive ? " responding" : " NOT responding") : "no port in config - run ckit start");
47
+ if (!alive) {
48
+ add("injector", !!launcher.injectorRunning(), launcher.injectorRunning() ? "pid " + launcher.injectorRunning() : "not running");
49
+ report.hint = alive ? "" :
50
+ "Cline has to be started by cline-kit (it sets WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS). " +
51
+ "If Cline is open but has no debug port, run: ckit start --restart";
52
+ return report;
53
+ }
54
+
55
+ const current = payload.compose(conf).version;
56
+ const installed = require("./injector").readActiveVersion();
57
+ const fresh = installed === current;
58
+ add("payload version", fresh, (fresh ? "in page " + current : "installed " + (installed || "?") + " but the source builds " + current +
59
+ " - run: ckit start (it restarts an injector whose code or dictionary went stale underneath it)"));
60
+
61
+ const pages = await cdp.eachPage(port, async (api) => {
62
+ const raw = await evaluate(api, PROBE);
63
+ try { return JSON.parse(raw); } catch (e) { return null; }
64
+ });
65
+ const live = pages.map((p) => p.result).filter(Boolean);
66
+ add("page reachable", live.length > 0, live.length + " of " + pages.length + " webview target(s) answered");
67
+
68
+ const info = payload.info(conf);
69
+ for (const f of info.features) {
70
+ if (!f.enabled) continue;
71
+ const seen = live.map((p) => p.features[f.id]).filter(Boolean);
72
+ add("feature " + f.id, seen.length > 0, seen.length
73
+ ? seen.map((s) => "build " + s.build + (s.stats ? ", " + s.stats.rows + " row(s) added of " + s.stats.registered + " registered, " + s.stats.nativeGroups + " native" : "")).join(" | ")
74
+ : "not present in any page - Cline's DOM may have changed");
75
+ }
76
+
77
+ // Idle-page write rate: an overlay that keeps rewriting nodes is chasing its own mutations, which
78
+ // freezes the webview long before anything looks wrong on screen.
79
+ const writesBefore = live.map((p) => p.stats && p.stats.writes).filter((v) => typeof v === "number");
80
+ await new Promise((r) => setTimeout(r, 3000));
81
+ const again = (await cdp.eachPage(port, async (api) => {
82
+ const raw = await evaluate(api, PROBE);
83
+ try { return JSON.parse(raw); } catch (e) { return null; }
84
+ })).map((p) => p.result).filter(Boolean);
85
+ const writesAfter = again.map((p) => p.stats && p.stats.writes).filter((v) => typeof v === "number");
86
+ const delta = writesBefore.length && writesAfter.length ? writesAfter[0] - writesBefore[0] : 0;
87
+ add("overlay write rate", delta < 300, delta + " DOM writes over 3 s on an idle page" +
88
+ (delta >= 300 ? " - the overlay looks to be reacting to its own mutations" : ""));
89
+
90
+ const eng = live.map((p) => p.engineBuild).filter(Boolean);
91
+ add("locale engine", eng.length > 0, eng.length ? "build " + eng[0] + ", dictionary v" + info.dictionaryVersion : "not installed");
92
+ add("dictionary", info.entries > 0, info.entries + " entries, " + info.rules + " rules"
93
+ + " (bundled " + info.sources.bundled + ", cached " + info.sources.cached +
94
+ ", local " + info.sources.local + ")");
95
+
96
+ // Once install() repoints a shortcut its target is wscript.exe, so the exe scan finds nothing;
97
+ // the list recorded at install time is what tells us the wiring exists.
98
+ const wired = (conf.originalShortcuts || []).filter((s) => s && s.lnk);
99
+ if (wired.length) {
100
+ const missing = wired.filter((s) => !fs.existsSync(s.lnk));
101
+ add("shortcuts", missing.length === 0, missing.length
102
+ ? missing.length + " of " + wired.length + " recorded shortcut(s) no longer exist (Cline was reinstalled?) - run ckit install"
103
+ : wired.length + " shortcut(s) go through the cline-kit launcher");
104
+ } else {
105
+ add("shortcuts", true, "not repointed - run ckit install to launch Cline through the overlay");
106
+ }
107
+ return report;
108
+ }
109
+
110
+ function format(r) {
111
+ const lines = r.checks.map((c) => (c.pass ? " ok " : " FAIL ") + c.name + (c.detail ? " - " + c.detail : ""));
112
+ if (r.hint) lines.push(" -> " + r.hint);
113
+ return (r.ok ? "Cline-kit looks healthy\n" : "Cline-kit needs attention\n") + lines.join("\n");
114
+ }
115
+
116
+ module.exports = { run, format, PROBE };
117
+
118
+ if (require.main === module) {
119
+ run().then((r) => { console.log(format(r)); process.exitCode = r.ok ? 0 : 1; })
120
+ .catch((e) => { console.error("doctor: " + e.message); process.exit(1); });
121
+ }
package/src/engine.js ADDED
@@ -0,0 +1,148 @@
1
+ /* cline-kit overlay engine (browser side).
2
+ * Runs inside the Cline webview. Expects a `DICT` const in scope:
3
+ * { version, entries: {en: zh}, prefixes: [{from,to}], rules: [{pattern,out}] }
4
+ * No dependencies, no network, no eval of remote code beyond this data.
5
+ */
6
+ (function () {
7
+ var VER = (DICT && DICT.version) || 1;
8
+ // Guard on the engine build *and* the dictionary version. Engine-only edits must re-run, and so
9
+ // must a locale switch or `ckit update`: those change DICT while this file stays byte-identical.
10
+ var BUILD = String((DICT && DICT.engineBuild) || "v" + VER) + "/d" + VER + "/" + (DICT && DICT.language);
11
+ if (window.__ckitEngineBuild === BUILD) return;
12
+ if (window.__ckitObserver) { try { window.__ckitObserver.disconnect(); } catch (e) { } }
13
+ if (window.__ckitTimer) { clearInterval(window.__ckitTimer); }
14
+ window.__ckitEngineBuild = BUILD;
15
+ window.__ckitEngineVersion = VER;
16
+
17
+ var ENTRIES = DICT.entries || {};
18
+ var PREFIXES = DICT.prefixes || [];
19
+ var RULES = (DICT.rules || []).map(function (r) {
20
+ try { return { re: new RegExp(r.pattern), out: r.out }; } catch (e) { return null; }
21
+ }).filter(Boolean);
22
+
23
+ function expand(str, m) {
24
+ return str.replace(/\$(\d)/g, function (_, n) { return m[+n] === undefined ? "" : m[+n]; });
25
+ }
26
+
27
+ function lookup(s) {
28
+ if (!s) return null;
29
+ var hit = ENTRIES[s];
30
+ if (hit) return hit;
31
+ for (var i = 0; i < RULES.length; i++) {
32
+ var m = s.match(RULES[i].re);
33
+ if (m) {
34
+ var r = expand(RULES[i].out, m);
35
+ if (r && r !== s) return r;
36
+ }
37
+ }
38
+ for (var j = 0; j < PREFIXES.length; j++) {
39
+ var from = PREFIXES[j].from, to = PREFIXES[j].to;
40
+ if (s.length > from.length && s.slice(0, from.length) === from) return to + s.slice(from.length);
41
+ }
42
+ return null;
43
+ }
44
+
45
+ var ATTRS = (DICT.attributes || ["placeholder", "aria-label", "title"]);
46
+ var SKIP = { SCRIPT: 1, STYLE: 1, TEXTAREA: 1, CODE: 1, PRE: 1, SVG: 1 };
47
+ var CJK = /[一-鿿]/;
48
+ var LATIN = /[A-Za-z]{3}/;
49
+
50
+ // Write counter: an overlay that keeps rewriting the same nodes is chasing its own mutations,
51
+ // which shows up as a frozen webview long before it shows up as a visual bug. `ckit doctor`
52
+ // samples this twice and fails when an idle page keeps taking writes.
53
+ var STATS = window.__ckitStats = { scans: 0, writes: 0, restored: 0 };
54
+
55
+ function norm(s) { return String(s || "").replace(/\s+/g, " ").trim(); }
56
+
57
+ // Text we replaced is remembered on the node, so switching locale (or pulling a newer dictionary)
58
+ // can start from the original English instead of from last run's output.
59
+ function translateText(node) {
60
+ var raw = node.nodeValue;
61
+ if (!raw || raw.length > 800) return;
62
+ var el = node.parentElement;
63
+ if (el && SKIP[el.tagName]) return;
64
+ var src = raw;
65
+ if (node.__ckitOut != null) {
66
+ src = norm(raw) === norm(node.__ckitOut) ? node.__ckitSrc : raw; // the app rewrote it: stale
67
+ }
68
+ var key = norm(src);
69
+ if (key.length < 2) return;
70
+ if (node.__ckitSrc == null && (CJK.test(key) || !LATIN.test(key))) return;
71
+ var rep = lookup(key);
72
+ var lead = raw.match(/^\s*/)[0];
73
+ var trail = raw.match(/\s*$/)[0];
74
+ if (!rep || rep === src) {
75
+ if (node.__ckitSrc != null) { node.nodeValue = lead + node.__ckitSrc + trail; node.__ckitSrc = null; node.__ckitOut = null; STATS.restored++; }
76
+ return;
77
+ }
78
+ node.__ckitSrc = src;
79
+ node.__ckitOut = rep;
80
+ // Never write an identical value: our own mutation would wake the observer again and the two
81
+ // would chase each other forever (that is a frozen webview, not a slow one).
82
+ var next = lead + rep + trail;
83
+ if (next !== raw) { node.nodeValue = next; STATS.writes++; }
84
+ }
85
+
86
+ function translateEl(el) {
87
+ if (!el || el.nodeType !== 1 || !el.getAttribute) return;
88
+ var keep = el.__ckitAttrSrc || (el.__ckitAttrSrc = {});
89
+ for (var i = 0; i < ATTRS.length; i++) {
90
+ var a = ATTRS[i];
91
+ var v = el.getAttribute(a);
92
+ if (!v && keep[a] == null) continue;
93
+ var src = keep[a] != null && norm(v) === norm(keep[a].out) ? keep[a].src : v;
94
+ var rep = lookup(norm(src));
95
+ if (!rep || rep === src) {
96
+ if (keep[a]) { el.setAttribute(a, keep[a].src); delete keep[a]; STATS.restored++; }
97
+ continue;
98
+ }
99
+ keep[a] = { src: src, out: rep };
100
+ if (rep !== v) { el.setAttribute(a, rep); STATS.writes++; }
101
+ }
102
+ }
103
+
104
+ function scan(root) {
105
+ var el = root && root.nodeType ? root : document.documentElement;
106
+ if (!el) return;
107
+ try {
108
+ STATS.scans++;
109
+ var w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, null, false);
110
+ var n;
111
+ while ((n = w.nextNode())) {
112
+ if (n.nodeType === 3) translateText(n);
113
+ else translateEl(n);
114
+ }
115
+ } catch (e) { /* DOM raced away; next pass will catch it */ }
116
+ }
117
+
118
+ var queued = false;
119
+ function schedule() {
120
+ if (queued) return;
121
+ queued = true;
122
+ var run = function () { queued = false; scan(document.documentElement); };
123
+ (window.requestAnimationFrame || setTimeout)(run, 16);
124
+ }
125
+
126
+ if (!document.documentElement) return;
127
+ scan(document.documentElement);
128
+
129
+ window.__ckitObserver = new MutationObserver(function (muts) {
130
+ for (var i = 0; i < muts.length; i++) {
131
+ var m = muts[i];
132
+ if (m.type === "characterData") { translateText(m.target); continue; }
133
+ if (m.type === "attributes") { translateEl(m.target); continue; }
134
+ for (var j = 0; j < m.addedNodes.length; j++) scan(m.addedNodes[j]);
135
+ }
136
+ schedule();
137
+ });
138
+ window.__ckitObserver.observe(document.documentElement, {
139
+ childList: true, subtree: true, characterData: true,
140
+ attributes: true, attributeFilter: ATTRS
141
+ });
142
+
143
+ // safety net for portals/menus mounted outside the observed subtree
144
+ window.__ckitTimer = setInterval(function () { scan(document.documentElement); }, 1200);
145
+
146
+ window.__zhUIStats = { version: VER, entries: Object.keys(ENTRIES).length };
147
+ try { console.log("[cline-kit] overlay v" + VER + " loaded (" + Object.keys(ENTRIES).length + " entries)"); } catch (e) { }
148
+ })();
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ // Feature registry. Each feature is a self-contained browser script in this folder.
3
+ // Versions are read from the scripts themselves so the registry cannot drift.
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const DEFS = [
8
+ {
9
+ id: "sidebar-groups",
10
+ // shared logic first, then the browser script that consumes window.__ckitSidebarLogic
11
+ parts: ["sidebar-groups.logic.js", "sidebar-groups.js"],
12
+ title: "Keep every registered project in the sidebar / 侧边栏全项目常驻",
13
+ defaultOn: true,
14
+ // config handed to the browser script; keep it JSON-serialisable
15
+ build(ctx) {
16
+ return {
17
+ installDir: ctx.installDir || "",
18
+ hide: ctx.cfg.featureHide || [],
19
+ maxRows: 80,
20
+ groupMode: true,
21
+ // leave empty to follow Cline's own cline.code.workspace-selection.vN key
22
+ storageKey: ctx.cfg.storageKey || "",
23
+ text: (ctx.featureText || {})[this.id] || {}
24
+ };
25
+ }
26
+ }
27
+ ];
28
+
29
+ function scriptVersion(src) {
30
+ const m = src.match(/var\s+VER\s*=\s*(\d+)/) || src.match(/const\s+VER\s*=\s*(\d+)/);
31
+ return m ? Number(m[1]) : 1;
32
+ }
33
+
34
+ function filesOf(def) { return def.parts || [def.file]; }
35
+
36
+ function readSource(def) {
37
+ const list = filesOf(def);
38
+ const src = list.map((f) => fs.readFileSync(path.join(__dirname, f), "utf8")).join("\n");
39
+ const main = fs.readFileSync(path.join(__dirname, list[list.length - 1]), "utf8");
40
+ return { src, version: scriptVersion(main), path: path.join(__dirname, list[0]) };
41
+ }
42
+
43
+ function list() {
44
+ return DEFS.map((d) => {
45
+ let version = 0;
46
+ try { version = readSource(d).version; } catch (e) { version = -1; }
47
+ return { id: d.id, title: d.title, version, defaultOn: d.defaultOn };
48
+ });
49
+ }
50
+
51
+ function enabledSet(cfgObj) {
52
+ const out = {};
53
+ for (const d of DEFS) out[d.id] = d.defaultOn;
54
+ const fromCfg = cfgObj.features || {};
55
+ for (const k of Object.keys(fromCfg)) {
56
+ if (k in out) out[k] = !!fromCfg[k];
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function resolve(cfgObj, dictObj) {
62
+ const on = enabledSet(cfgObj);
63
+ const ctx = {
64
+ cfg: cfgObj,
65
+ installDir: cfgObj.clinePath ? path.dirname(cfgObj.clinePath) : "",
66
+ featureText: (dictObj && dictObj.featureText) || {}
67
+ };
68
+ const picked = [];
69
+ for (const d of DEFS) {
70
+ if (!on[d.id]) continue;
71
+ let entry;
72
+ try {
73
+ entry = readSource(d);
74
+ } catch (e) {
75
+ entry = null;
76
+ }
77
+ if (!entry) continue;
78
+ picked.push({ id: d.id, version: entry.version, source: entry.src, config: d.build(ctx) });
79
+ }
80
+ return { picked, enabled: on };
81
+ }
82
+
83
+ module.exports = { list, resolve, enabledSet, DEFS };