pinokiod 7.3.0 → 7.3.3

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 (125) hide show
  1. package/kernel/api/github/index.js +444 -0
  2. package/kernel/api/index.js +199 -11
  3. package/kernel/api/process/index.js +124 -44
  4. package/kernel/api/shell_run_template.js +273 -0
  5. package/kernel/api/uri/index.js +51 -0
  6. package/kernel/bin/{conda-python.js → conda-pins.js} +23 -0
  7. package/kernel/bin/conda.js +15 -5
  8. package/kernel/bin/git.js +9 -10
  9. package/kernel/bin/huggingface.js +1 -1
  10. package/kernel/bin/index.js +5 -2
  11. package/kernel/bin/zip.js +9 -1
  12. package/kernel/connect/providers/github/README.md +5 -4
  13. package/kernel/environment.js +195 -92
  14. package/kernel/git.js +98 -19
  15. package/kernel/gitconfig_template +7 -0
  16. package/kernel/gpu/amd.js +72 -0
  17. package/kernel/gpu/apple.js +8 -0
  18. package/kernel/gpu/common.js +12 -0
  19. package/kernel/gpu/intel.js +47 -0
  20. package/kernel/gpu/nvidia.js +8 -0
  21. package/kernel/index.js +11 -1
  22. package/kernel/managed_skills.js +871 -0
  23. package/kernel/plugin.js +6 -58
  24. package/kernel/plugin_sources.js +316 -0
  25. package/kernel/resource_usage/gpu.js +349 -0
  26. package/kernel/resource_usage/index.js +322 -0
  27. package/kernel/resource_usage/macos_footprint.js +197 -0
  28. package/kernel/resource_usage/preferences.js +92 -0
  29. package/kernel/resource_usage/process_tree.js +303 -0
  30. package/kernel/scripts/git/create +4 -4
  31. package/kernel/scripts/git/fork +7 -8
  32. package/kernel/shell.js +23 -2
  33. package/kernel/shells.js +41 -0
  34. package/kernel/sysinfo.js +62 -9
  35. package/kernel/util.js +60 -0
  36. package/package.json +1 -1
  37. package/server/index.js +984 -156
  38. package/server/lib/app_log_report.js +543 -0
  39. package/server/lib/content_validation.js +55 -33
  40. package/server/lib/launcher_instruction_bootstrap.js +4 -96
  41. package/server/lib/terminal_session_helpers.js +0 -3
  42. package/server/public/common.js +77 -31
  43. package/server/public/create-launcher.js +4 -32
  44. package/server/public/logs.js +1428 -0
  45. package/server/public/nav.js +7 -0
  46. package/server/public/plugin-detail.js +93 -10
  47. package/server/public/privacy_filter_worker.js +391 -0
  48. package/server/public/style.css +1104 -154
  49. package/server/public/task-launcher.js +8 -29
  50. package/server/public/universal-launcher.css +8 -6
  51. package/server/public/universal-launcher.js +3 -27
  52. package/server/routes/apps.js +195 -1
  53. package/server/views/app.ejs +3041 -717
  54. package/server/views/autolaunch.ejs +917 -0
  55. package/server/views/bootstrap.ejs +7 -1
  56. package/server/views/d.ejs +408 -65
  57. package/server/views/editor.ejs +85 -19
  58. package/server/views/index.ejs +661 -111
  59. package/server/views/init/index.ejs +1 -1
  60. package/server/views/install.ejs +1 -1
  61. package/server/views/logs.ejs +164 -86
  62. package/server/views/net.ejs +7 -1
  63. package/server/views/partials/d_terminal_column.ejs +2 -2
  64. package/server/views/partials/d_terminal_options.ejs +0 -8
  65. package/server/views/partials/fs_status.ejs +47 -0
  66. package/server/views/partials/home_action_modal.ejs +86 -0
  67. package/server/views/partials/home_run_menu.ejs +87 -0
  68. package/server/views/partials/main_sidebar.ejs +2 -0
  69. package/server/views/partials/menu.ejs +1 -1
  70. package/server/views/plugin_detail.ejs +19 -4
  71. package/server/views/plugins.ejs +201 -3
  72. package/server/views/pre.ejs +1 -1
  73. package/server/views/pro.ejs +1 -1
  74. package/server/views/shell.ejs +40 -18
  75. package/server/views/skills.ejs +506 -0
  76. package/server/views/terminal.ejs +45 -19
  77. package/spec/INSTRUCTION_SYNC.md +20 -10
  78. package/system/plugin/antigravity-cli/antigravity.png +0 -0
  79. package/system/plugin/antigravity-cli/common.js +155 -0
  80. package/system/plugin/antigravity-cli/install.js +272 -0
  81. package/system/plugin/antigravity-cli/pinokio.js +13 -0
  82. package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
  83. package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
  84. package/system/plugin/claude/claude.png +0 -0
  85. package/system/plugin/claude/pinokio.js +47 -0
  86. package/system/plugin/claude-auto/claude.png +0 -0
  87. package/system/plugin/claude-auto/pinokio.js +58 -0
  88. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  89. package/system/plugin/claude-desktop/pinokio.js +23 -0
  90. package/system/plugin/codex/openai.webp +0 -0
  91. package/system/plugin/codex/pinokio.js +42 -0
  92. package/system/plugin/codex-auto/openai.webp +0 -0
  93. package/system/plugin/codex-auto/pinokio.js +49 -0
  94. package/system/plugin/codex-desktop/icon.png +0 -0
  95. package/system/plugin/codex-desktop/pinokio.js +23 -0
  96. package/system/plugin/crush/crush.png +0 -0
  97. package/system/plugin/crush/pinokio.js +15 -0
  98. package/system/plugin/cursor/cursor.jpeg +0 -0
  99. package/system/plugin/cursor/pinokio.js +23 -0
  100. package/system/plugin/qwen/pinokio.js +34 -0
  101. package/system/plugin/qwen/qwen.png +0 -0
  102. package/system/plugin/vscode/pinokio.js +20 -0
  103. package/system/plugin/vscode/vscode.png +0 -0
  104. package/system/plugin/windsurf/pinokio.js +23 -0
  105. package/system/plugin/windsurf/windsurf.png +0 -0
  106. package/test/antigravity-cli-plugin.test.js +185 -0
  107. package/test/app-api.test.js +239 -0
  108. package/test/app-log-report.test.js +67 -0
  109. package/test/environment-cache-preflight.test.js +98 -0
  110. package/test/git-bin.test.js +59 -0
  111. package/test/git-defaults.test.js +97 -0
  112. package/test/github-api.test.js +158 -0
  113. package/test/github-connection.test.js +117 -0
  114. package/test/huggingface-bin.test.js +25 -0
  115. package/test/managed-skills.test.js +351 -0
  116. package/test/plugin-action-functions.test.js +337 -0
  117. package/test/plugin-dev-iframe.test.js +17 -0
  118. package/test/plugin-sources.test.js +203 -0
  119. package/test/privacy-filter-worker-heuristics.test.js +69 -0
  120. package/test/process-wait.test.js +169 -0
  121. package/test/script-api.test.js +97 -0
  122. package/test/shell-api.test.js +134 -0
  123. package/test/shell-run-template.test.js +209 -0
  124. package/test/storage-api.test.js +137 -0
  125. package/test/uri-api.test.js +100 -0
@@ -25,6 +25,12 @@ document.addEventListener("DOMContentLoaded", () => {
25
25
  return;
26
26
  }
27
27
 
28
+ const headerMinimizeEnabled = !!minimizeButton;
29
+ if (!headerMinimizeEnabled) {
30
+ header.classList.remove("minimized", "transitioning");
31
+ }
32
+
33
+ if (headerMinimizeEnabled) {
28
34
  const homeIcon = homeLink ? homeLink.querySelector("img.icon") : null;
29
35
  const ensureHomeExpandIcon = () => {
30
36
  if (!homeLink || !homeIcon) {
@@ -492,6 +498,7 @@ document.addEventListener("DOMContentLoaded", () => {
492
498
  log("resize:clamp", { before, after: { left, top } });
493
499
  });
494
500
 
501
+ }
495
502
 
496
503
  // Inspector handling
497
504
  const inspectorButton = document.querySelector('#inspector');
@@ -15,6 +15,14 @@
15
15
  let share = bootstrap.share || {};
16
16
  const apps = Array.isArray(bootstrap.apps) ? bootstrap.apps : [];
17
17
  const stateUrl = typeof bootstrap.stateUrl === "string" ? bootstrap.stateUrl : "";
18
+ const cwdFromUrl = (() => {
19
+ try {
20
+ return new URLSearchParams(window.location.search).get("cwd") || "";
21
+ } catch (_) {
22
+ return "";
23
+ }
24
+ })();
25
+ plugin.cwd = typeof plugin.cwd === "string" && plugin.cwd ? plugin.cwd : cwdFromUrl;
18
26
 
19
27
  const ACTION_LABELS = {
20
28
  install: "Install",
@@ -47,9 +55,14 @@
47
55
  const overlayRefresh = document.querySelector("[data-plugin-share-overlay-refresh]");
48
56
  const actionsSection = document.querySelector("[data-plugin-actions-section]");
49
57
  const nextStepBanner = document.querySelector("[data-plugin-next-step-banner]");
58
+ const aiPermissionCard = document.querySelector("[data-plugin-ai-permissions]");
59
+ const aiPermissionClear = document.querySelector("[data-plugin-ai-permission-clear]");
60
+ const aiPermissionPath = document.querySelector("[data-plugin-ai-permission-path]");
61
+ const aiPermissionStatus = document.querySelector("[data-plugin-ai-permission-status]");
50
62
 
51
63
  let refreshInFlight = false;
52
64
  let createFormOpen = false;
65
+ const AI_CONSENT_PREFIX = "pinokio:ai-consent:";
53
66
 
54
67
  function readDownloadState() {
55
68
  try {
@@ -116,6 +129,59 @@
116
129
  }, 2200);
117
130
  }
118
131
 
132
+ function normalizeConsentSource(value) {
133
+ if (!value || typeof value !== "string") return "";
134
+ return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
135
+ }
136
+
137
+ function aiPermissionKey() {
138
+ const source = normalizeConsentSource(plugin.cwd || "");
139
+ return source ? `${AI_CONSENT_PREFIX}${encodeURIComponent(source)}` : "";
140
+ }
141
+
142
+ function updateAiPermissionStatus() {
143
+ const cwd = normalizeConsentSource(plugin.cwd || "");
144
+ if (aiPermissionCard) {
145
+ aiPermissionCard.classList.toggle("task-hidden", !cwd);
146
+ }
147
+ if (aiPermissionPath) {
148
+ aiPermissionPath.textContent = cwd;
149
+ }
150
+ if (!aiPermissionStatus) {
151
+ return;
152
+ }
153
+ const key = aiPermissionKey();
154
+ let status = "No project permission context.";
155
+ try {
156
+ if (!key || !window.localStorage) {
157
+ status = "Browser storage is unavailable.";
158
+ } else if (window.localStorage.getItem(key) !== null) {
159
+ status = "Saved permission exists for this project.";
160
+ } else {
161
+ status = "No saved permission for this project.";
162
+ }
163
+ } catch (_) {
164
+ status = "Browser storage is unavailable.";
165
+ }
166
+ aiPermissionStatus.textContent = status;
167
+ }
168
+
169
+ function clearAiPermission() {
170
+ const key = aiPermissionKey();
171
+ if (!key) {
172
+ updateAiPermissionStatus();
173
+ return;
174
+ }
175
+ try {
176
+ window.localStorage.removeItem(key);
177
+ updateAiPermissionStatus();
178
+ showFeedback("AI permission cleared for this project.", "success");
179
+ } catch (_) {
180
+ updateAiPermissionStatus();
181
+ showFeedback("Unable to clear AI permission.", "error");
182
+ }
183
+ }
184
+
119
185
  function clearNode(node) {
120
186
  while (node && node.firstChild) {
121
187
  node.removeChild(node.firstChild);
@@ -225,15 +291,15 @@
225
291
  return "";
226
292
  }
227
293
  const queryPairs = [];
228
- const pushPair = (key, value, { rawValue = false } = {}) => {
294
+ const pushPair = (key, value) => {
229
295
  if (value === undefined || value === null) {
230
296
  return;
231
297
  }
232
298
  const encodedKey = encodeURIComponent(key);
233
- const encodedValue = rawValue ? String(value) : encodeURIComponent(String(value));
299
+ const encodedValue = encodeURIComponent(String(value));
234
300
  queryPairs.push(`${encodedKey}=${encodedValue}`);
235
301
  };
236
- pushPair("plugin", plugin.pluginPath, { rawValue: true });
302
+ pushPair("plugin", plugin.pluginPath);
237
303
  if (Array.isArray(plugin.extraParams)) {
238
304
  plugin.extraParams.forEach(([key, value]) => {
239
305
  pushPair(key, value);
@@ -749,23 +815,32 @@
749
815
 
750
816
  function applyDownloadedState() {
751
817
  const downloadState = readDownloadState();
752
- if (!downloadState.downloaded) {
818
+ if (!downloadState.downloaded && !downloadState.next) {
753
819
  return;
754
820
  }
755
821
 
756
822
  const installCard = document.querySelector('[data-plugin-action-card="install"]');
757
823
  const openCard = document.querySelector('[data-plugin-action-card="open"]');
758
- const nextAction = downloadState.next || (installCard ? "install" : (openCard ? "open" : ""));
824
+ let nextAction = downloadState.next || (installCard ? "install" : (openCard ? "open" : ""));
825
+ if (nextAction === "install" && !installCard && openCard && plugin.installed === true) {
826
+ nextAction = "open";
827
+ }
759
828
  const targetCard = nextAction ? document.querySelector(`[data-plugin-action-card="${nextAction}"]`) : null;
760
829
 
761
830
  if (nextStepBanner) {
762
831
  nextStepBanner.hidden = false;
763
832
  nextStepBanner.classList.remove("task-hidden");
764
- nextStepBanner.textContent = nextAction === "install"
765
- ? "Downloaded successfully. Next step: install this plugin."
766
- : nextAction === "open"
767
- ? "Downloaded successfully. Next step: open this plugin in a project."
768
- : "Downloaded successfully.";
833
+ if (downloadState.downloaded) {
834
+ nextStepBanner.textContent = nextAction === "install"
835
+ ? "Downloaded successfully. Next step: install this plugin."
836
+ : nextAction === "open"
837
+ ? (plugin.installed === true ? "Installed. You can open this plugin in a project." : "Downloaded successfully. Next step: open this plugin in a project.")
838
+ : "Downloaded successfully.";
839
+ } else {
840
+ nextStepBanner.textContent = nextAction === "install"
841
+ ? "This plugin needs to be installed before it can run."
842
+ : "Next step: open this plugin in a project.";
843
+ }
769
844
  }
770
845
 
771
846
  if (targetCard) {
@@ -915,6 +990,14 @@
915
990
  });
916
991
  }
917
992
 
993
+ if (aiPermissionClear) {
994
+ aiPermissionClear.addEventListener("click", (event) => {
995
+ event.preventDefault();
996
+ clearAiPermission();
997
+ });
998
+ updateAiPermissionStatus();
999
+ }
1000
+
918
1001
  render();
919
1002
  applyDownloadedState();
920
1003
  })();
@@ -0,0 +1,391 @@
1
+ const TRANSFORMERS_URL = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0/+esm'
2
+ const MODEL_ID = 'openai/privacy-filter'
3
+ const MAX_CHUNK_CHARS = 1800
4
+ const CHUNK_OVERLAP_CHARS = 120
5
+ const URL_COMPONENT_LABELS = new Set([
6
+ 'private_url',
7
+ 'private_person',
8
+ 'private_organization',
9
+ 'private_username',
10
+ 'private_name'
11
+ ])
12
+
13
+ let transformersPromise = null
14
+ let pipelinePromise = null
15
+ let pipelineKey = ''
16
+ let activeDevice = 'webgpu'
17
+ let activeDtype = 'q4f16'
18
+
19
+ const post = (message) => {
20
+ self.postMessage(message)
21
+ }
22
+
23
+ const normalizeLabel = (value) => {
24
+ const label = String(value || 'private')
25
+ .replace(/^[bieos]-/i, '')
26
+ .replace(/[^a-z0-9_-]+/gi, '_')
27
+ .replace(/^_+|_+$/g, '')
28
+ .toLowerCase()
29
+ return label || 'private'
30
+ }
31
+
32
+ const trimUrlCandidate = (value) => {
33
+ return String(value || '')
34
+ .replace(/^[([{<]+/, '')
35
+ .replace(/[)\]}>.,;:]+$/g, '')
36
+ }
37
+
38
+ const tokenAround = (text, start, end) => {
39
+ const value = String(text || '')
40
+ let left = Math.max(0, start)
41
+ let right = Math.min(value.length, end)
42
+ while (left > 0 && !/[\s"'`<>]/.test(value[left - 1])) {
43
+ left -= 1
44
+ }
45
+ while (right < value.length && !/[\s"'`<>]/.test(value[right])) {
46
+ right += 1
47
+ }
48
+ let raw = value.slice(left, right)
49
+ const trimmed = trimUrlCandidate(raw)
50
+ const leadingTrim = raw.length - raw.replace(/^[([{<]+/, '').length
51
+ left += leadingTrim
52
+ right = left + trimmed.length
53
+ return {
54
+ start: left,
55
+ end: right,
56
+ value: trimmed
57
+ }
58
+ }
59
+
60
+ const parseUrlCandidate = (value) => {
61
+ let candidate = trimUrlCandidate(value)
62
+ candidate = candidate.replace(/^git\+/i, '')
63
+ const sshMatch = candidate.match(/^git@([^:\s]+):(.+)$/i)
64
+ if (sshMatch) {
65
+ return {
66
+ protocol: 'ssh:',
67
+ username: 'git',
68
+ password: '',
69
+ hostname: sshMatch[1],
70
+ search: '',
71
+ hash: ''
72
+ }
73
+ }
74
+ if (/^[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?:[/:?#]|$)/.test(candidate)) {
75
+ candidate = `https://${candidate}`
76
+ }
77
+ try {
78
+ return new URL(candidate)
79
+ } catch (_) {
80
+ return null
81
+ }
82
+ }
83
+
84
+ const isExposableUrl = (value) => {
85
+ const url = parseUrlCandidate(value)
86
+ if (!url) {
87
+ return false
88
+ }
89
+ if (url.protocol === 'ssh:') {
90
+ return Boolean(url.hostname)
91
+ }
92
+ return /^(?:https?|git|ssh):$/i.test(url.protocol) && Boolean(url.hostname)
93
+ }
94
+
95
+ const shouldKeepEntityUnmasked = (text, entity) => {
96
+ if (!URL_COMPONENT_LABELS.has(entity.label)) {
97
+ return false
98
+ }
99
+ const candidate = tokenAround(text, entity.start, entity.end)
100
+ return Boolean(candidate.value && isExposableUrl(candidate.value))
101
+ }
102
+
103
+ const detectUrlSecretEntities = (text) => {
104
+ const value = String(text || '')
105
+ const entities = []
106
+ const urlPattern = /(?:git\+)?https?:\/\/[^\s"'`<>]+|git@[^\s"'`<>]+:[^\s"'`<>]+/gi
107
+ let match = urlPattern.exec(value)
108
+ while (match) {
109
+ const token = tokenAround(value, match.index, match.index + match[0].length)
110
+ const candidate = token.value
111
+ const parsed = parseUrlCandidate(candidate)
112
+ if (parsed) {
113
+ if (parsed.protocol !== 'ssh:' && (parsed.username || parsed.password)) {
114
+ const userInfoMatch = candidate.match(/^[a-z][a-z0-9+.-]*:\/\/([^/@]+)@/i)
115
+ if (userInfoMatch) {
116
+ const start = token.start + candidate.indexOf(userInfoMatch[1])
117
+ entities.push({
118
+ label: 'url_credential',
119
+ start,
120
+ end: start + userInfoMatch[1].length
121
+ })
122
+ }
123
+ }
124
+ const sensitiveValuePattern = /([?&#;](?:access[_-]?token|refresh[_-]?token|token|secret|password|passwd|api[_-]?key|apikey|auth|authorization|session|cookie|key|signature|sig|jwt)=)([^&#;\s"'`<>]+)/gi
125
+ let sensitiveMatch = sensitiveValuePattern.exec(candidate)
126
+ while (sensitiveMatch) {
127
+ const start = token.start + sensitiveMatch.index + sensitiveMatch[1].length
128
+ entities.push({
129
+ label: 'url_secret',
130
+ start,
131
+ end: start + sensitiveMatch[2].length
132
+ })
133
+ sensitiveMatch = sensitiveValuePattern.exec(candidate)
134
+ }
135
+ }
136
+ match = urlPattern.exec(value)
137
+ }
138
+ return entities
139
+ }
140
+
141
+ const prepareMaskEntities = (text, entities) => {
142
+ return [
143
+ ...detectUrlSecretEntities(text),
144
+ ...(entities || []).filter((entity) => !shouldKeepEntityUnmasked(text, entity))
145
+ ]
146
+ }
147
+
148
+ const attachOffsets = (text, raw, absoluteStart) => {
149
+ const entities = []
150
+ let cursor = 0
151
+ for (const entry of raw || []) {
152
+ const label = normalizeLabel(entry.entity_group || entry.entity || entry.label)
153
+ const score = Number(entry.score)
154
+ if (typeof entry.start === 'number' && typeof entry.end === 'number' && entry.end > entry.start) {
155
+ entities.push({
156
+ label,
157
+ score: Number.isFinite(score) ? score : 0,
158
+ start: absoluteStart + entry.start,
159
+ end: absoluteStart + entry.end
160
+ })
161
+ cursor = Math.max(cursor, entry.end)
162
+ continue
163
+ }
164
+ const word = String(entry.word || '').replace(/^\s+/, '')
165
+ const found = word ? text.indexOf(word, cursor) : -1
166
+ if (found >= 0) {
167
+ entities.push({
168
+ label,
169
+ score: Number.isFinite(score) ? score : 0,
170
+ start: absoluteStart + found,
171
+ end: absoluteStart + found + word.length
172
+ })
173
+ cursor = found + word.length
174
+ }
175
+ }
176
+ return entities
177
+ }
178
+
179
+ const chooseChunkEnd = (text, start, hardEnd) => {
180
+ if (hardEnd >= text.length) {
181
+ return text.length
182
+ }
183
+ const slice = text.slice(start, hardEnd)
184
+ const candidates = [
185
+ slice.lastIndexOf('\n\n'),
186
+ slice.lastIndexOf('\n'),
187
+ slice.lastIndexOf(' ')
188
+ ].filter((index) => index > Math.floor(MAX_CHUNK_CHARS * 0.55))
189
+ if (candidates.length > 0) {
190
+ return start + Math.max(...candidates) + 1
191
+ }
192
+ return hardEnd
193
+ }
194
+
195
+ const buildChunks = (text) => {
196
+ const chunks = []
197
+ let start = 0
198
+ while (start < text.length) {
199
+ const hardEnd = Math.min(start + MAX_CHUNK_CHARS, text.length)
200
+ const end = chooseChunkEnd(text, start, hardEnd)
201
+ chunks.push({
202
+ start,
203
+ text: text.slice(start, end)
204
+ })
205
+ if (end >= text.length) {
206
+ break
207
+ }
208
+ start = Math.max(end - CHUNK_OVERLAP_CHARS, start + 1)
209
+ }
210
+ return chunks
211
+ }
212
+
213
+ const mergeEntities = (entities) => {
214
+ const sorted = entities
215
+ .filter((entity) => Number.isFinite(entity.start) && Number.isFinite(entity.end) && entity.end > entity.start)
216
+ .sort((a, b) => a.start - b.start || b.end - a.end || b.score - a.score)
217
+ const merged = []
218
+ for (const entity of sorted) {
219
+ const last = merged[merged.length - 1]
220
+ if (!last || entity.start >= last.end) {
221
+ merged.push(entity)
222
+ continue
223
+ }
224
+ const entityLength = entity.end - entity.start
225
+ const lastLength = last.end - last.start
226
+ if (entityLength > lastLength || (entityLength === lastLength && entity.score > last.score)) {
227
+ merged[merged.length - 1] = entity
228
+ }
229
+ }
230
+ return merged.sort((a, b) => a.start - b.start || a.end - b.end)
231
+ }
232
+
233
+ const maskText = (text, entities) => {
234
+ let cursor = 0
235
+ let masked = ''
236
+ const counts = {}
237
+ const items = []
238
+ for (const entity of entities) {
239
+ if (entity.start < cursor) {
240
+ continue
241
+ }
242
+ masked += text.slice(cursor, entity.start)
243
+ const replacement = `[${entity.label}]`
244
+ const maskedStart = masked.length
245
+ masked += replacement
246
+ const maskedEnd = masked.length
247
+ counts[entity.label] = (counts[entity.label] || 0) + 1
248
+ items.push({
249
+ id: items.length,
250
+ label: entity.label,
251
+ score: entity.score,
252
+ sourceStart: entity.start,
253
+ sourceEnd: entity.end,
254
+ maskedStart,
255
+ maskedEnd,
256
+ replacement
257
+ })
258
+ cursor = entity.end
259
+ }
260
+ masked += text.slice(cursor)
261
+ for (const item of items) {
262
+ const lineStart = masked.lastIndexOf('\n', item.maskedStart - 1) + 1
263
+ const nextLine = masked.indexOf('\n', item.maskedEnd)
264
+ const lineEnd = nextLine >= 0 ? nextLine : masked.length
265
+ item.line = masked.slice(0, item.maskedStart).split('\n').length
266
+ item.context = masked.slice(lineStart, lineEnd).trim()
267
+ }
268
+ return { masked, counts, items }
269
+ }
270
+
271
+ const loadTransformers = async () => {
272
+ if (!transformersPromise) {
273
+ transformersPromise = import(TRANSFORMERS_URL).then((mod) => {
274
+ if (mod.env) {
275
+ mod.env.allowLocalModels = false
276
+ mod.env.allowRemoteModels = true
277
+ mod.env.useBrowserCache = true
278
+ mod.env.useWasmCache = true
279
+ mod.env.cacheKey = 'pinokio-privacy-filter-cache'
280
+ }
281
+ return mod
282
+ })
283
+ }
284
+ return transformersPromise
285
+ }
286
+
287
+ const getPipeline = async (device, dtype) => {
288
+ const nextDevice = device || activeDevice
289
+ const nextDtype = dtype || activeDtype
290
+ const nextKey = `${nextDevice}:${nextDtype}`
291
+ if (pipelinePromise && pipelineKey === nextKey) {
292
+ return pipelinePromise
293
+ }
294
+ activeDevice = nextDevice
295
+ activeDtype = nextDtype
296
+ pipelineKey = nextKey
297
+ pipelinePromise = loadTransformers().then(({ pipeline }) => {
298
+ return pipeline('token-classification', MODEL_ID, {
299
+ device: activeDevice,
300
+ dtype: activeDtype,
301
+ progress_callback: (progress) => {
302
+ if (progress && progress.status === 'progress') {
303
+ post({
304
+ type: 'download',
305
+ file: progress.file || '',
306
+ loaded: progress.loaded || 0,
307
+ total: progress.total || 0,
308
+ progress: progress.progress || 0
309
+ })
310
+ }
311
+ }
312
+ })
313
+ })
314
+ pipelinePromise.catch(() => {
315
+ if (pipelineKey === nextKey) {
316
+ pipelinePromise = null
317
+ pipelineKey = ''
318
+ }
319
+ })
320
+ return pipelinePromise
321
+ }
322
+
323
+ const classifyChunks = async ({ classifier, chunks, id }) => {
324
+ const entities = []
325
+ for (let index = 0; index < chunks.length; index += 1) {
326
+ const chunk = chunks[index]
327
+ post({ type: 'chunk', id, done: index, total: chunks.length })
328
+ const output = await classifier(chunk.text, { aggregation_strategy: 'simple' })
329
+ entities.push(...attachOffsets(chunk.text, output, chunk.start))
330
+ }
331
+ return entities
332
+ }
333
+
334
+ const filterText = async ({ id, text, device, dtype }) => {
335
+ const reportText = String(text || '')
336
+ const chunks = buildChunks(reportText)
337
+ const startedAt = performance.now()
338
+ let requestedDevice = device || activeDevice
339
+ let requestedDtype = dtype || activeDtype
340
+ let classifier = null
341
+ let entities = []
342
+ try {
343
+ classifier = await getPipeline(requestedDevice, requestedDtype)
344
+ entities = await classifyChunks({ classifier, chunks, id })
345
+ } catch (error) {
346
+ if (requestedDevice === 'wasm') {
347
+ throw error
348
+ }
349
+ pipelinePromise = null
350
+ pipelineKey = ''
351
+ requestedDevice = 'wasm'
352
+ requestedDtype = 'q8'
353
+ post({
354
+ type: 'fallback',
355
+ id,
356
+ device: requestedDevice,
357
+ dtype: requestedDtype,
358
+ message: 'WebGPU privacy filtering failed. Retrying locally with WASM.'
359
+ })
360
+ classifier = await getPipeline(requestedDevice, requestedDtype)
361
+ entities = await classifyChunks({ classifier, chunks, id })
362
+ }
363
+ const merged = mergeEntities(prepareMaskEntities(reportText, entities))
364
+ const result = maskText(reportText, merged)
365
+ post({
366
+ type: 'result',
367
+ id,
368
+ text: result.masked,
369
+ entities: merged,
370
+ items: result.items,
371
+ counts: result.counts,
372
+ latencyMs: performance.now() - startedAt,
373
+ device: activeDevice,
374
+ dtype: activeDtype,
375
+ chunks: chunks.length
376
+ })
377
+ }
378
+
379
+ self.addEventListener('message', (event) => {
380
+ const message = event.data || {}
381
+ if (message.type !== 'filter') {
382
+ return
383
+ }
384
+ filterText(message).catch((error) => {
385
+ post({
386
+ type: 'error',
387
+ id: message.id,
388
+ message: error && error.message ? error.message : String(error || 'Privacy filter failed')
389
+ })
390
+ })
391
+ })