sneakoscope 6.7.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +71 -0
- package/dist/cli/install-helpers-codex-lb-selftest.js +11 -14
- package/dist/cli/install-helpers.js +305 -53
- package/dist/config/skills-manifest.json +52 -52
- package/dist/core/agents/agent-orchestrator.js +56 -5
- package/dist/core/agents/agent-roster.js +4 -2
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +18 -1
- package/dist/core/codex-app/codex-app-ui-state-snapshot.js +16 -0
- package/dist/core/codex-app.js +52 -6
- package/dist/core/hooks-runtime.js +77 -3
- package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
- package/dist/core/release/package-size-budget.js +1 -1
- package/dist/core/subagents/official-subagent-config.js +8 -5
- package/dist/core/subagents/official-subagent-preparation.js +24 -1
- package/dist/core/subagents/official-subagent-prompt.js +3 -2
- package/dist/core/subagents/wave-lifecycle.js +20 -0
- package/dist/core/subagents/wave-parent-guidance.js +42 -0
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/AppIdentity.swift +3 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +14 -8
- package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +71 -7
- package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +50 -9
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +61 -8
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +120 -29
- package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +58 -23
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +41 -7
- package/dist/native/sks-menubar/Sources/StatusItemController.swift +34 -3
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +38 -8
- package/dist/scripts/docs-truthfulness-check.js +2 -2
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +7 -3
- package/package.json +1 -1
|
@@ -65,7 +65,7 @@ private struct McpRow {
|
|
|
65
65
|
|
|
66
66
|
private struct McpDraft { let scope: String; let payload: [String: Any] }
|
|
67
67
|
|
|
68
|
-
final class MCPServersViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
|
|
68
|
+
final class MCPServersViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate, ControlCenterPage {
|
|
69
69
|
private let processClient: ProcessClient
|
|
70
70
|
private let operations: OperationCoordinator
|
|
71
71
|
private let notifications: NotificationCoordinator
|
|
@@ -197,11 +197,18 @@ final class MCPServersViewController: NSViewController, NSTableViewDataSource, N
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
func refreshOnAppear() { refresh() }
|
|
201
|
+
|
|
200
202
|
@objc private func testConnection() {
|
|
201
|
-
guard let selection = selected(),
|
|
203
|
+
guard let selection = selected(), selection.row.managedBy != "plugin" else { return }
|
|
202
204
|
status.stringValue = "Testing \(selection.row.name) with bounded initialize and tools/list…"
|
|
203
|
-
processClient.run(["mcp", "config", "test", selection.row.name, "--scope", selection.row.scope] + scopeContext(selection.row.scope, mutation: false) + ["--json"]) { [weak self] result in
|
|
204
|
-
guard let self = self, let json = self.json(result.output) else {
|
|
205
|
+
processClient.run(["mcp", "config", "test", selection.row.name, "--scope", selection.row.scope] + scopeContext(selection.row.scope, mutation: false) + ["--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
206
|
+
guard let self = self, let json = self.json(result.output) else {
|
|
207
|
+
self?.status.stringValue = result.code == 0
|
|
208
|
+
? "Connection test failed without a readable receipt."
|
|
209
|
+
: "Connection test failed · \(NativeView.redactPreview(result.output))"
|
|
210
|
+
return
|
|
211
|
+
}
|
|
205
212
|
let state = json["status"] as? String ?? "unknown"
|
|
206
213
|
let latency = json["latency_ms"] as? Int
|
|
207
214
|
let tools = json["tool_count"] as? Int
|
|
@@ -363,6 +370,8 @@ private final class McpEditorSheet: NSObject {
|
|
|
363
370
|
|
|
364
371
|
private func build() {
|
|
365
372
|
panel.title = row == nil ? "Add MCP Server" : "Edit \(row!.name)"
|
|
373
|
+
panel.styleMask = [.titled, .resizable]
|
|
374
|
+
panel.minSize = NSSize(width: 560, height: 480)
|
|
366
375
|
let grid = NSGridView(views: [
|
|
367
376
|
pair("Scope", scope), pair("Transport", transport), pair("Name", name), pair("Executable", command), pair("Remote URL", url),
|
|
368
377
|
pair("Arguments — one per line", area(args, "Command arguments")), pair("Working directory", cwd), pair("Environment names — no values", area(env, "Environment-variable names")),
|
|
@@ -399,20 +408,52 @@ private final class McpEditorSheet: NSObject {
|
|
|
399
408
|
guard (payload["startup_timeout_sec"] as? Int).map({ 1...30 ~= $0 }) == true, (payload["tool_timeout_sec"] as? Int).map({ 1...3600 ~= $0 }) == true else { return nil }
|
|
400
409
|
if selectedTransport == "stdio" {
|
|
401
410
|
let executable = command.stringValue.trimmingCharacters(in: .whitespacesAndNewlines); if row == nil || row?.transport != selectedTransport { guard !executable.isEmpty else { return nil } }; if !executable.isEmpty { payload["command"] = executable }
|
|
402
|
-
let argv =
|
|
403
|
-
let names =
|
|
411
|
+
let argv = orderedLines(args.string); if argv.contains(where: { $0.contains("=") || $0.lowercased().contains("token") || $0.lowercased().contains("secret") }) { return nil }; if !argv.isEmpty { payload["args"] = argv }
|
|
412
|
+
let names = uniqueSortedLines(env.string); guard names.allSatisfy({ $0.range(of: #"^[A-Za-z_][A-Za-z0-9_]*$"#, options: .regularExpression) != nil }) else { return nil }; if !names.isEmpty { payload["env_vars"] = names }
|
|
404
413
|
let directory = cwd.stringValue.trimmingCharacters(in: .whitespacesAndNewlines); if !directory.isEmpty { guard directory.hasPrefix("/") else { return nil }; payload["cwd"] = directory }
|
|
405
414
|
if remote.state == .on { payload["experimental_environment"] = "remote" }
|
|
406
415
|
} else {
|
|
407
416
|
let endpoint = url.stringValue.trimmingCharacters(in: .whitespacesAndNewlines); if row == nil || row?.transport != selectedTransport { guard !endpoint.isEmpty else { return nil } }; if !endpoint.isEmpty { guard let parsed = URL(string: endpoint), ["http", "https"].contains(parsed.scheme?.lowercased() ?? ""), parsed.user == nil, parsed.password == nil else { return nil }; payload["url"] = endpoint }
|
|
408
417
|
let envName = bearer.stringValue.trimmingCharacters(in: .whitespacesAndNewlines); if !envName.isEmpty { guard envName.range(of: #"^[A-Za-z_][A-Za-z0-9_]*$"#, options: .regularExpression) != nil else { return nil }; payload["bearer_token_env_var"] = envName }
|
|
409
418
|
}
|
|
410
|
-
let allow =
|
|
419
|
+
let allow = uniqueSortedLines(enabledTools.string); let deny = uniqueSortedLines(disabledTools.string); guard Set(allow).isDisjoint(with: Set(deny)) else { return nil }; if !allow.isEmpty { payload["enabled_tools"] = allow }; if !deny.isEmpty { payload["disabled_tools"] = deny }
|
|
411
420
|
return McpDraft(scope: scope.titleOfSelectedItem?.lowercased() ?? "global", payload: payload)
|
|
412
421
|
}
|
|
413
422
|
|
|
414
|
-
private func validationMessage() -> String {
|
|
423
|
+
private func validationMessage() -> String {
|
|
424
|
+
let server = name.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
425
|
+
if server.range(of: #"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"#, options: .regularExpression) == nil {
|
|
426
|
+
return "Fix the server name: use 1–64 letters, numbers, underscores, or hyphens."
|
|
427
|
+
}
|
|
428
|
+
if transport.indexOfSelectedItem == 0 {
|
|
429
|
+
let executable = command.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
430
|
+
if (row == nil || row?.transport != "stdio") && executable.isEmpty { return "Executable is required for a local command server." }
|
|
431
|
+
if orderedLines(args.string).contains(where: { $0.contains("=") || $0.lowercased().contains("token") || $0.lowercased().contains("secret") }) {
|
|
432
|
+
return "Arguments must stay secret-free. Remove token/secret values and inline assignments."
|
|
433
|
+
}
|
|
434
|
+
if !uniqueSortedLines(env.string).allSatisfy({ $0.range(of: #"^[A-Za-z_][A-Za-z0-9_]*$"#, options: .regularExpression) != nil }) {
|
|
435
|
+
return "Environment names must be bare variable names with no values."
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
let endpoint = url.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
439
|
+
if (row == nil || row?.transport != "streamable-http") && endpoint.isEmpty { return "Remote URL is required for a remote MCP server." }
|
|
440
|
+
}
|
|
441
|
+
let startupSec = Int(startup.stringValue) ?? 0
|
|
442
|
+
let toolSec = Int(tool.stringValue) ?? 0
|
|
443
|
+
if !(1...30 ~= startupSec) { return "Startup timeout must be between 1 and 30 seconds." }
|
|
444
|
+
if !(1...3600 ~= toolSec) { return "Tool timeout must be between 1 and 3600 seconds." }
|
|
445
|
+
let allow = uniqueSortedLines(enabledTools.string); let deny = uniqueSortedLines(disabledTools.string)
|
|
446
|
+
if !Set(allow).isDisjoint(with: Set(deny)) { return "Enabled and disabled tool lists must not overlap." }
|
|
447
|
+
return "Check the selected connection field, timeout ranges, environment names, tool lists, and secret-free arguments before review."
|
|
448
|
+
}
|
|
415
449
|
private func pair(_ label: String, _ control: NSView) -> [NSView] { [NSTextField(labelWithString: label), control] }
|
|
416
450
|
private func area(_ text: NSTextView, _ label: String) -> NSScrollView { text.isRichText = false; text.font = NSFont.systemFont(ofSize: 12); text.setAccessibilityLabel(label); let scroll = NSScrollView(); scroll.documentView = text; scroll.hasVerticalScroller = true; scroll.borderType = .bezelBorder; scroll.heightAnchor.constraint(equalToConstant: 52).isActive = true; return scroll }
|
|
417
|
-
private func
|
|
451
|
+
private func uniqueSortedLines(_ value: String) -> [String] { Array(Set(rawLines(value))).sorted() }
|
|
452
|
+
private func orderedLines(_ value: String) -> [String] {
|
|
453
|
+
var seen = Set<String>()
|
|
454
|
+
return rawLines(value).filter { seen.insert($0).inserted }
|
|
455
|
+
}
|
|
456
|
+
private func rawLines(_ value: String) -> [String] {
|
|
457
|
+
value.split(whereSeparator: \.isNewline).map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }
|
|
458
|
+
}
|
|
418
459
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import Cocoa
|
|
2
2
|
|
|
3
|
+
/// Pages that should reload local status whenever the Control Center section becomes visible.
|
|
4
|
+
protocol ControlCenterPage: AnyObject {
|
|
5
|
+
func refreshOnAppear()
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
enum NativeView {
|
|
9
|
+
static let statusTimeout: TimeInterval = 8
|
|
10
|
+
static let mutationTimeout: TimeInterval = 90
|
|
11
|
+
|
|
4
12
|
static func title(_ value: String) -> NSTextField {
|
|
5
13
|
let field = NSTextField(labelWithString: value)
|
|
6
14
|
field.font = NSFont.systemFont(ofSize: 18, weight: .semibold)
|
|
@@ -29,6 +37,35 @@ enum NativeView {
|
|
|
29
37
|
stack.edgeInsets = NSEdgeInsets(top: 22, left: 24, bottom: 22, right: 24)
|
|
30
38
|
return stack
|
|
31
39
|
}
|
|
40
|
+
|
|
41
|
+
static func scrollable(_ document: NSView) -> NSScrollView {
|
|
42
|
+
let scroll = NSScrollView()
|
|
43
|
+
scroll.drawsBackground = false
|
|
44
|
+
scroll.hasVerticalScroller = true
|
|
45
|
+
scroll.hasHorizontalScroller = false
|
|
46
|
+
scroll.borderType = .noBorder
|
|
47
|
+
scroll.autohidesScrollers = true
|
|
48
|
+
scroll.translatesAutoresizingMaskIntoConstraints = false
|
|
49
|
+
document.translatesAutoresizingMaskIntoConstraints = false
|
|
50
|
+
scroll.documentView = document
|
|
51
|
+
if let content = scroll.contentView.documentView {
|
|
52
|
+
NSLayoutConstraint.activate([
|
|
53
|
+
content.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
|
|
54
|
+
content.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
|
|
55
|
+
content.topAnchor.constraint(equalTo: scroll.contentView.topAnchor)
|
|
56
|
+
])
|
|
57
|
+
}
|
|
58
|
+
return scroll
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static func redactPreview(_ output: String, limit: Int = 160) -> String {
|
|
62
|
+
let compact = output
|
|
63
|
+
.replacingOccurrences(of: "\n", with: " ")
|
|
64
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
65
|
+
guard !compact.isEmpty else { return "No public detail was returned." }
|
|
66
|
+
if compact.count <= limit { return compact }
|
|
67
|
+
return String(compact.prefix(limit)) + "…"
|
|
68
|
+
}
|
|
32
69
|
}
|
|
33
70
|
|
|
34
71
|
enum OverviewSummary {
|
|
@@ -182,12 +219,14 @@ enum OverviewSummary {
|
|
|
182
219
|
}
|
|
183
220
|
}
|
|
184
221
|
|
|
185
|
-
final class OverviewViewController: NSViewController {
|
|
222
|
+
final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
186
223
|
private let processClient: ProcessClient
|
|
187
224
|
private let operations: OperationCoordinator
|
|
188
225
|
private let status = NativeView.detail("Loading local SKS status…")
|
|
189
226
|
private let notificationInbox = NativeView.detail("Notifications: checking authorization…")
|
|
190
227
|
private var generation = 0
|
|
228
|
+
private var doctorButton: NSButton!
|
|
229
|
+
private var refreshButton: NSButton!
|
|
191
230
|
|
|
192
231
|
init(processClient: ProcessClient, operations: OperationCoordinator) {
|
|
193
232
|
self.processClient = processClient
|
|
@@ -197,9 +236,9 @@ final class OverviewViewController: NSViewController {
|
|
|
197
236
|
required init?(coder: NSCoder) { nil }
|
|
198
237
|
|
|
199
238
|
override func loadView() {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
let buttons = NSStackView(views: [
|
|
239
|
+
doctorButton = NativeView.button("Run Doctor", target: self, action: #selector(doctor))
|
|
240
|
+
refreshButton = NativeView.button("Refresh", target: self, action: #selector(refreshStatus))
|
|
241
|
+
let buttons = NSStackView(views: [doctorButton, refreshButton])
|
|
203
242
|
buttons.orientation = .horizontal
|
|
204
243
|
buttons.spacing = 8
|
|
205
244
|
view = NativeView.stack([
|
|
@@ -212,6 +251,10 @@ final class OverviewViewController: NSViewController {
|
|
|
212
251
|
loadStatus(forceUpdateRefresh: false)
|
|
213
252
|
}
|
|
214
253
|
|
|
254
|
+
func refreshOnAppear() {
|
|
255
|
+
loadStatus(forceUpdateRefresh: false)
|
|
256
|
+
}
|
|
257
|
+
|
|
215
258
|
func setNotificationAuthorizationDenied(_ denied: Bool) {
|
|
216
259
|
notificationInbox.stringValue = denied
|
|
217
260
|
? "Notifications: permission denied — operation results remain available in this Control Center inbox."
|
|
@@ -225,6 +268,7 @@ final class OverviewViewController: NSViewController {
|
|
|
225
268
|
private func loadStatus(forceUpdateRefresh: Bool) {
|
|
226
269
|
generation += 1
|
|
227
270
|
let requestGeneration = generation
|
|
271
|
+
refreshButton?.isEnabled = false
|
|
228
272
|
status.stringValue = "Checking versions, MCP servers, Telegram Hub, remote fleet, and operations…"
|
|
229
273
|
var update: [String: Any]?
|
|
230
274
|
var mcp: [String: Any]?
|
|
@@ -234,7 +278,7 @@ final class OverviewViewController: NSViewController {
|
|
|
234
278
|
var updateArguments = ["update", "status"]
|
|
235
279
|
if forceUpdateRefresh { updateArguments.append("--refresh") }
|
|
236
280
|
updateArguments.append("--json")
|
|
237
|
-
processClient.run(updateArguments) { [weak self] result in
|
|
281
|
+
processClient.run(updateArguments, timeout: NativeView.statusTimeout) { [weak self] result in
|
|
238
282
|
update = result.code == 0 ? self?.json(result.output) : nil
|
|
239
283
|
group.leave()
|
|
240
284
|
}
|
|
@@ -247,24 +291,33 @@ final class OverviewViewController: NSViewController {
|
|
|
247
291
|
group.leave()
|
|
248
292
|
}
|
|
249
293
|
group.enter()
|
|
250
|
-
processClient.run(["telegram", "status", "--project-root", AppRuntime.projectRoot, "--json"]) { [weak self] result in
|
|
294
|
+
processClient.run(["telegram", "status", "--project-root", AppRuntime.projectRoot, "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
251
295
|
telegram = result.code == 0 ? self?.json(result.output) : nil
|
|
252
296
|
group.leave()
|
|
253
297
|
}
|
|
254
298
|
group.notify(queue: .main) { [weak self] in
|
|
255
299
|
guard let self = self, self.generation == requestGeneration else { return }
|
|
300
|
+
self.refreshButton?.isEnabled = true
|
|
256
301
|
self.status.stringValue = self.summary(update: update, mcp: mcp, telegram: telegram)
|
|
257
302
|
}
|
|
258
303
|
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
|
|
259
304
|
guard let self = self, self.generation == requestGeneration else { return }
|
|
305
|
+
self.refreshButton?.isEnabled = true
|
|
260
306
|
self.status.stringValue = self.summary(update: update, mcp: mcp, telegram: telegram)
|
|
261
307
|
}
|
|
262
308
|
}
|
|
263
309
|
|
|
264
310
|
@objc private func doctor() {
|
|
311
|
+
doctorButton?.isEnabled = false
|
|
265
312
|
status.stringValue = "Doctor is running…"
|
|
266
|
-
processClient.run(["doctor", "--json"]) { [weak self] result in
|
|
267
|
-
self
|
|
313
|
+
processClient.run(["doctor", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
314
|
+
guard let self = self else { return }
|
|
315
|
+
self.doctorButton?.isEnabled = true
|
|
316
|
+
if result.code == 0 {
|
|
317
|
+
self.status.stringValue = "Doctor completed. No blocking issue was reported."
|
|
318
|
+
} else {
|
|
319
|
+
self.status.stringValue = "Doctor found an issue. Open Diagnostics · \(NativeView.redactPreview(result.output))"
|
|
320
|
+
}
|
|
268
321
|
}
|
|
269
322
|
}
|
|
270
323
|
|
|
@@ -1,51 +1,120 @@
|
|
|
1
1
|
import Cocoa
|
|
2
2
|
|
|
3
|
-
final class ProvidersViewController: NSViewController {
|
|
3
|
+
final class ProvidersViewController: NSViewController, ControlCenterPage {
|
|
4
4
|
private let processClient: ProcessClient
|
|
5
|
+
private let operations: OperationCoordinator
|
|
5
6
|
private let providerStatus = NativeView.detail("Provider status unchecked.")
|
|
6
7
|
private let fastStatus = NativeView.detail("Fast Mode: checking current desktop setting…")
|
|
7
|
-
|
|
8
|
+
private var actionButtons: [NSButton] = []
|
|
9
|
+
private var busy = false
|
|
10
|
+
|
|
11
|
+
init(processClient: ProcessClient, operations: OperationCoordinator) {
|
|
12
|
+
self.processClient = processClient
|
|
13
|
+
self.operations = operations
|
|
14
|
+
super.init(nibName: nil, bundle: nil)
|
|
15
|
+
}
|
|
8
16
|
required init?(coder: NSCoder) { nil }
|
|
9
17
|
|
|
10
18
|
override func loadView() {
|
|
11
|
-
let
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
let setDomain = NativeView.button("Set Domain and Key…", target: self, action: #selector(setDomainAndKey))
|
|
20
|
+
let replace = NativeView.button("Replace Key…", target: self, action: #selector(replaceKey))
|
|
21
|
+
let useOAuth = NativeView.button("Use ChatGPT OAuth", target: self, action: #selector(useOAuth))
|
|
22
|
+
let useLb = NativeView.button("Use codex-lb", target: self, action: #selector(useCodexLb))
|
|
23
|
+
let fastOn = NativeView.button("Fast Mode On", target: self, action: #selector(fastOn))
|
|
24
|
+
let fastOff = NativeView.button("Fast Mode Off", target: self, action: #selector(fastOff))
|
|
25
|
+
actionButtons = [setDomain, replace, useOAuth, useLb, fastOn, fastOff]
|
|
26
|
+
let credentials = NSStackView(views: [setDomain, replace])
|
|
15
27
|
credentials.orientation = .horizontal; credentials.spacing = 8
|
|
16
|
-
let buttons = NSStackView(views: [
|
|
17
|
-
NativeView.button("Use ChatGPT OAuth", target: self, action: #selector(useOAuth)),
|
|
18
|
-
NativeView.button("Use codex-lb", target: self, action: #selector(useCodexLb)),
|
|
19
|
-
NativeView.button("Fast Mode On", target: self, action: #selector(fastOn)),
|
|
20
|
-
NativeView.button("Fast Mode Off", target: self, action: #selector(fastOff))
|
|
21
|
-
])
|
|
28
|
+
let buttons = NSStackView(views: [useOAuth, useLb, fastOn, fastOff])
|
|
22
29
|
buttons.orientation = .horizontal; buttons.spacing = 8
|
|
23
30
|
view = NativeView.stack([
|
|
24
31
|
NativeView.title("Providers"),
|
|
25
|
-
NativeView.detail("
|
|
32
|
+
NativeView.detail("Set Domain and Key only stores credentials. Use codex-lb activates them with a shared OpenAI routing guard so sk-clb keys never hit api.openai.com. Secrets stay on stdin and out of logs."),
|
|
26
33
|
credentials, providerStatus, fastStatus, buttons
|
|
27
34
|
])
|
|
28
35
|
refresh()
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
func refreshOnAppear() { refresh() }
|
|
39
|
+
|
|
40
|
+
private func setBusy(_ value: Bool) {
|
|
41
|
+
busy = value
|
|
42
|
+
for button in actionButtons { button.isEnabled = !value }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private func run(_ args: [String], title: String, kind: String, group: String?, timeout: TimeInterval = NativeView.mutationTimeout, completion: (() -> Void)? = nil) {
|
|
46
|
+
guard !busy else {
|
|
47
|
+
providerStatus.stringValue = "Another provider action is already running."
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
guard let snapshot = operations.begin(kind: kind, mutationGroup: group, summary: title) else {
|
|
51
|
+
providerStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
setBusy(true)
|
|
32
55
|
providerStatus.stringValue = "\(title)…"
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
_ = operations.update(snapshot, state: .running, stage: "running", progress: nil, summary: title)
|
|
57
|
+
processClient.run(args, timeout: timeout) { [weak self] result in
|
|
58
|
+
guard let self = self else { return }
|
|
59
|
+
self.setBusy(false)
|
|
60
|
+
_ = self.operations.update(
|
|
61
|
+
snapshot,
|
|
62
|
+
state: result.code == 0 ? .succeeded : .failed,
|
|
63
|
+
stage: "complete",
|
|
64
|
+
progress: 1,
|
|
65
|
+
summary: result.code == 0 ? "\(title) completed" : "\(title) failed"
|
|
66
|
+
)
|
|
67
|
+
if result.code != 0 {
|
|
68
|
+
self.providerStatus.stringValue = "\(title) failed · \(NativeView.redactPreview(result.output))"
|
|
69
|
+
}
|
|
70
|
+
self.refresh()
|
|
35
71
|
completion?()
|
|
36
72
|
}
|
|
37
73
|
}
|
|
38
74
|
|
|
39
75
|
private func refresh() {
|
|
40
|
-
processClient.run(["codex-lb", "status", "--json"]) { [weak self] result in
|
|
41
|
-
self
|
|
76
|
+
processClient.run(["codex-lb", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
77
|
+
guard let self = self else { return }
|
|
78
|
+
guard result.code == 0, let json = self.json(result.output) else {
|
|
79
|
+
if !self.busy { self.providerStatus.stringValue = "Provider status unavailable. Retry or open Diagnostics." }
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if !self.busy { self.providerStatus.stringValue = self.describeProviderStatus(json) }
|
|
42
83
|
}
|
|
43
84
|
refreshFastStatus()
|
|
44
85
|
}
|
|
45
86
|
|
|
87
|
+
private func describeProviderStatus(_ json: [String: Any]) -> String {
|
|
88
|
+
let configured = json["provider_configured"] as? Bool == true
|
|
89
|
+
let selected = json["selected"] as? Bool == true
|
|
90
|
+
let ok = json["ok"] as? Bool == true
|
|
91
|
+
let authMode = json["auth_mode"] as? String ?? "unknown"
|
|
92
|
+
let coherent = json["auth_routing_coherent"] as? Bool
|
|
93
|
+
let routing = json["shared_openai_routing"] as? [String: Any]
|
|
94
|
+
let routingSafe = routing?["safe"] as? Bool
|
|
95
|
+
let keyInShared = json["codex_lb_key_in_shared_auth"] as? Bool == true
|
|
96
|
+
|
|
97
|
+
if !configured {
|
|
98
|
+
return "Codex LB: not configured. Use Set Domain and Key, then Use codex-lb."
|
|
99
|
+
}
|
|
100
|
+
if routingSafe == false || (keyInShared && coherent == false) {
|
|
101
|
+
return "Codex LB: routing unsafe — shared key can hit api.openai.com. Click Use codex-lb to repair."
|
|
102
|
+
}
|
|
103
|
+
if selected && ok {
|
|
104
|
+
return "Codex LB: active (auth=\(authMode), routing guarded)."
|
|
105
|
+
}
|
|
106
|
+
if selected && !ok {
|
|
107
|
+
return "Codex LB: selected but not ready. Click Use codex-lb or check Diagnostics."
|
|
108
|
+
}
|
|
109
|
+
if keyInShared {
|
|
110
|
+
return "Codex LB: credentials in shared auth but provider not selected. Click Use ChatGPT OAuth or Use codex-lb."
|
|
111
|
+
}
|
|
112
|
+
return "Codex LB: credentials stored, not selected (auth=\(authMode)). Click Use codex-lb to activate."
|
|
113
|
+
}
|
|
114
|
+
|
|
46
115
|
private func refreshFastStatus() {
|
|
47
|
-
fastStatus.stringValue = "Fast Mode: checking current desktop setting…"
|
|
48
|
-
processClient.run(["fast-mode", "status", "--json"]) { [weak self] result in
|
|
116
|
+
if !busy { fastStatus.stringValue = "Fast Mode: checking current desktop setting…" }
|
|
117
|
+
processClient.run(["fast-mode", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
49
118
|
guard let self = self else { return }
|
|
50
119
|
guard result.code == 0, let json = self.json(result.output),
|
|
51
120
|
let global = json["global"] as? [String: Any], let on = global["on"] as? Bool else {
|
|
@@ -66,27 +135,49 @@ final class ProvidersViewController: NSViewController {
|
|
|
66
135
|
guard let window = view.window else { return }
|
|
67
136
|
AlertFactory.textSheet(window: window, title: "Codex LB Domain", message: "Enter a codex-lb domain or base URL.") { [weak self] host in
|
|
68
137
|
guard let self = self, let host = host else { return }
|
|
69
|
-
self.promptForKey(window: window, args: ["codex-lb", "setup", "--host", host, "--api-key-stdin", "--yes", "--no-default-provider", "--preserve-auth", "--no-keychain", "--no-restart-app", "--json"])
|
|
138
|
+
self.promptForKey(window: window, args: ["codex-lb", "setup", "--host", host, "--api-key-stdin", "--yes", "--no-default-provider", "--preserve-auth", "--no-keychain", "--no-restart-app", "--json"], kind: "codex-lb-setup", title: "Save Codex LB credentials")
|
|
70
139
|
}
|
|
71
140
|
}
|
|
72
141
|
|
|
73
142
|
@objc private func replaceKey() {
|
|
74
143
|
guard let window = view.window else { return }
|
|
75
|
-
promptForKey(window: window, args: ["codex-lb", "set-key", "--api-key-stdin", "--preserve-auth", "--restart-app", "--json"])
|
|
144
|
+
promptForKey(window: window, args: ["codex-lb", "set-key", "--api-key-stdin", "--preserve-auth", "--restart-app", "--json"], kind: "codex-lb-set-key", title: "Replace Codex LB API key")
|
|
76
145
|
}
|
|
77
146
|
|
|
78
|
-
private func promptForKey(window: NSWindow, args: [String]) {
|
|
147
|
+
private func promptForKey(window: NSWindow, args: [String], kind: String, title: String) {
|
|
79
148
|
AlertFactory.textSheet(window: window, title: "Codex LB API Key", message: "Paste a new key. It is sent through stdin and never shown again.", secure: true) { [weak self] key in
|
|
80
149
|
guard let self = self, let key = key else { return }
|
|
150
|
+
guard !self.busy else {
|
|
151
|
+
self.providerStatus.stringValue = "Another provider action is already running."
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
guard let snapshot = self.operations.begin(kind: kind, mutationGroup: "codex-config", summary: title) else {
|
|
155
|
+
self.providerStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
self.setBusy(true)
|
|
81
159
|
self.providerStatus.stringValue = "Saving Codex LB API key…"
|
|
82
|
-
self.
|
|
83
|
-
|
|
160
|
+
_ = self.operations.update(snapshot, state: .running, stage: "running", progress: nil, summary: title)
|
|
161
|
+
self.processClient.run(args, stdin: key + "\n", timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
162
|
+
guard let self = self else { return }
|
|
163
|
+
self.setBusy(false)
|
|
164
|
+
_ = self.operations.update(
|
|
165
|
+
snapshot,
|
|
166
|
+
state: result.code == 0 ? .succeeded : .failed,
|
|
167
|
+
stage: "complete",
|
|
168
|
+
progress: 1,
|
|
169
|
+
summary: result.code == 0 ? "Codex LB API key saved" : "Codex LB API key save failed"
|
|
170
|
+
)
|
|
171
|
+
if result.code != 0 {
|
|
172
|
+
self.providerStatus.stringValue = "Codex LB API key save failed · \(NativeView.redactPreview(result.output))"
|
|
173
|
+
}
|
|
174
|
+
self.refresh()
|
|
84
175
|
}
|
|
85
176
|
}
|
|
86
177
|
}
|
|
87
178
|
|
|
88
|
-
@objc private func useOAuth() { run(["codex-lb", "use-oauth", "--restart-app", "--json"], title: "Use ChatGPT OAuth") }
|
|
89
|
-
@objc private func useCodexLb() { run(["codex-lb", "use-codex-lb", "--restart-app", "--json"], title: "Use codex-lb") }
|
|
90
|
-
@objc private func fastOn() { run(["fast-mode", "on", "--json"], title: "Fast Mode On") { [weak self] in self?.refreshFastStatus() } }
|
|
91
|
-
@objc private func fastOff() { run(["fast-mode", "off", "--json"], title: "Fast Mode Off") { [weak self] in self?.refreshFastStatus() } }
|
|
179
|
+
@objc private func useOAuth() { run(["codex-lb", "use-oauth", "--restart-app", "--json"], title: "Use ChatGPT OAuth", kind: "codex-lb-use-oauth", group: "codex-config") }
|
|
180
|
+
@objc private func useCodexLb() { run(["codex-lb", "use-codex-lb", "--restart-app", "--json"], title: "Use codex-lb", kind: "codex-lb-use-lb", group: "codex-config") }
|
|
181
|
+
@objc private func fastOn() { run(["fast-mode", "on", "--json"], title: "Fast Mode On", kind: "fast-mode-on", group: "codex-config", timeout: NativeView.statusTimeout) { [weak self] in self?.refreshFastStatus() } }
|
|
182
|
+
@objc private func fastOff() { run(["fast-mode", "off", "--json"], title: "Fast Mode Off", kind: "fast-mode-off", group: "codex-config", timeout: NativeView.statusTimeout) { [weak self] in self?.refreshFastStatus() } }
|
|
92
183
|
}
|
|
@@ -1,61 +1,96 @@
|
|
|
1
1
|
import Cocoa
|
|
2
2
|
|
|
3
|
-
final class RemoteTelegramViewController: NSViewController {
|
|
3
|
+
final class RemoteTelegramViewController: NSViewController, ControlCenterPage {
|
|
4
4
|
private let processClient: ProcessClient
|
|
5
5
|
private let status = NativeView.detail("Remote readiness has not been checked.")
|
|
6
6
|
private var generation = 0
|
|
7
|
+
private var checkButton: NSButton!
|
|
8
|
+
|
|
7
9
|
init(processClient: ProcessClient) { self.processClient = processClient; super.init(nibName: nil, bundle: nil) }
|
|
8
10
|
required init?(coder: NSCoder) { nil }
|
|
9
11
|
|
|
10
12
|
override func loadView() {
|
|
11
|
-
|
|
13
|
+
checkButton = NativeView.button("Check Readiness", target: self, action: #selector(check))
|
|
12
14
|
view = NativeView.stack([
|
|
13
15
|
NativeView.title("Remote & Telegram"),
|
|
14
16
|
NativeView.detail("OpenAI Remote remains the high-fidelity coding surface. Telegram is limited to proof-aware fleet status and typed approvals; arbitrary remote shell is never enabled."),
|
|
15
|
-
status,
|
|
17
|
+
status, checkButton
|
|
16
18
|
])
|
|
17
19
|
check()
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
func refreshOnAppear() { check() }
|
|
23
|
+
|
|
20
24
|
@objc private func check() {
|
|
21
25
|
generation += 1
|
|
22
26
|
let requestGeneration = generation
|
|
27
|
+
checkButton?.isEnabled = false
|
|
23
28
|
status.stringValue = "Checking official Remote compatibility and Telegram Hub state…"
|
|
24
29
|
var remote: [String: Any]?
|
|
25
30
|
var telegram: [String: Any]?
|
|
31
|
+
var remoteFailed = false
|
|
32
|
+
var telegramFailed = false
|
|
26
33
|
let group = DispatchGroup()
|
|
27
34
|
group.enter()
|
|
28
|
-
processClient.run(["remote", "readiness", "--project-root", AppRuntime.projectRoot, "--json"]) { [weak self] result in
|
|
35
|
+
processClient.run(["remote", "readiness", "--project-root", AppRuntime.projectRoot, "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
29
36
|
remote = self?.json(result.output)
|
|
37
|
+
remoteFailed = result.code != 0 || remote == nil
|
|
30
38
|
group.leave()
|
|
31
39
|
}
|
|
32
40
|
group.enter()
|
|
33
|
-
processClient.run(["telegram", "status", "--project-root", AppRuntime.projectRoot, "--json"]) { [weak self] result in
|
|
34
|
-
telegram = self?.json(result.output)
|
|
41
|
+
processClient.run(["telegram", "status", "--project-root", AppRuntime.projectRoot, "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
42
|
+
telegram = result.code == 0 ? self?.json(result.output) : nil
|
|
43
|
+
telegramFailed = result.code != 0 || telegram == nil
|
|
35
44
|
group.leave()
|
|
36
45
|
}
|
|
37
46
|
group.notify(queue: .main) { [weak self] in
|
|
38
47
|
guard let self = self, self.generation == requestGeneration else { return }
|
|
39
|
-
self.
|
|
48
|
+
self.checkButton?.isEnabled = true
|
|
49
|
+
self.status.stringValue = self.summary(remote: remote, telegram: telegram, remoteFailed: remoteFailed, telegramFailed: telegramFailed)
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
private func summary(remote: [String: Any]?, telegram: [String: Any]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"Remote
|
|
58
|
-
|
|
53
|
+
private func summary(remote: [String: Any]?, telegram: [String: Any]?, remoteFailed: Bool, telegramFailed: Bool) -> String {
|
|
54
|
+
if remoteFailed && telegramFailed {
|
|
55
|
+
return "Remote readiness and Telegram status are unavailable. Retry Check Readiness, or open Diagnostics for the redacted log."
|
|
56
|
+
}
|
|
57
|
+
var lines: [String] = []
|
|
58
|
+
if remoteFailed || remote == nil {
|
|
59
|
+
lines.append("Official Remote compatibility: unavailable — probe failed or returned no readable JSON.")
|
|
60
|
+
} else if let remote = remote {
|
|
61
|
+
let host = remote["host"] as? [String: Any]
|
|
62
|
+
let project = remote["project"] as? [String: Any]
|
|
63
|
+
let mcp = remote["mcp"] as? [String: Any]
|
|
64
|
+
let sks = remote["sks"] as? [String: Any]
|
|
65
|
+
let remoteBlockers = remote["blockers"] as? [String] ?? []
|
|
66
|
+
let remoteWarnings = remote["warnings"] as? [String] ?? []
|
|
67
|
+
lines.append("Official Remote compatibility: \(remote["ok"] as? Bool == true ? "Ready" : "Needs attention") · Codex app \(host?["codex_app_found"] as? Bool == true ? "found" : "missing") · CLI \(host?["codex_cli_found"] as? Bool == true ? "found" : "missing")")
|
|
68
|
+
lines.append("Project: Git \(project?["git_repo"] as? Bool == true ? "ready" : "missing") · worktree \(project?["worktree"] as? Bool == true ? "yes" : "no") · dirty \(project?["dirty"] as? Bool == true ? "yes" : "no") · allowlist \(project?["allowed"] as? Bool == true ? "ready" : "blocked")")
|
|
69
|
+
lines.append("MCP: \(mcp?["effective_count"] as? Int ?? 0) effective · \(mcp?["failed_count"] as? Int ?? 0) failed · SKS proof \(sks?["proof_surfaces_ready"] as? Bool == true ? "ready" : "missing")")
|
|
70
|
+
if !remoteBlockers.isEmpty {
|
|
71
|
+
lines.append("Remote blockers: \(remoteBlockers.prefix(3).joined(separator: "; "))\(remoteBlockers.count > 3 ? "…" : "")")
|
|
72
|
+
} else if !remoteWarnings.isEmpty {
|
|
73
|
+
lines.append("Remote warnings: \(remoteWarnings.prefix(3).joined(separator: "; "))\(remoteWarnings.count > 3 ? "…" : "")")
|
|
74
|
+
} else {
|
|
75
|
+
lines.append("Remote blockers 0 · warnings 0")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if telegramFailed || telegram == nil {
|
|
80
|
+
lines.append("Telegram Hub: unavailable — status probe failed.")
|
|
81
|
+
lines.append("Remote fleet: unavailable until Telegram status succeeds.")
|
|
82
|
+
} else if let telegram = telegram {
|
|
83
|
+
let owner = telegram["owner"] as? [String: Any]
|
|
84
|
+
let configured = telegram["configured"] as? Bool
|
|
85
|
+
let hubState = owner != nil ? "Running" : configured == true ? "Stopped" : configured == false ? "Not configured" : "Unknown"
|
|
86
|
+
let telegramIssues = (telegram["config_issues"] as? [String] ?? []) + (telegram["remote_config_issues"] as? [String] ?? [])
|
|
87
|
+
lines.append("Telegram Hub: \(hubState) · \(telegram["topic_count"] as? Int ?? 0) topics · \(telegram["machine_count"] as? Int ?? 0) machines · \(telegram["target_count"] as? Int ?? 0) targets")
|
|
88
|
+
if telegramIssues.isEmpty {
|
|
89
|
+
lines.append("Telegram issues 0")
|
|
90
|
+
} else {
|
|
91
|
+
lines.append("Telegram issues \(telegramIssues.count): \(telegramIssues.prefix(2).joined(separator: "; "))\(telegramIssues.count > 2 ? "…" : "")")
|
|
92
|
+
}
|
|
93
|
+
}
|
|
59
94
|
return lines.joined(separator: "\n")
|
|
60
95
|
}
|
|
61
96
|
|