salidium 0.3.0 → 0.4.1

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/README.md CHANGED
@@ -23,6 +23,15 @@ may contact its provider; the first run asks before enabling them.
23
23
  An optional local profile can restate an existing generated Why and How in familiar terms. Saving
24
24
  it sends nothing; each personalization call is explicit, ephemeral, and never changes evidence.
25
25
 
26
+ Local operations are available through the interface and CLI: queue and storage readings are exact
27
+ when safely observable and explicitly unavailable rather than partial, while health trends remain
28
+ bounded estimates. Both surfaces show durable maintenance progress, transition-based local alerts,
29
+ and versioned policy with source labels. The CLI can also produce a redacted diagnostic bundle, and
30
+ native desktop notifications are separately opt-in. The browser is only a control panel: closing it
31
+ does not stop the local daemon, and CLI status and control remain available without it. On macOS,
32
+ optional always-on mode adds login startup, crash recovery, and a native menu-bar control without
33
+ adding telemetry or a cloud dependency.
34
+
26
35
  <picture>
27
36
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twinkling-reality/salidium/main/apps/site/public/report-dark.png">
28
37
  <img src="https://raw.githubusercontent.com/twinkling-reality/salidium/main/apps/site/public/report-light.png" alt="A full report: the verdict, Why drawn as two paths converging on one order charged twice, How drawn as one idempotency key per order, and the approach the agent abandoned beside the one it adopted.">
@@ -0,0 +1,38 @@
1
+ Third-Party Notices
2
+ ===================
3
+
4
+ The Salidium npm package contains bundled code from these MIT-licensed projects:
5
+
6
+ - Zod 4.4.3 — Copyright (c) 2025 Colin McDonnell
7
+ - React 19.2.8, React DOM 19.2.8, and Scheduler 0.27.0 — Copyright (c) Meta
8
+ Platforms, Inc. and affiliates
9
+ - Zustand 5.0.15 — Copyright (c) 2019 Paul Henschel
10
+ - TanStack React Virtual 3.14.9 and TanStack Virtual Core 3.17.7 — Copyright (c)
11
+ 2021-present Tanner Linsley
12
+ - Vite 8.2.1 generated browser runtime — Copyright (c) 2019-present, VoidZero Inc.
13
+ and Vite contributors
14
+ - Rolldown 1.2.4 generated browser runtime — Copyright (c) 2024-present VoidZero
15
+ Inc. & Contributors
16
+ - esbuild 0.28.2 generated bundle helpers — Copyright (c) 2020 Evan Wallace
17
+ - Rollup-derived portions of the Rolldown runtime — Copyright (c) 2017 Rollup
18
+ contributors
19
+
20
+ MIT License
21
+
22
+ Permission is hereby granted, free of charge, to any person obtaining a copy
23
+ of this software and associated documentation files (the "Software"), to deal
24
+ in the Software without restriction, including without limitation the rights
25
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26
+ copies of the Software, and to permit persons to whom the Software is
27
+ furnished to do so, subject to the following conditions:
28
+
29
+ The above copyright notice and this permission notice shall be included in all
30
+ copies or substantial portions of the Software.
31
+
32
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
+ SOFTWARE.
@@ -0,0 +1,383 @@
1
+ import AppKit
2
+ import Foundation
3
+
4
+ private struct Configuration {
5
+ let home: String
6
+ let node: String
7
+ let cli: String
8
+
9
+ static func parse(_ arguments: [String]) -> Configuration? {
10
+ var values: [String: String] = [:]
11
+ var index = 1
12
+ while index + 1 < arguments.count {
13
+ let key = arguments[index]
14
+ if key.hasPrefix("--") {
15
+ values[key] = arguments[index + 1]
16
+ index += 2
17
+ } else {
18
+ index += 1
19
+ }
20
+ }
21
+ guard let home = values["--home"],
22
+ let node = values["--node"],
23
+ let cli = values["--cli"] else { return nil }
24
+ return Configuration(home: home, node: node, cli: cli)
25
+ }
26
+ }
27
+
28
+ private enum Health: String {
29
+ case healthy
30
+ case attention
31
+ case critical
32
+ case offline
33
+ }
34
+
35
+ private struct Snapshot {
36
+ var health: Health = .offline
37
+ var pid: Int?
38
+ var collection = "Unavailable"
39
+ var queueFiles: Int?
40
+ var queueBytes: Int?
41
+ var storeBytes: Int?
42
+ var retention = "Unavailable"
43
+ var activeAlerts = 0
44
+ var maintenance: String?
45
+
46
+ static let offline = Snapshot()
47
+ }
48
+
49
+ private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
50
+ private let configuration: Configuration
51
+ private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
52
+ private let menu = NSMenu()
53
+ private var snapshot = Snapshot.offline
54
+ private var refreshInProgress = false
55
+ private var timer: Timer?
56
+
57
+ init(configuration: Configuration) {
58
+ self.configuration = configuration
59
+ super.init()
60
+ }
61
+
62
+ func applicationDidFinishLaunching(_ notification: Notification) {
63
+ NSApp.setActivationPolicy(.accessory)
64
+ menu.delegate = self
65
+ statusItem.menu = menu
66
+ if let button = statusItem.button {
67
+ button.toolTip = "Salidium local operations"
68
+ button.setAccessibilityLabel("Salidium local operations")
69
+ }
70
+ rebuildMenu()
71
+ refresh()
72
+ timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
73
+ self?.refresh()
74
+ }
75
+ }
76
+
77
+ func applicationWillTerminate(_ notification: Notification) {
78
+ timer?.invalidate()
79
+ }
80
+
81
+ func menuWillOpen(_ menu: NSMenu) {
82
+ refresh()
83
+ }
84
+
85
+ private func refresh() {
86
+ guard !refreshInProgress else { return }
87
+ refreshInProgress = true
88
+ let configuration = self.configuration
89
+ DispatchQueue.global(qos: .utility).async { [weak self] in
90
+ let next = Self.readSnapshot(configuration: configuration)
91
+ DispatchQueue.main.async {
92
+ guard let self = self else { return }
93
+ self.snapshot = next
94
+ self.refreshInProgress = false
95
+ self.rebuildMenu()
96
+ }
97
+ }
98
+ }
99
+
100
+ private static func readSnapshot(configuration: Configuration) -> Snapshot {
101
+ let daemonPath = URL(fileURLWithPath: configuration.home)
102
+ .appendingPathComponent("daemon.json")
103
+ guard let daemonData = try? Data(contentsOf: daemonPath),
104
+ let daemon = try? JSONSerialization.jsonObject(with: daemonData) as? [String: Any],
105
+ let port = integer(daemon["port"]),
106
+ let token = daemon["token"] as? String,
107
+ let recordedPID = integer(daemon["pid"]),
108
+ (1...65535).contains(port) else { return .offline }
109
+
110
+ guard let url = URL(string: "http://127.0.0.1:\(port)/api/operations") else {
111
+ return .offline
112
+ }
113
+ var request = URLRequest(url: url)
114
+ request.timeoutInterval = 2
115
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
116
+ let semaphore = DispatchSemaphore(value: 0)
117
+ var responseData: Data?
118
+ var responseStatus: Int?
119
+ let task = URLSession.shared.dataTask(with: request) { data, response, _ in
120
+ responseData = data
121
+ responseStatus = (response as? HTTPURLResponse)?.statusCode
122
+ semaphore.signal()
123
+ }
124
+ task.resume()
125
+ guard semaphore.wait(timeout: .now() + 2.5) == .success,
126
+ responseStatus == 200,
127
+ let data = responseData,
128
+ let overview = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
129
+ let health = overview["health"] as? [String: Any],
130
+ let daemonHealth = health["daemon"] as? [String: Any],
131
+ integer(daemonHealth["pid"]) == recordedPID else {
132
+ task.cancel()
133
+ return .offline
134
+ }
135
+
136
+ var snapshot = Snapshot()
137
+ snapshot.health = Health(rawValue: health["overall"] as? String ?? "") ?? .attention
138
+ snapshot.pid = integer(daemonHealth["pid"])
139
+ if let collection = health["collection"] as? [String: Any],
140
+ let state = collection["state"] as? String {
141
+ snapshot.collection = state == "active" ? "On" : "Paused"
142
+ }
143
+ if let queue = health["queue"] as? [String: Any] {
144
+ snapshot.queueFiles = integer(queue["files"])
145
+ snapshot.queueBytes = integer(queue["bytes"])
146
+ }
147
+ if let store = health["store"] as? [String: Any] {
148
+ snapshot.storeBytes = integer(store["totalBytes"])
149
+ if let value = store["retention"] {
150
+ snapshot.retention = retentionLabel(value)
151
+ }
152
+ }
153
+ if let alerts = overview["alerts"] as? [String: Any],
154
+ let active = alerts["active"] as? [Any] {
155
+ snapshot.activeAlerts = active.count
156
+ }
157
+ if let maintenance = health["maintenance"] as? [String: Any],
158
+ let phase = maintenance["phase"] as? String {
159
+ let message = maintenance["message"] as? String
160
+ snapshot.maintenance = message?.isEmpty == false
161
+ ? "\(titleCase(phase)) · \(shortLabel(message!))"
162
+ : titleCase(phase)
163
+ }
164
+ return snapshot
165
+ }
166
+
167
+ private static func integer(_ value: Any?) -> Int? {
168
+ if value is NSNull { return nil }
169
+ return (value as? NSNumber)?.intValue
170
+ }
171
+
172
+ private static func retentionLabel(_ value: Any) -> String {
173
+ if let text = value as? String { return text == "forever" ? "kept forever" : text }
174
+ if let days = integer(value) { return "kept \(days) days" }
175
+ return "retention unavailable"
176
+ }
177
+
178
+ private static func titleCase(_ value: String) -> String {
179
+ value.replacingOccurrences(of: "-", with: " ")
180
+ .split(separator: " ")
181
+ .map { $0.prefix(1).uppercased() + $0.dropFirst() }
182
+ .joined(separator: " ")
183
+ }
184
+
185
+ private static func shortLabel(_ value: String) -> String {
186
+ let limit = 88
187
+ return value.count > limit ? String(value.prefix(limit - 1)) + "…" : value
188
+ }
189
+
190
+ private func rebuildMenu() {
191
+ menu.removeAllItems()
192
+ configureStatusIcon()
193
+
194
+ // No em dash in anything the product says. `printedVoice.test.ts` reads this file for them.
195
+ let heading: String
196
+ switch snapshot.health {
197
+ case .healthy: heading = "Salidium · Healthy"
198
+ case .attention: heading = "Salidium · Needs Attention"
199
+ case .critical: heading = "Salidium · Critical"
200
+ case .offline: heading = "Salidium · Not Running"
201
+ }
202
+ addLabel(heading, emphasized: true)
203
+ menu.addItem(.separator())
204
+
205
+ addLabel(snapshot.pid.map { "Running · PID \($0)" } ?? "Not running")
206
+ addLabel("Recording · \(snapshot.collection)")
207
+ addLabel("Waiting to be stored · \(queueLabel())")
208
+ addLabel("On this Mac · \(storageLabel())")
209
+ addLabel(snapshot.activeAlerts == 0
210
+ ? "Nothing needs attention"
211
+ : "\(snapshot.activeAlerts) need\(snapshot.activeAlerts == 1 ? "s" : "") attention")
212
+ if let maintenance = snapshot.maintenance { addLabel("Maintenance · \(maintenance)") }
213
+
214
+ menu.addItem(.separator())
215
+ addAction("Open Salidium", #selector(openSalidium), key: "o", enabled: true)
216
+ if snapshot.pid == nil {
217
+ addAction("Start Salidium", #selector(startSalidium), enabled: true)
218
+ } else {
219
+ let paused = snapshot.collection == "Paused"
220
+ addAction(paused ? "Resume Collection" : "Pause Collection",
221
+ paused ? #selector(resumeCollection) : #selector(pauseCollection),
222
+ enabled: true)
223
+ addAction("Store One Batch Now", #selector(drainQueue), enabled: true)
224
+ addAction("Stop Salidium", #selector(stopSalidium), enabled: true)
225
+ }
226
+ addAction("Refresh Now", #selector(refreshNow), key: "r", enabled: true)
227
+ menu.addItem(.separator())
228
+ addLabel("Always-on · Starts at login")
229
+ addAction("Open Data Folder", #selector(openDataFolder), enabled: true)
230
+ addAction("Turn Off Always-On Mode…", #selector(turnOffAlwaysOn), enabled: true)
231
+ }
232
+
233
+ private func configureStatusIcon() {
234
+ let symbol: String
235
+ let color: NSColor
236
+ switch snapshot.health {
237
+ case .healthy:
238
+ symbol = "checkmark.circle.fill"
239
+ color = .systemGreen
240
+ case .attention:
241
+ symbol = "exclamationmark.triangle.fill"
242
+ color = .systemOrange
243
+ case .critical:
244
+ symbol = "exclamationmark.octagon.fill"
245
+ color = .systemRed
246
+ case .offline:
247
+ symbol = "circle.slash"
248
+ color = .secondaryLabelColor
249
+ }
250
+ let image = NSImage(systemSymbolName: symbol, accessibilityDescription: "Salidium \(snapshot.health.rawValue)")
251
+ image?.isTemplate = false
252
+ statusItem.button?.image = image
253
+ statusItem.button?.contentTintColor = color
254
+ }
255
+
256
+ private func queueLabel() -> String {
257
+ guard let files = snapshot.queueFiles, let bytes = snapshot.queueBytes else {
258
+ return "Unavailable"
259
+ }
260
+ return "\(files) \(files == 1 ? "file" : "files") · \(Self.byteLabel(bytes))"
261
+ }
262
+
263
+ private func storageLabel() -> String {
264
+ guard let bytes = snapshot.storeBytes else { return "Unavailable" }
265
+ return "\(Self.byteLabel(bytes)) · \(snapshot.retention)"
266
+ }
267
+
268
+ /*
269
+ * The same rendering as `formatBytes` in `@salidium/core`, which the app and the CLI use.
270
+ *
271
+ * Decimal, matching Finder, but not `ByteCountFormatter`: its adaptive mode renders one byte as
272
+ * "0 KB" and 999999 as "1 MB", and the same function formats rates where that loses the value.
273
+ * `byteLabelVectors` in that module pins the cases, and `macosService.test.ts` checks them
274
+ * against this function, because Swift cannot import it.
275
+ */
276
+ private static func byteLabel(_ bytes: Int) -> String {
277
+ let value = Double(bytes)
278
+ if bytes < 1000 { return "\(bytes) B" }
279
+ if bytes < 1000 * 1000 { return String(format: "%.1f KB", value / 1000) }
280
+ if bytes < 1000 * 1000 * 1000 { return String(format: "%.1f MB", value / (1000 * 1000)) }
281
+ return String(format: "%.2f GB", value / (1000 * 1000 * 1000))
282
+ }
283
+
284
+ private func addLabel(_ title: String, emphasized: Bool = false) {
285
+ let item = NSMenuItem(title: title, action: nil, keyEquivalent: "")
286
+ item.isEnabled = false
287
+ if emphasized {
288
+ item.attributedTitle = NSAttributedString(
289
+ string: title,
290
+ attributes: [.font: NSFont.systemFont(ofSize: NSFont.systemFontSize, weight: .semibold)]
291
+ )
292
+ }
293
+ menu.addItem(item)
294
+ }
295
+
296
+ private func addAction(_ title: String,
297
+ _ action: Selector,
298
+ key: String = "",
299
+ enabled: Bool) {
300
+ let item = NSMenuItem(title: title, action: action, keyEquivalent: key)
301
+ item.target = self
302
+ item.isEnabled = enabled
303
+ menu.addItem(item)
304
+ }
305
+
306
+ private func runCLI(_ arguments: [String], refreshAfter: Bool = true) {
307
+ let process = Process()
308
+ process.executableURL = URL(fileURLWithPath: configuration.node)
309
+ process.arguments = [configuration.cli] + arguments
310
+ var environment = ProcessInfo.processInfo.environment
311
+ environment["HOME"] = FileManager.default.homeDirectoryForCurrentUser.path
312
+ environment["SALIDIUM_HOME"] = configuration.home
313
+ process.environment = environment
314
+ process.standardOutput = FileHandle.nullDevice
315
+ let errorPipe = Pipe()
316
+ process.standardError = errorPipe
317
+ if refreshAfter {
318
+ process.terminationHandler = { [weak self] finished in
319
+ let data = errorPipe.fileHandleForReading.readDataToEndOfFile()
320
+ let detail = String(data: data, encoding: .utf8)?
321
+ .trimmingCharacters(in: .whitespacesAndNewlines)
322
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
323
+ guard let self = self else { return }
324
+ if finished.terminationStatus != 0 {
325
+ self.showError("Salidium could not complete that command",
326
+ detail: detail?.isEmpty == false ? detail! : "The command exited with status \(finished.terminationStatus).")
327
+ }
328
+ self.refresh()
329
+ }
330
+ }
331
+ } else {
332
+ process.standardError = FileHandle.nullDevice
333
+ }
334
+ do {
335
+ try process.run()
336
+ } catch {
337
+ showError("Salidium could not run that command", detail: error.localizedDescription)
338
+ }
339
+ }
340
+
341
+ private func showError(_ message: String, detail: String) {
342
+ NSApp.activate(ignoringOtherApps: true)
343
+ let alert = NSAlert()
344
+ alert.alertStyle = .warning
345
+ alert.messageText = message
346
+ alert.informativeText = detail
347
+ alert.runModal()
348
+ }
349
+
350
+ @objc private func openSalidium() { runCLI(["open"]) }
351
+ @objc private func startSalidium() { runCLI(["start"]) }
352
+ @objc private func pauseCollection() { runCLI(["pause", "--quiet"]) }
353
+ @objc private func resumeCollection() { runCLI(["resume", "--quiet"]) }
354
+ @objc private func drainQueue() { runCLI(["maintenance", "drain", "--quiet"]) }
355
+ @objc private func stopSalidium() { runCLI(["stop", "--quiet"]) }
356
+ @objc private func refreshNow() { refresh() }
357
+
358
+ @objc private func openDataFolder() {
359
+ NSWorkspace.shared.open(URL(fileURLWithPath: configuration.home, isDirectory: true))
360
+ }
361
+
362
+ @objc private func turnOffAlwaysOn() {
363
+ NSApp.activate(ignoringOtherApps: true)
364
+ let alert = NSAlert()
365
+ alert.alertStyle = .warning
366
+ alert.messageText = "Turn off Salidium always-on mode?"
367
+ alert.informativeText = "This stops Salidium and removes its menu-bar control until you run “salidium service enable”. Your reports and settings stay on this Mac."
368
+ alert.addButton(withTitle: "Turn Off")
369
+ alert.addButton(withTitle: "Cancel")
370
+ guard alert.runModal() == .alertFirstButtonReturn else { return }
371
+ runCLI(["service", "disable"], refreshAfter: false)
372
+ }
373
+ }
374
+
375
+ guard let configuration = Configuration.parse(CommandLine.arguments) else {
376
+ FileHandle.standardError.write(Data("usage: salidium-menubar --home PATH --node PATH --cli PATH\n".utf8))
377
+ exit(2)
378
+ }
379
+
380
+ let application = NSApplication.shared
381
+ private let delegate = AppDelegate(configuration: configuration)
382
+ application.delegate = delegate
383
+ application.run()