pinokiod 7.3.1 → 7.3.4

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 (122) 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/git.js +9 -10
  7. package/kernel/bin/huggingface.js +1 -1
  8. package/kernel/bin/zip.js +9 -1
  9. package/kernel/connect/providers/github/README.md +5 -4
  10. package/kernel/environment.js +195 -92
  11. package/kernel/git.js +126 -19
  12. package/kernel/gitconfig_template +7 -0
  13. package/kernel/gpu/amd.js +72 -0
  14. package/kernel/gpu/apple.js +8 -0
  15. package/kernel/gpu/common.js +12 -0
  16. package/kernel/gpu/intel.js +47 -0
  17. package/kernel/gpu/nvidia.js +8 -0
  18. package/kernel/index.js +11 -1
  19. package/kernel/managed_skills.js +871 -0
  20. package/kernel/plugin.js +6 -58
  21. package/kernel/plugin_sources.js +316 -0
  22. package/kernel/resource_usage/gpu.js +349 -0
  23. package/kernel/resource_usage/index.js +322 -0
  24. package/kernel/resource_usage/macos_footprint.js +197 -0
  25. package/kernel/resource_usage/preferences.js +92 -0
  26. package/kernel/resource_usage/process_tree.js +303 -0
  27. package/kernel/scripts/git/create +4 -4
  28. package/kernel/scripts/git/fork +7 -8
  29. package/kernel/shell.js +23 -2
  30. package/kernel/shells.js +41 -0
  31. package/kernel/sysinfo.js +62 -9
  32. package/kernel/util.js +60 -0
  33. package/package.json +1 -1
  34. package/server/index.js +984 -156
  35. package/server/lib/app_log_report.js +543 -0
  36. package/server/lib/content_validation.js +55 -33
  37. package/server/lib/launcher_instruction_bootstrap.js +4 -96
  38. package/server/lib/terminal_session_helpers.js +0 -3
  39. package/server/public/common.js +77 -31
  40. package/server/public/create-launcher.js +4 -32
  41. package/server/public/logs.js +1428 -0
  42. package/server/public/nav.js +7 -0
  43. package/server/public/plugin-detail.js +93 -10
  44. package/server/public/privacy_filter_worker.js +391 -0
  45. package/server/public/style.css +1104 -154
  46. package/server/public/task-launcher.js +8 -29
  47. package/server/public/universal-launcher.css +8 -6
  48. package/server/public/universal-launcher.js +3 -27
  49. package/server/routes/apps.js +195 -1
  50. package/server/views/app.ejs +3041 -717
  51. package/server/views/autolaunch.ejs +917 -0
  52. package/server/views/bootstrap.ejs +7 -1
  53. package/server/views/d.ejs +408 -65
  54. package/server/views/editor.ejs +85 -19
  55. package/server/views/index.ejs +661 -111
  56. package/server/views/init/index.ejs +1 -1
  57. package/server/views/install.ejs +1 -1
  58. package/server/views/logs.ejs +164 -86
  59. package/server/views/net.ejs +7 -1
  60. package/server/views/partials/d_terminal_column.ejs +2 -2
  61. package/server/views/partials/d_terminal_options.ejs +0 -8
  62. package/server/views/partials/fs_status.ejs +47 -0
  63. package/server/views/partials/home_action_modal.ejs +86 -0
  64. package/server/views/partials/home_run_menu.ejs +87 -0
  65. package/server/views/partials/main_sidebar.ejs +2 -0
  66. package/server/views/partials/menu.ejs +1 -1
  67. package/server/views/plugin_detail.ejs +19 -4
  68. package/server/views/plugins.ejs +201 -3
  69. package/server/views/pre.ejs +1 -1
  70. package/server/views/pro.ejs +1 -1
  71. package/server/views/shell.ejs +40 -18
  72. package/server/views/skills.ejs +506 -0
  73. package/server/views/terminal.ejs +45 -19
  74. package/spec/INSTRUCTION_SYNC.md +20 -10
  75. package/system/plugin/antigravity-cli/antigravity.png +0 -0
  76. package/system/plugin/antigravity-cli/common.js +155 -0
  77. package/system/plugin/antigravity-cli/install.js +272 -0
  78. package/system/plugin/antigravity-cli/pinokio.js +13 -0
  79. package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
  80. package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
  81. package/system/plugin/claude/claude.png +0 -0
  82. package/system/plugin/claude/pinokio.js +47 -0
  83. package/system/plugin/claude-auto/claude.png +0 -0
  84. package/system/plugin/claude-auto/pinokio.js +58 -0
  85. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  86. package/system/plugin/claude-desktop/pinokio.js +23 -0
  87. package/system/plugin/codex/openai.webp +0 -0
  88. package/system/plugin/codex/pinokio.js +42 -0
  89. package/system/plugin/codex-auto/openai.webp +0 -0
  90. package/system/plugin/codex-auto/pinokio.js +49 -0
  91. package/system/plugin/codex-desktop/icon.png +0 -0
  92. package/system/plugin/codex-desktop/pinokio.js +23 -0
  93. package/system/plugin/crush/crush.png +0 -0
  94. package/system/plugin/crush/pinokio.js +15 -0
  95. package/system/plugin/cursor/cursor.jpeg +0 -0
  96. package/system/plugin/cursor/pinokio.js +23 -0
  97. package/system/plugin/qwen/pinokio.js +34 -0
  98. package/system/plugin/qwen/qwen.png +0 -0
  99. package/system/plugin/vscode/pinokio.js +20 -0
  100. package/system/plugin/vscode/vscode.png +0 -0
  101. package/system/plugin/windsurf/pinokio.js +23 -0
  102. package/system/plugin/windsurf/windsurf.png +0 -0
  103. package/test/antigravity-cli-plugin.test.js +185 -0
  104. package/test/app-api.test.js +239 -0
  105. package/test/app-log-report.test.js +67 -0
  106. package/test/environment-cache-preflight.test.js +98 -0
  107. package/test/git-bin.test.js +59 -0
  108. package/test/git-defaults.test.js +150 -0
  109. package/test/github-api.test.js +158 -0
  110. package/test/github-connection.test.js +117 -0
  111. package/test/huggingface-bin.test.js +25 -0
  112. package/test/managed-skills.test.js +351 -0
  113. package/test/plugin-action-functions.test.js +337 -0
  114. package/test/plugin-dev-iframe.test.js +17 -0
  115. package/test/plugin-sources.test.js +203 -0
  116. package/test/privacy-filter-worker-heuristics.test.js +69 -0
  117. package/test/process-wait.test.js +169 -0
  118. package/test/script-api.test.js +97 -0
  119. package/test/shell-api.test.js +134 -0
  120. package/test/shell-run-template.test.js +209 -0
  121. package/test/storage-api.test.js +137 -0
  122. package/test/uri-api.test.js +100 -0
@@ -0,0 +1,349 @@
1
+ "use strict"
2
+
3
+ const fs = require("fs")
4
+ const os = require("os")
5
+ const path = require("path")
6
+ const { execFileText, normalizePid } = require("./process_tree")
7
+
8
+ const DEFAULT_GPU_TTL_MS = 10000
9
+ const DEFAULT_GPU_TIMEOUT_MS = 2500
10
+ const MIB = 1024 * 1024
11
+
12
+ function unique(values) {
13
+ const seen = new Set()
14
+ const next = []
15
+ for (const value of values) {
16
+ if (!value || seen.has(value)) continue
17
+ seen.add(value)
18
+ next.push(value)
19
+ }
20
+ return next
21
+ }
22
+
23
+ function pathExists(filepath) {
24
+ try {
25
+ fs.accessSync(filepath, fs.constants.X_OK)
26
+ return true
27
+ } catch (_) {
28
+ return false
29
+ }
30
+ }
31
+
32
+ function executableCandidates(candidates) {
33
+ return unique(candidates).filter((candidate) => {
34
+ if (!candidate) return false
35
+ if (path.isAbsolute(candidate)) {
36
+ return pathExists(candidate)
37
+ }
38
+ return true
39
+ })
40
+ }
41
+
42
+ function getPinokioCondaCandidates(kernel, names) {
43
+ if (!kernel || !kernel.homedir) {
44
+ return []
45
+ }
46
+ const prefix = path.resolve(kernel.homedir, "bin", "miniconda")
47
+ const suffixes = os.platform() === "win32"
48
+ ? ["", ".exe"]
49
+ : [""]
50
+ const folders = os.platform() === "win32"
51
+ ? ["Library/bin", "Scripts", ""]
52
+ : ["bin", "Library/bin", ""]
53
+ const candidates = []
54
+ for (const name of names) {
55
+ for (const folder of folders) {
56
+ for (const suffix of suffixes) {
57
+ candidates.push(path.resolve(prefix, folder, `${name}${suffix}`))
58
+ }
59
+ }
60
+ }
61
+ return candidates
62
+ }
63
+
64
+ function parseMemoryToBytes(value, defaultUnit = "") {
65
+ if (value == null) return null
66
+ if (typeof value === "number") {
67
+ if (!Number.isFinite(value) || value < 0) return null
68
+ if (defaultUnit === "mib") return Math.round(value * MIB)
69
+ if (defaultUnit === "kb") return Math.round(value * 1024)
70
+ return Math.round(value)
71
+ }
72
+ const raw = String(value).trim()
73
+ if (!raw || /N\/A|not supported|none/i.test(raw)) {
74
+ return null
75
+ }
76
+ const match = /(-?\d+(?:\.\d+)?)\s*([KMGT]?i?B|[KMGT]B|bytes?)?/i.exec(raw)
77
+ if (!match) return null
78
+ const amount = Number.parseFloat(match[1])
79
+ if (!Number.isFinite(amount) || amount < 0) return null
80
+ const unit = (match[2] || defaultUnit || "bytes").toLowerCase()
81
+ if (unit === "mib" || unit === "mb") return Math.round(amount * MIB)
82
+ if (unit === "gib" || unit === "gb") return Math.round(amount * 1024 * MIB)
83
+ if (unit === "kib" || unit === "kb") return Math.round(amount * 1024)
84
+ if (unit === "tib" || unit === "tb") return Math.round(amount * 1024 * 1024 * MIB)
85
+ return Math.round(amount)
86
+ }
87
+
88
+ function addGpuProcess(processes, pid, bytes) {
89
+ const normalizedPid = normalizePid(pid)
90
+ if (!normalizedPid || !Number.isFinite(bytes) || bytes < 0) {
91
+ return
92
+ }
93
+ const current = processes.get(normalizedPid) || {
94
+ pid: normalizedPid,
95
+ usedGpuMemoryBytes: 0
96
+ }
97
+ current.usedGpuMemoryBytes += bytes
98
+ processes.set(normalizedPid, current)
99
+ }
100
+
101
+ function parseNvidiaCsv(stdout) {
102
+ const processes = new Map()
103
+ for (const line of String(stdout || "").split(/\r?\n/)) {
104
+ const trimmed = line.trim()
105
+ if (!trimmed) continue
106
+ const parts = trimmed.split(",").map((part) => part.trim())
107
+ const pid = normalizePid(parts[0])
108
+ const bytes = parseMemoryToBytes(parts[1], "mib")
109
+ addGpuProcess(processes, pid, bytes)
110
+ }
111
+ return processes
112
+ }
113
+
114
+ function findObjectValue(object, predicate) {
115
+ if (!object || typeof object !== "object" || Array.isArray(object)) {
116
+ return null
117
+ }
118
+ for (const [key, value] of Object.entries(object)) {
119
+ if (predicate(key, value)) {
120
+ return value
121
+ }
122
+ }
123
+ return null
124
+ }
125
+
126
+ function extractAmdProcessesFromJson(value, processes = new Map()) {
127
+ if (Array.isArray(value)) {
128
+ for (const item of value) {
129
+ extractAmdProcessesFromJson(item, processes)
130
+ }
131
+ return processes
132
+ }
133
+ if (!value || typeof value !== "object") {
134
+ return processes
135
+ }
136
+
137
+ const pidValue = findObjectValue(value, (key) => /(^|[_\s-])pid$|process[_\s-]*id/i.test(key))
138
+ const memoryValue = findObjectValue(value, (key) => {
139
+ const normalized = key.toLowerCase()
140
+ if (/total|free|available|limit/.test(normalized)) return false
141
+ return /vram|memory/.test(normalized) && /usage|used|mem|size/.test(normalized)
142
+ })
143
+ const pid = normalizePid(pidValue)
144
+ const bytes = parseMemoryToBytes(memoryValue)
145
+ addGpuProcess(processes, pid, bytes)
146
+
147
+ for (const child of Object.values(value)) {
148
+ if (child && typeof child === "object") {
149
+ extractAmdProcessesFromJson(child, processes)
150
+ }
151
+ }
152
+ return processes
153
+ }
154
+
155
+ function parseAmdJson(stdout) {
156
+ const parsed = JSON.parse(stdout || "[]")
157
+ return extractAmdProcessesFromJson(parsed)
158
+ }
159
+
160
+ class GpuSampler {
161
+ constructor(options = {}) {
162
+ this.kernel = options.kernel || null
163
+ this.ttlMs = options.ttlMs || DEFAULT_GPU_TTL_MS
164
+ this.timeoutMs = options.timeoutMs || DEFAULT_GPU_TIMEOUT_MS
165
+ this.current = null
166
+ this.inFlight = null
167
+ this.providerBackoff = new Map()
168
+ }
169
+
170
+ nvidiaCandidates() {
171
+ const platform = os.platform()
172
+ const candidates = [
173
+ process.env.NVIDIA_SMI,
174
+ "nvidia-smi",
175
+ ...getPinokioCondaCandidates(this.kernel, ["nvidia-smi"])
176
+ ]
177
+ if (platform === "win32") {
178
+ candidates.push(
179
+ "C:\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe",
180
+ "C:\\Windows\\System32\\nvidia-smi.exe"
181
+ )
182
+ } else if (platform === "linux") {
183
+ candidates.push(
184
+ "/usr/bin/nvidia-smi",
185
+ "/usr/local/bin/nvidia-smi",
186
+ "/usr/local/nvidia/bin/nvidia-smi",
187
+ "/usr/local/cuda/bin/nvidia-smi"
188
+ )
189
+ }
190
+ return executableCandidates(candidates)
191
+ }
192
+
193
+ amdCandidates() {
194
+ const candidates = [
195
+ process.env.AMD_SMI,
196
+ "amd-smi",
197
+ ...getPinokioCondaCandidates(this.kernel, ["amd-smi"])
198
+ ]
199
+ if (os.platform() === "linux") {
200
+ candidates.push("/opt/rocm/bin/amd-smi", "/usr/bin/amd-smi", "/usr/local/bin/amd-smi")
201
+ }
202
+ return executableCandidates(candidates)
203
+ }
204
+
205
+ isBackedOff(provider) {
206
+ const until = this.providerBackoff.get(provider) || 0
207
+ return Date.now() < until
208
+ }
209
+
210
+ backoff(provider, ms = 60000) {
211
+ this.providerBackoff.set(provider, Date.now() + ms)
212
+ }
213
+
214
+ async collectNvidia() {
215
+ if (this.isBackedOff("nvidia")) {
216
+ return null
217
+ }
218
+ const args = [
219
+ "--query-compute-apps=pid,used_gpu_memory",
220
+ "--format=csv,noheader,nounits"
221
+ ]
222
+ let lastError = null
223
+ for (const command of this.nvidiaCandidates()) {
224
+ try {
225
+ const { stdout } = await execFileText(command, args, { timeoutMs: this.timeoutMs })
226
+ return {
227
+ provider: "nvidia-smi",
228
+ processes: parseNvidiaCsv(stdout),
229
+ error: null
230
+ }
231
+ } catch (error) {
232
+ lastError = error
233
+ if (error && error.code === "ENOENT") {
234
+ continue
235
+ }
236
+ break
237
+ }
238
+ }
239
+ this.backoff("nvidia", 60000)
240
+ return {
241
+ provider: "nvidia-smi",
242
+ processes: new Map(),
243
+ error: lastError && lastError.message ? lastError.message : "nvidia-smi unavailable"
244
+ }
245
+ }
246
+
247
+ async collectAmd() {
248
+ if (os.platform() !== "linux" || this.isBackedOff("amd")) {
249
+ return null
250
+ }
251
+ let lastError = null
252
+ for (const command of this.amdCandidates()) {
253
+ try {
254
+ const { stdout } = await execFileText(command, ["process", "--json", "-G"], { timeoutMs: this.timeoutMs })
255
+ return {
256
+ provider: "amd-smi",
257
+ processes: parseAmdJson(stdout),
258
+ error: null
259
+ }
260
+ } catch (error) {
261
+ lastError = error
262
+ if (error && error.code === "ENOENT") {
263
+ continue
264
+ }
265
+ break
266
+ }
267
+ }
268
+ this.backoff("amd", 90000)
269
+ return {
270
+ provider: "amd-smi",
271
+ processes: new Map(),
272
+ error: lastError && lastError.message ? lastError.message : "amd-smi unavailable"
273
+ }
274
+ }
275
+
276
+ async collect() {
277
+ const results = []
278
+ const nvidia = await this.collectNvidia()
279
+ if (nvidia) results.push(nvidia)
280
+ const amd = await this.collectAmd()
281
+ if (amd) results.push(amd)
282
+
283
+ const processes = new Map()
284
+ const providers = []
285
+ const errors = []
286
+ for (const result of results) {
287
+ if (!result) continue
288
+ if (result.provider) providers.push(result.provider)
289
+ if (result.error) errors.push({ provider: result.provider, error: result.error })
290
+ for (const entry of result.processes.values()) {
291
+ addGpuProcess(processes, entry.pid, entry.usedGpuMemoryBytes)
292
+ }
293
+ }
294
+ return {
295
+ available: providers.length > 0 && errors.length < providers.length,
296
+ stale: false,
297
+ collectedAt: Date.now(),
298
+ providers,
299
+ processes,
300
+ errors
301
+ }
302
+ }
303
+
304
+ async getSnapshot() {
305
+ const now = Date.now()
306
+ if (this.current && now - this.current.collectedAt < this.ttlMs) {
307
+ return this.current
308
+ }
309
+ if (this.inFlight) {
310
+ return this.inFlight
311
+ }
312
+ this.inFlight = this.collect().then((snapshot) => {
313
+ this.current = snapshot
314
+ return snapshot
315
+ }).catch((error) => {
316
+ if (this.current) {
317
+ return { ...this.current, stale: true }
318
+ }
319
+ return {
320
+ available: false,
321
+ stale: false,
322
+ collectedAt: Date.now(),
323
+ providers: [],
324
+ processes: new Map(),
325
+ errors: [{ provider: "gpu", error: error && error.message ? error.message : String(error) }]
326
+ }
327
+ }).finally(() => {
328
+ this.inFlight = null
329
+ })
330
+ return this.inFlight
331
+ }
332
+ }
333
+
334
+ function sumGpuMemory(snapshot, pids) {
335
+ const processes = snapshot && snapshot.processes instanceof Map ? snapshot.processes : new Map()
336
+ let bytes = 0
337
+ for (const pid of pids || []) {
338
+ const entry = processes.get(pid)
339
+ if (!entry) continue
340
+ bytes += entry.usedGpuMemoryBytes || 0
341
+ }
342
+ return { bytes }
343
+ }
344
+
345
+ module.exports = {
346
+ GpuSampler,
347
+ parseMemoryToBytes,
348
+ sumGpuMemory
349
+ }
@@ -0,0 +1,322 @@
1
+ "use strict"
2
+
3
+ const os = require("os")
4
+ const { GpuSampler, sumGpuMemory } = require("./gpu")
5
+ const { MacFootprintSampler } = require("./macos_footprint")
6
+ const { ProcessSampler, getDescendantPids, sumProcessMetrics } = require("./process_tree")
7
+ const { ResourceUsagePreferences } = require("./preferences")
8
+
9
+ function formatBytes(bytes) {
10
+ const value = Number(bytes)
11
+ if (!Number.isFinite(value) || value < 0) {
12
+ return "--"
13
+ }
14
+ const k = 1024
15
+ if (value >= k * k * k) {
16
+ return `${Math.floor((value / k / k / k) * 100) / 100} GB`
17
+ }
18
+ if (value >= k * k) {
19
+ return `${Math.floor((value / k / k) * 100) / 100} MB`
20
+ }
21
+ if (value >= k) {
22
+ return `${Math.floor((value / k) * 100) / 100} KB`
23
+ }
24
+ return `${Math.floor(value)} B`
25
+ }
26
+
27
+ function normalizeWorkspaceName(value) {
28
+ if (typeof value !== "string") {
29
+ return ""
30
+ }
31
+ const trimmed = value.trim()
32
+ if (!trimmed || trimmed.includes("/") || trimmed.includes("\\") || trimmed === "." || trimmed === "..") {
33
+ return ""
34
+ }
35
+ return trimmed
36
+ }
37
+
38
+ class ResourceUsageService {
39
+ constructor(options = {}) {
40
+ if (!options.kernel) {
41
+ throw new Error("ResourceUsageService requires kernel")
42
+ }
43
+ this.kernel = options.kernel
44
+ this.platform = this.kernel.platform || os.platform()
45
+ this.preferences = new ResourceUsagePreferences({ kernel: this.kernel })
46
+ this.processSampler = new ProcessSampler({
47
+ platform: this.platform,
48
+ ttlMs: 4000,
49
+ timeoutMs: 2500
50
+ })
51
+ this.macFootprintSampler = new MacFootprintSampler({
52
+ platform: this.platform,
53
+ ttlMs: 15000,
54
+ timeoutMs: 2000
55
+ })
56
+ this.gpuSampler = new GpuSampler({
57
+ kernel: this.kernel,
58
+ ttlMs: 10000,
59
+ timeoutMs: 2500
60
+ })
61
+ this.cpuAverages = new Map()
62
+ this.workspaceCache = new Map()
63
+ this.collectInFlight = null
64
+ this.lastGlobalCollectAt = 0
65
+ this.globalTtlMs = options.globalTtlMs || 5000
66
+ }
67
+
68
+ async getPreferences() {
69
+ return this.preferences.read()
70
+ }
71
+
72
+ async updatePreferences(updates = {}) {
73
+ const preferences = await this.preferences.update(updates)
74
+ this.workspaceCache.clear()
75
+ this.lastGlobalCollectAt = 0
76
+ return preferences
77
+ }
78
+
79
+ getShellRootGroups() {
80
+ if (!this.kernel || !this.kernel.shell || typeof this.kernel.path !== "function") {
81
+ return new Map()
82
+ }
83
+ const apiRoot = this.kernel.path("api")
84
+ if (typeof this.kernel.shell.resourceRootsByWorkspace === "function") {
85
+ return this.kernel.shell.resourceRootsByWorkspace(apiRoot)
86
+ }
87
+ return new Map()
88
+ }
89
+
90
+ smoothCpu(workspaceName, value) {
91
+ if (!Number.isFinite(value)) {
92
+ this.cpuAverages.delete(workspaceName)
93
+ return null
94
+ }
95
+ const now = Date.now()
96
+ const previous = this.cpuAverages.get(workspaceName)
97
+ let smoothed = value
98
+ if (previous && Number.isFinite(previous.value) && now - previous.updatedAt < 30000) {
99
+ smoothed = (previous.value * 0.55) + (value * 0.45)
100
+ }
101
+ this.cpuAverages.set(workspaceName, { value: smoothed, updatedAt: now })
102
+ return smoothed
103
+ }
104
+
105
+ metric(enabled, available, value = {}) {
106
+ return {
107
+ enabled: !!enabled,
108
+ available: !!available,
109
+ ...value
110
+ }
111
+ }
112
+
113
+ selectFootprintPids(pids) {
114
+ return Array.from(pids || []).sort((a, b) => a - b)
115
+ }
116
+
117
+ emptyWorkspaceUsage(name, preferences, options = {}) {
118
+ const updatedAt = options.updatedAt || Date.now()
119
+ return {
120
+ ok: true,
121
+ workspace: name,
122
+ running: false,
123
+ updated_at: new Date(updatedAt).toISOString(),
124
+ stale: !!options.stale,
125
+ preferences,
126
+ metrics: {
127
+ ram: this.metric(preferences.show_ram, false, {
128
+ bytes: 0,
129
+ formatted: "0 B"
130
+ }),
131
+ cpu: this.metric(preferences.show_cpu, false, {
132
+ percent: null
133
+ }),
134
+ vram: this.metric(preferences.show_vram, false, {
135
+ bytes: 0,
136
+ formatted: "0 MB"
137
+ })
138
+ }
139
+ }
140
+ }
141
+
142
+ sumFootprintBytes(footprintSnapshot, pids) {
143
+ const perPid = footprintSnapshot && footprintSnapshot.perPid instanceof Map ? footprintSnapshot.perPid : new Map()
144
+ let bytes = 0
145
+ for (const pid of pids || []) {
146
+ bytes += perPid.get(pid) || 0
147
+ }
148
+ return bytes
149
+ }
150
+
151
+ buildWorkspaceUsage(name, preferences, roots, processSnapshot, processData, gpuSnapshot, footprintSnapshot) {
152
+ const pids = processData && processData.pids instanceof Set ? processData.pids : new Set()
153
+ const processSummary = processData && processData.summary ? processData.summary : {
154
+ processCount: 0,
155
+ rssBytes: 0,
156
+ cpuPercent: null,
157
+ cpuPercentCores: null
158
+ }
159
+
160
+ let ramBytes = processSummary.rssBytes
161
+ const footprintBytes = preferences.show_ram && this.platform === "darwin"
162
+ ? this.sumFootprintBytes(footprintSnapshot, pids)
163
+ : 0
164
+ if (footprintBytes > 0) {
165
+ ramBytes = footprintBytes
166
+ }
167
+
168
+ const smoothedCpu = preferences.show_cpu
169
+ ? this.smoothCpu(name, processSummary.cpuPercent)
170
+ : null
171
+
172
+ const gpuSummary = preferences.show_vram && pids.size > 0
173
+ ? sumGpuMemory(gpuSnapshot, pids)
174
+ : {
175
+ bytes: 0
176
+ }
177
+
178
+ const rootPids = roots.map((root) => root.pid).filter((pid) => Number.isFinite(pid))
179
+ const running = rootPids.length > 0 && (processSummary.processCount > 0 || pids.size > 0)
180
+ const processAvailable = !!(processSnapshot && processSnapshot.available)
181
+ const ramAvailable = running && !!(processAvailable || footprintBytes > 0)
182
+ const gpuAvailable = !!(gpuSnapshot && gpuSnapshot.available)
183
+ const updatedAt = Math.max(
184
+ processSnapshot && processSnapshot.collectedAt ? processSnapshot.collectedAt : 0,
185
+ footprintSnapshot && footprintSnapshot.collectedAt ? footprintSnapshot.collectedAt : 0,
186
+ gpuSnapshot && gpuSnapshot.collectedAt ? gpuSnapshot.collectedAt : 0,
187
+ Date.now()
188
+ )
189
+
190
+ return {
191
+ ok: true,
192
+ workspace: name,
193
+ running,
194
+ updated_at: new Date(updatedAt).toISOString(),
195
+ stale: !!((processSnapshot && processSnapshot.stale) || (footprintSnapshot && footprintSnapshot.stale) || (gpuSnapshot && gpuSnapshot.stale)),
196
+ preferences,
197
+ metrics: {
198
+ ram: this.metric(preferences.show_ram, ramAvailable, {
199
+ bytes: ramBytes,
200
+ formatted: formatBytes(ramBytes)
201
+ }),
202
+ cpu: this.metric(preferences.show_cpu, Number.isFinite(smoothedCpu), {
203
+ percent: Number.isFinite(smoothedCpu) ? Math.max(0, Math.round(smoothedCpu * 10) / 10) : null
204
+ }),
205
+ vram: this.metric(preferences.show_vram, gpuAvailable && running, {
206
+ bytes: gpuSummary.bytes,
207
+ formatted: gpuSummary.bytes > 0 ? formatBytes(gpuSummary.bytes) : "0 MB"
208
+ })
209
+ }
210
+ }
211
+ }
212
+
213
+ async collectGlobalUsage(preferencesOverride = null) {
214
+ const preferences = preferencesOverride || await this.getPreferences()
215
+ const rootGroups = this.getShellRootGroups()
216
+ const hasRoots = rootGroups.size > 0
217
+ const shouldCollectProcesses = hasRoots && (
218
+ preferences.show_ram ||
219
+ preferences.show_cpu ||
220
+ preferences.show_vram
221
+ )
222
+
223
+ const processSnapshot = shouldCollectProcesses ? await this.processSampler.getSnapshot() : null
224
+ const workspaceProcesses = new Map()
225
+ const allPids = new Set()
226
+
227
+ for (const [name, roots] of rootGroups.entries()) {
228
+ const rootPids = roots.map((root) => root.pid).filter((pid) => Number.isFinite(pid))
229
+ const pids = processSnapshot ? getDescendantPids(processSnapshot, rootPids) : new Set()
230
+ const summary = processSnapshot ? sumProcessMetrics(processSnapshot, pids) : {
231
+ processCount: 0,
232
+ rssBytes: 0,
233
+ cpuPercent: null,
234
+ cpuPercentCores: null
235
+ }
236
+ workspaceProcesses.set(name, { pids, summary })
237
+ for (const pid of pids) {
238
+ allPids.add(pid)
239
+ }
240
+ }
241
+
242
+ const footprintSnapshot = preferences.show_ram && this.platform === "darwin" && allPids.size > 0
243
+ ? await this.macFootprintSampler.getFootprintByPid(this.selectFootprintPids(allPids))
244
+ : null
245
+
246
+ const gpuSnapshot = preferences.show_vram && allPids.size > 0
247
+ ? await this.gpuSampler.getSnapshot()
248
+ : null
249
+
250
+ const nextCache = new Map()
251
+ for (const [name, roots] of rootGroups.entries()) {
252
+ const usage = this.buildWorkspaceUsage(
253
+ name,
254
+ preferences,
255
+ roots,
256
+ processSnapshot,
257
+ workspaceProcesses.get(name),
258
+ gpuSnapshot,
259
+ footprintSnapshot
260
+ )
261
+ nextCache.set(name, usage)
262
+ }
263
+ for (const name of this.cpuAverages.keys()) {
264
+ if (!rootGroups.has(name)) {
265
+ this.cpuAverages.delete(name)
266
+ }
267
+ }
268
+
269
+ this.workspaceCache = nextCache
270
+ this.lastGlobalCollectAt = Date.now()
271
+ return nextCache
272
+ }
273
+
274
+ async ensureGlobalRefresh(options = {}) {
275
+ const force = !!options.force
276
+ const wait = !!options.wait
277
+ const now = Date.now()
278
+ if (!force && this.lastGlobalCollectAt && now - this.lastGlobalCollectAt < this.globalTtlMs) {
279
+ return this.workspaceCache
280
+ }
281
+ if (!this.collectInFlight) {
282
+ this.collectInFlight = this.collectGlobalUsage().catch(() => {
283
+ this.lastGlobalCollectAt = Date.now()
284
+ return this.workspaceCache
285
+ }).finally(() => {
286
+ this.collectInFlight = null
287
+ })
288
+ }
289
+ if (wait) {
290
+ return this.collectInFlight
291
+ }
292
+ return this.workspaceCache
293
+ }
294
+
295
+ markCachedUsage(usage) {
296
+ if (!usage) return usage
297
+ const stale = !!(this.lastGlobalCollectAt && Date.now() - this.lastGlobalCollectAt >= this.globalTtlMs)
298
+ return stale ? { ...usage, stale: true } : usage
299
+ }
300
+
301
+ async getWorkspaceUsage(workspaceName) {
302
+ const name = normalizeWorkspaceName(workspaceName)
303
+ if (!name) {
304
+ return {
305
+ ok: false,
306
+ error: "Invalid workspace",
307
+ preferences: await this.getPreferences()
308
+ }
309
+ }
310
+
311
+ const cached = this.workspaceCache.get(name)
312
+ await this.ensureGlobalRefresh({ wait: !cached })
313
+ const updated = this.workspaceCache.get(name)
314
+ if (updated) {
315
+ return this.markCachedUsage(updated)
316
+ }
317
+ const preferences = await this.getPreferences()
318
+ return this.emptyWorkspaceUsage(name, preferences, { updatedAt: this.lastGlobalCollectAt || Date.now() })
319
+ }
320
+ }
321
+
322
+ module.exports = ResourceUsageService