sneakoscope 6.1.2 → 6.2.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 +11 -4
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/install-helpers.js +8 -3
- package/dist/config/skills-manifest.json +58 -58
- package/dist/core/agents/agent-effort-policy.js +28 -17
- package/dist/core/agents/agent-schema.js +1 -1
- package/dist/core/codex/codex-config-guard.js +178 -7
- package/dist/core/codex/codex-config-readability.js +21 -8
- package/dist/core/codex/codex-config-toml.js +14 -11
- package/dist/core/codex-app/mcp-manager.js +679 -0
- package/dist/core/codex-app/sks-menubar.js +405 -6
- package/dist/core/codex-control/codex-lb-launch-recovery.js +15 -0
- package/dist/core/codex-native/core-skill-manifest.js +1 -1
- package/dist/core/commands/mad-sks-command.js +44 -3
- package/dist/core/commands/menubar-command.js +94 -0
- package/dist/core/commands/naruto-command.js +3 -1
- package/dist/core/commands/wiki-command.js +11 -5
- package/dist/core/fsx.js +1 -0
- package/dist/core/hooks-runtime/code-pack-freshness-preflight.js +14 -8
- package/dist/core/hooks-runtime/naruto-decision-gate.js +184 -0
- package/dist/core/hooks-runtime/stop-repeat-guard.js +89 -0
- package/dist/core/hooks-runtime.js +36 -105
- package/dist/core/init/skills.js +4 -4
- package/dist/core/init.js +1 -1
- package/dist/core/managed-assets/managed-assets-manifest.js +178 -29
- package/dist/core/preflight/parallel-preflight-engine.js +16 -3
- package/dist/core/proof/route-adapter.js +1 -1
- package/dist/core/proof/selftest-proof-fixtures.js +3 -5
- package/dist/core/provider/model-router.js +15 -6
- package/dist/core/release/package-size-budget.js +4 -6
- package/dist/core/research/research-plan-markdown.js +123 -0
- package/dist/core/research/research-super-search.js +8 -4
- package/dist/core/research.js +4 -119
- package/dist/core/retention.js +70 -2
- package/dist/core/routes/dollar-manifest-lite.js +1 -1
- package/dist/core/routes.js +75 -22
- package/dist/core/subagents/agent-catalog.js +87 -15
- package/dist/core/subagents/model-policy.js +189 -48
- package/dist/core/subagents/naruto-help-contract.js +11 -4
- package/dist/core/subagents/official-subagent-preparation.js +55 -9
- package/dist/core/subagents/official-subagent-prompt.js +45 -23
- package/dist/core/subagents/thread-budget.js +1 -1
- package/dist/core/subagents/triwiki-attention.js +117 -14
- package/dist/core/triwiki/code-pack-head-freshness.js +291 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-launcher.js +12 -2
- package/dist/core/zellij/zellij-pane-proof.js +9 -1
- package/dist/core/zellij/zellij-update.js +14 -1
- package/dist/scripts/canonical-test-runner.js +7 -1
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +11 -1
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +9 -2
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +13 -2
- package/dist/scripts/codex-native-agent-role-content-check.js +13 -1
- package/dist/scripts/codex-sdk-backend-router-check.js +2 -0
- package/dist/scripts/lib/codex-sdk-gate-lib.js +4 -0
- package/dist/scripts/official-subagent-workflow-check.js +1 -1
- package/dist/scripts/packlist-performance-check.js +1 -18
- package/dist/scripts/python-codex-sdk-all-pipelines-check.js +8 -0
- package/dist/scripts/sks-menubar-install-check.js +6 -1
- package/dist/scripts/super-search-provider-interface-check.js +31 -18
- package/package.json +3 -1
- package/dist/scripts/codex-0139-feature-probes-check.js +0 -30
- package/dist/scripts/codex-0139-marketplace-source-check.js +0 -13
|
@@ -22,6 +22,7 @@ const MENU_ITEMS = [
|
|
|
22
22
|
'Update SKS Now',
|
|
23
23
|
'Codex CLI Version',
|
|
24
24
|
'Update Codex CLI Now',
|
|
25
|
+
'Manage MCP Servers',
|
|
25
26
|
'Run sks doctor --fix',
|
|
26
27
|
'Open Dashboard',
|
|
27
28
|
'Open Codex Settings',
|
|
@@ -961,7 +962,12 @@ func humanizeSksCode(_ code: String) -> String {
|
|
|
961
962
|
"setup_needed": "codex-lb is not configured yet.",
|
|
962
963
|
"cancelled": "Setup was cancelled.",
|
|
963
964
|
"process_only_cancelled": "Setup was cancelled (process-only mode was not confirmed).",
|
|
964
|
-
"process_only_requires_yes": "This setup would only be kept for the current session — nothing durable was saved."
|
|
965
|
+
"process_only_requires_yes": "This setup would only be kept for the current session — nothing durable was saved.",
|
|
966
|
+
"codex_mcp_config_read_failed": "Could not read ~/.codex/config.toml. Check its ownership and permissions; no MCP change was made.",
|
|
967
|
+
"codex_mcp_config_symlink_unsupported": "~/.codex/config.toml is a symbolic link. The MCP manager left it unchanged to avoid replacing the link.",
|
|
968
|
+
"codex_mcp_config_not_regular_file": "~/.codex/config.toml is not a regular file. The MCP manager left it unchanged.",
|
|
969
|
+
"codex_mcp_config_toml_parse_failed": "~/.codex/config.toml contains invalid TOML. Fix it before managing MCP servers.",
|
|
970
|
+
"codex_mcp_config_busy": "Another MCP configuration change is in progress. Try again shortly."
|
|
965
971
|
]
|
|
966
972
|
if let mapped = known[code] { return mapped }
|
|
967
973
|
let words = code.split(separator: "_").joined(separator: " ")
|
|
@@ -1011,6 +1017,55 @@ func promptChoice(title: String, message: String, options: [String]) -> String?
|
|
|
1011
1017
|
return popup.titleOfSelectedItem
|
|
1012
1018
|
}
|
|
1013
1019
|
|
|
1020
|
+
func promptTextAllowEmpty(title: String, message: String, placeholder: String = "", secure: Bool = false) -> String? {
|
|
1021
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
1022
|
+
let alert = NSAlert()
|
|
1023
|
+
alert.messageText = title
|
|
1024
|
+
alert.informativeText = message
|
|
1025
|
+
alert.addButton(withTitle: "OK")
|
|
1026
|
+
alert.addButton(withTitle: "Cancel")
|
|
1027
|
+
let field: NSTextField = secure ? NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 360, height: 24)) : NSTextField(frame: NSRect(x: 0, y: 0, width: 360, height: 24))
|
|
1028
|
+
field.placeholderString = placeholder
|
|
1029
|
+
alert.accessoryView = field
|
|
1030
|
+
let response = alert.runModal()
|
|
1031
|
+
if response != .alertFirstButtonReturn { return nil }
|
|
1032
|
+
return field.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
func promptMultiline(title: String, message: String, placeholder: String = "") -> String? {
|
|
1036
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
1037
|
+
let alert = NSAlert()
|
|
1038
|
+
alert.messageText = title
|
|
1039
|
+
alert.informativeText = message
|
|
1040
|
+
alert.addButton(withTitle: "OK")
|
|
1041
|
+
alert.addButton(withTitle: "Cancel")
|
|
1042
|
+
let scroll = NSScrollView(frame: NSRect(x: 0, y: 0, width: 420, height: 120))
|
|
1043
|
+
scroll.hasVerticalScroller = true
|
|
1044
|
+
scroll.borderType = .bezelBorder
|
|
1045
|
+
let textView = NSTextView(frame: scroll.bounds)
|
|
1046
|
+
textView.isRichText = false
|
|
1047
|
+
textView.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
|
|
1048
|
+
textView.string = ""
|
|
1049
|
+
textView.setAccessibilityLabel(title)
|
|
1050
|
+
textView.setAccessibilityHelp(placeholder)
|
|
1051
|
+
scroll.documentView = textView
|
|
1052
|
+
alert.accessoryView = scroll
|
|
1053
|
+
let response = alert.runModal()
|
|
1054
|
+
if response != .alertFirstButtonReturn { return nil }
|
|
1055
|
+
return textView.string.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
func confirmAction(title: String, message: String, destructive: Bool = false) -> Bool {
|
|
1059
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
1060
|
+
let alert = NSAlert()
|
|
1061
|
+
alert.messageText = title
|
|
1062
|
+
alert.informativeText = message
|
|
1063
|
+
alert.alertStyle = destructive ? .warning : .informational
|
|
1064
|
+
alert.addButton(withTitle: destructive ? "Remove" : "OK")
|
|
1065
|
+
alert.addButton(withTitle: "Cancel")
|
|
1066
|
+
return alert.runModal() == .alertFirstButtonReturn
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1014
1069
|
func parseJsonObject(_ text: String) -> [String: Any]? {
|
|
1015
1070
|
guard let data = text.data(using: .utf8) else { return nil }
|
|
1016
1071
|
return (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any]
|
|
@@ -1029,13 +1084,17 @@ func runProcess(_ executable: String, _ args: [String] = [], stdinText: String?
|
|
|
1029
1084
|
process.standardInput = pipe
|
|
1030
1085
|
inputPipe = pipe
|
|
1031
1086
|
}
|
|
1032
|
-
process.terminationHandler = { proc in
|
|
1033
|
-
let data = output.fileHandleForReading.readDataToEndOfFile()
|
|
1034
|
-
let text = String(data: data, encoding: .utf8) ?? ""
|
|
1035
|
-
completion?(proc.terminationStatus, text)
|
|
1036
|
-
}
|
|
1037
1087
|
do {
|
|
1038
1088
|
try process.run()
|
|
1089
|
+
output.fileHandleForWriting.closeFile()
|
|
1090
|
+
DispatchQueue.global(qos: .utility).async {
|
|
1091
|
+
let data = output.fileHandleForReading.readDataToEndOfFile()
|
|
1092
|
+
process.waitUntilExit()
|
|
1093
|
+
let text = String(data: data, encoding: .utf8) ?? ""
|
|
1094
|
+
DispatchQueue.main.async {
|
|
1095
|
+
completion?(process.terminationStatus, text)
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1039
1098
|
if let stdinText = stdinText, let inputPipe = inputPipe {
|
|
1040
1099
|
inputPipe.fileHandleForWriting.write(Data(stdinText.utf8))
|
|
1041
1100
|
inputPipe.fileHandleForWriting.closeFile()
|
|
@@ -1103,6 +1162,339 @@ struct MenuBarConfig {
|
|
|
1103
1162
|
let quitWithCodex: Bool
|
|
1104
1163
|
}
|
|
1105
1164
|
|
|
1165
|
+
struct McpServerRow {
|
|
1166
|
+
let name: String
|
|
1167
|
+
let enabled: Bool
|
|
1168
|
+
let transport: String
|
|
1169
|
+
let summary: String
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
final class McpManagerController: NSObject, NSWindowDelegate, NSTableViewDataSource, NSTableViewDelegate {
|
|
1173
|
+
weak var owner: AppDelegate?
|
|
1174
|
+
var panel: NSPanel!
|
|
1175
|
+
var tableView: NSTableView!
|
|
1176
|
+
var statusLabel: NSTextField!
|
|
1177
|
+
var removeButton: NSButton!
|
|
1178
|
+
var toggleButton: NSButton!
|
|
1179
|
+
var refreshButton: NSButton!
|
|
1180
|
+
var addButton: NSButton!
|
|
1181
|
+
var rows: [McpServerRow] = []
|
|
1182
|
+
var working = false
|
|
1183
|
+
|
|
1184
|
+
init(owner: AppDelegate) {
|
|
1185
|
+
self.owner = owner
|
|
1186
|
+
super.init()
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
func show() {
|
|
1190
|
+
if panel == nil { buildPanel() }
|
|
1191
|
+
if panel.isVisible {
|
|
1192
|
+
panel.makeKeyAndOrderFront(nil)
|
|
1193
|
+
return
|
|
1194
|
+
}
|
|
1195
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
1196
|
+
panel.center()
|
|
1197
|
+
panel.makeKeyAndOrderFront(nil)
|
|
1198
|
+
refresh()
|
|
1199
|
+
NSApp.runModal(for: panel)
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
func buildPanel() {
|
|
1203
|
+
panel = NSPanel(
|
|
1204
|
+
contentRect: NSRect(x: 0, y: 0, width: 760, height: 440),
|
|
1205
|
+
styleMask: [.titled, .closable, .resizable, .utilityWindow],
|
|
1206
|
+
backing: .buffered,
|
|
1207
|
+
defer: false
|
|
1208
|
+
)
|
|
1209
|
+
panel.title = "MCP Servers"
|
|
1210
|
+
panel.minSize = NSSize(width: 640, height: 360)
|
|
1211
|
+
panel.isFloatingPanel = true
|
|
1212
|
+
panel.delegate = self
|
|
1213
|
+
|
|
1214
|
+
let content = NSView(frame: panel.contentView?.bounds ?? .zero)
|
|
1215
|
+
panel.contentView = content
|
|
1216
|
+
|
|
1217
|
+
let heading = NSTextField(labelWithString: "Global Codex MCP servers")
|
|
1218
|
+
heading.font = NSFont.systemFont(ofSize: 15, weight: .semibold)
|
|
1219
|
+
heading.translatesAutoresizingMaskIntoConstraints = false
|
|
1220
|
+
|
|
1221
|
+
let scope = NSTextField(labelWithString: "Changes are written safely to ~/.codex/config.toml and apply to new Codex sessions.")
|
|
1222
|
+
scope.textColor = .secondaryLabelColor
|
|
1223
|
+
scope.font = NSFont.systemFont(ofSize: 11)
|
|
1224
|
+
scope.translatesAutoresizingMaskIntoConstraints = false
|
|
1225
|
+
|
|
1226
|
+
tableView = NSTableView()
|
|
1227
|
+
tableView.delegate = self
|
|
1228
|
+
tableView.dataSource = self
|
|
1229
|
+
tableView.usesAlternatingRowBackgroundColors = true
|
|
1230
|
+
tableView.allowsMultipleSelection = false
|
|
1231
|
+
tableView.rowSizeStyle = .medium
|
|
1232
|
+
tableView.setAccessibilityLabel("Configured MCP servers")
|
|
1233
|
+
let statusColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("status"))
|
|
1234
|
+
statusColumn.title = "State"
|
|
1235
|
+
statusColumn.width = 70
|
|
1236
|
+
statusColumn.minWidth = 64
|
|
1237
|
+
statusColumn.maxWidth = 88
|
|
1238
|
+
let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name"))
|
|
1239
|
+
nameColumn.title = "Name"
|
|
1240
|
+
nameColumn.width = 180
|
|
1241
|
+
nameColumn.minWidth = 120
|
|
1242
|
+
let transportColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("transport"))
|
|
1243
|
+
transportColumn.title = "Transport"
|
|
1244
|
+
transportColumn.width = 90
|
|
1245
|
+
transportColumn.minWidth = 80
|
|
1246
|
+
let detailColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("summary"))
|
|
1247
|
+
detailColumn.title = "Configuration"
|
|
1248
|
+
detailColumn.width = 360
|
|
1249
|
+
detailColumn.minWidth = 220
|
|
1250
|
+
tableView.addTableColumn(statusColumn)
|
|
1251
|
+
tableView.addTableColumn(nameColumn)
|
|
1252
|
+
tableView.addTableColumn(transportColumn)
|
|
1253
|
+
tableView.addTableColumn(detailColumn)
|
|
1254
|
+
|
|
1255
|
+
let scroll = NSScrollView()
|
|
1256
|
+
scroll.documentView = tableView
|
|
1257
|
+
scroll.hasVerticalScroller = true
|
|
1258
|
+
scroll.hasHorizontalScroller = false
|
|
1259
|
+
scroll.borderType = .bezelBorder
|
|
1260
|
+
scroll.translatesAutoresizingMaskIntoConstraints = false
|
|
1261
|
+
|
|
1262
|
+
statusLabel = NSTextField(labelWithString: "Loading MCP configuration…")
|
|
1263
|
+
statusLabel.textColor = .secondaryLabelColor
|
|
1264
|
+
statusLabel.font = NSFont.systemFont(ofSize: 11)
|
|
1265
|
+
statusLabel.lineBreakMode = .byTruncatingMiddle
|
|
1266
|
+
statusLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
1267
|
+
|
|
1268
|
+
addButton = makeButton("Add…", #selector(addServer))
|
|
1269
|
+
removeButton = makeButton("Remove", #selector(removeServer))
|
|
1270
|
+
toggleButton = makeButton("Disable", #selector(toggleServer))
|
|
1271
|
+
refreshButton = makeButton("Refresh", #selector(refresh))
|
|
1272
|
+
let closeButton = makeButton("Close", #selector(closePanel))
|
|
1273
|
+
closeButton.keyEquivalent = "\\u{1b}"
|
|
1274
|
+
|
|
1275
|
+
let buttonStack = NSStackView(views: [addButton, removeButton, toggleButton, refreshButton, NSView(), closeButton])
|
|
1276
|
+
buttonStack.orientation = .horizontal
|
|
1277
|
+
buttonStack.spacing = 8
|
|
1278
|
+
buttonStack.distribution = .fill
|
|
1279
|
+
buttonStack.translatesAutoresizingMaskIntoConstraints = false
|
|
1280
|
+
|
|
1281
|
+
content.addSubview(heading)
|
|
1282
|
+
content.addSubview(scope)
|
|
1283
|
+
content.addSubview(scroll)
|
|
1284
|
+
content.addSubview(statusLabel)
|
|
1285
|
+
content.addSubview(buttonStack)
|
|
1286
|
+
NSLayoutConstraint.activate([
|
|
1287
|
+
heading.topAnchor.constraint(equalTo: content.topAnchor, constant: 18),
|
|
1288
|
+
heading.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 18),
|
|
1289
|
+
heading.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -18),
|
|
1290
|
+
scope.topAnchor.constraint(equalTo: heading.bottomAnchor, constant: 4),
|
|
1291
|
+
scope.leadingAnchor.constraint(equalTo: heading.leadingAnchor),
|
|
1292
|
+
scope.trailingAnchor.constraint(equalTo: heading.trailingAnchor),
|
|
1293
|
+
scroll.topAnchor.constraint(equalTo: scope.bottomAnchor, constant: 12),
|
|
1294
|
+
scroll.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 18),
|
|
1295
|
+
scroll.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -18),
|
|
1296
|
+
scroll.bottomAnchor.constraint(equalTo: statusLabel.topAnchor, constant: -10),
|
|
1297
|
+
statusLabel.leadingAnchor.constraint(equalTo: scroll.leadingAnchor),
|
|
1298
|
+
statusLabel.centerYAnchor.constraint(equalTo: buttonStack.centerYAnchor),
|
|
1299
|
+
statusLabel.trailingAnchor.constraint(lessThanOrEqualTo: buttonStack.leadingAnchor, constant: -12),
|
|
1300
|
+
buttonStack.trailingAnchor.constraint(equalTo: scroll.trailingAnchor),
|
|
1301
|
+
buttonStack.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -16),
|
|
1302
|
+
buttonStack.heightAnchor.constraint(equalToConstant: 30)
|
|
1303
|
+
])
|
|
1304
|
+
updateControls()
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
func makeButton(_ title: String, _ selector: Selector) -> NSButton {
|
|
1308
|
+
let button = NSButton(title: title, target: self, action: selector)
|
|
1309
|
+
button.bezelStyle = .rounded
|
|
1310
|
+
button.setAccessibilityLabel(title)
|
|
1311
|
+
return button
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
func numberOfRows(in tableView: NSTableView) -> Int {
|
|
1315
|
+
return rows.count
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
|
1319
|
+
guard row >= 0, row < rows.count, let tableColumn = tableColumn else { return nil }
|
|
1320
|
+
let identifier = tableColumn.identifier
|
|
1321
|
+
let cell = tableView.makeView(withIdentifier: identifier, owner: self) as? NSTableCellView ?? NSTableCellView()
|
|
1322
|
+
cell.identifier = identifier
|
|
1323
|
+
let field: NSTextField
|
|
1324
|
+
if let existing = cell.textField {
|
|
1325
|
+
field = existing
|
|
1326
|
+
} else {
|
|
1327
|
+
field = NSTextField(labelWithString: "")
|
|
1328
|
+
field.translatesAutoresizingMaskIntoConstraints = false
|
|
1329
|
+
field.lineBreakMode = .byTruncatingTail
|
|
1330
|
+
cell.textField = field
|
|
1331
|
+
cell.addSubview(field)
|
|
1332
|
+
NSLayoutConstraint.activate([
|
|
1333
|
+
field.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 4),
|
|
1334
|
+
field.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -4),
|
|
1335
|
+
field.centerYAnchor.constraint(equalTo: cell.centerYAnchor)
|
|
1336
|
+
])
|
|
1337
|
+
}
|
|
1338
|
+
let server = rows[row]
|
|
1339
|
+
switch identifier.rawValue {
|
|
1340
|
+
case "status":
|
|
1341
|
+
field.stringValue = server.enabled ? "Enabled" : "Disabled"
|
|
1342
|
+
field.textColor = server.enabled ? .systemGreen : .secondaryLabelColor
|
|
1343
|
+
field.font = NSFont.systemFont(ofSize: 11, weight: .medium)
|
|
1344
|
+
case "name":
|
|
1345
|
+
field.stringValue = server.name
|
|
1346
|
+
field.textColor = .labelColor
|
|
1347
|
+
field.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
|
1348
|
+
case "transport":
|
|
1349
|
+
field.stringValue = server.transport == "url" ? "Remote" : server.transport == "stdio" ? "Local" : "Unknown"
|
|
1350
|
+
field.textColor = .labelColor
|
|
1351
|
+
field.font = NSFont.systemFont(ofSize: 12)
|
|
1352
|
+
default:
|
|
1353
|
+
field.stringValue = server.summary
|
|
1354
|
+
field.textColor = .secondaryLabelColor
|
|
1355
|
+
field.font = NSFont.systemFont(ofSize: 11)
|
|
1356
|
+
}
|
|
1357
|
+
return cell
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
func tableViewSelectionDidChange(_ notification: Notification) {
|
|
1361
|
+
updateControls()
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
func selectedServer() -> McpServerRow? {
|
|
1365
|
+
let selected = tableView.selectedRow
|
|
1366
|
+
return selected >= 0 && selected < rows.count ? rows[selected] : nil
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
func setWorking(_ value: Bool, status: String? = nil) {
|
|
1370
|
+
working = value
|
|
1371
|
+
if let status = status { statusLabel.stringValue = status }
|
|
1372
|
+
updateControls()
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
func updateControls() {
|
|
1376
|
+
let selected = selectedServer()
|
|
1377
|
+
addButton?.isEnabled = !working
|
|
1378
|
+
refreshButton?.isEnabled = !working
|
|
1379
|
+
removeButton?.isEnabled = !working && selected != nil
|
|
1380
|
+
toggleButton?.isEnabled = !working && selected != nil
|
|
1381
|
+
toggleButton?.title = selected?.enabled == true ? "Disable" : "Enable"
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
@objc func refresh() {
|
|
1385
|
+
guard !working else { return }
|
|
1386
|
+
guard let owner = owner else { return }
|
|
1387
|
+
setWorking(true, status: "Loading MCP configuration…")
|
|
1388
|
+
owner.runSksSilent(["menubar", "mcp", "list", "--json"]) { [weak self] code, output in
|
|
1389
|
+
guard let self = self else { return }
|
|
1390
|
+
guard code == 0,
|
|
1391
|
+
let json = parseJsonObject(output),
|
|
1392
|
+
json["schema"] as? String == "sks.menubar-mcp-list.v1",
|
|
1393
|
+
json["ok"] as? Bool == true,
|
|
1394
|
+
let servers = json["servers"] as? [[String: Any]] else {
|
|
1395
|
+
self.rows = []
|
|
1396
|
+
self.tableView.reloadData()
|
|
1397
|
+
self.setWorking(false, status: "Could not read MCP configuration")
|
|
1398
|
+
showAlert("MCP Manager", informative: humanizeSksFailure(output))
|
|
1399
|
+
return
|
|
1400
|
+
}
|
|
1401
|
+
self.rows = servers.compactMap { server in
|
|
1402
|
+
guard let name = server["name"] as? String else { return nil }
|
|
1403
|
+
return McpServerRow(
|
|
1404
|
+
name: name,
|
|
1405
|
+
enabled: server["enabled"] as? Bool != false,
|
|
1406
|
+
transport: server["transport"] as? String ?? "unknown",
|
|
1407
|
+
summary: server["summary"] as? String ?? "Configured"
|
|
1408
|
+
)
|
|
1409
|
+
}
|
|
1410
|
+
self.tableView.reloadData()
|
|
1411
|
+
if !self.rows.isEmpty { self.tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) }
|
|
1412
|
+
let enabled = self.rows.filter { $0.enabled }.count
|
|
1413
|
+
self.setWorking(false, status: "\(self.rows.count) configured · \(enabled) enabled · Global")
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
@objc func addServer() {
|
|
1418
|
+
guard !working else { return }
|
|
1419
|
+
guard let transport = promptChoice(title: "Add MCP Server", message: "Choose how Codex connects to this server.", options: ["Remote URL", "Local command"]) else { return }
|
|
1420
|
+
guard let name = promptText(title: "MCP Server Name", message: "Use a unique name (letters, numbers, dots, dashes, or underscores).", placeholder: "context7") else { return }
|
|
1421
|
+
var payload: [String: Any] = ["name": name]
|
|
1422
|
+
if transport == "Remote URL" {
|
|
1423
|
+
guard let url = promptText(title: "Remote MCP URL", message: "Enter the full HTTP or HTTPS MCP endpoint.", placeholder: "https://mcp.example.com/mcp") else { return }
|
|
1424
|
+
guard let bearer = promptTextAllowEmpty(title: "Bearer Token Environment Variable", message: "Optional. Enter the environment variable name that contains the bearer token, or leave blank.", placeholder: "MCP_ACCESS_TOKEN") else { return }
|
|
1425
|
+
payload["transport"] = "url"
|
|
1426
|
+
payload["url"] = url
|
|
1427
|
+
if !bearer.isEmpty { payload["bearer_token_env_var"] = bearer }
|
|
1428
|
+
} else {
|
|
1429
|
+
guard let command = promptText(title: "Local MCP Command", message: "Enter the executable used to start the MCP server.", placeholder: "npx") else { return }
|
|
1430
|
+
guard let arguments = promptMultiline(title: "Command Arguments", message: "Optional. Enter one argument per line.", placeholder: "-y\\n@upstash/context7-mcp") else { return }
|
|
1431
|
+
guard let environment = promptMultiline(title: "Environment Variables", message: "Optional. Enter KEY=VALUE pairs, one per line. Values are written only to Codex config and never shown in the MCP list.", placeholder: "API_TOKEN=...") else { return }
|
|
1432
|
+
payload["transport"] = "stdio"
|
|
1433
|
+
payload["command"] = command
|
|
1434
|
+
payload["args"] = arguments.split(separator: "\\n").map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }
|
|
1435
|
+
var env: [String: String] = [:]
|
|
1436
|
+
for line in environment.split(separator: "\\n") {
|
|
1437
|
+
let text = String(line)
|
|
1438
|
+
guard let separator = text.firstIndex(of: "=") else {
|
|
1439
|
+
showAlert("Invalid environment variable", informative: "Use KEY=VALUE, one entry per line.")
|
|
1440
|
+
return
|
|
1441
|
+
}
|
|
1442
|
+
let key = String(text[..<separator]).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1443
|
+
let value = String(text[text.index(after: separator)...]).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1444
|
+
env[key] = value
|
|
1445
|
+
}
|
|
1446
|
+
if !env.isEmpty { payload["env"] = env }
|
|
1447
|
+
}
|
|
1448
|
+
guard JSONSerialization.isValidJSONObject(payload),
|
|
1449
|
+
let data = try? JSONSerialization.data(withJSONObject: payload),
|
|
1450
|
+
let json = String(data: data, encoding: .utf8) else {
|
|
1451
|
+
showAlert("Add MCP Server", informative: "Could not prepare the MCP configuration payload.")
|
|
1452
|
+
return
|
|
1453
|
+
}
|
|
1454
|
+
runMutation(["menubar", "mcp", "add", "--stdin-json", "--json"], title: "Add MCP Server", stdinText: json + "\\n")
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
@objc func removeServer() {
|
|
1458
|
+
guard !working, let server = selectedServer() else { return }
|
|
1459
|
+
guard confirmAction(title: "Remove \(server.name)?", message: "This removes the global MCP configuration and creates a protected backup first.", destructive: true) else { return }
|
|
1460
|
+
runMutation(["menubar", "mcp", "remove", server.name, "--json"], title: "Remove MCP Server")
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
@objc func toggleServer() {
|
|
1464
|
+
guard !working, let server = selectedServer() else { return }
|
|
1465
|
+
let action = server.enabled ? "disable" : "enable"
|
|
1466
|
+
runMutation(["menubar", "mcp", action, server.name, "--json"], title: server.enabled ? "Disable MCP Server" : "Enable MCP Server")
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
func runMutation(_ args: [String], title: String, stdinText: String? = nil) {
|
|
1470
|
+
guard let owner = owner else { return }
|
|
1471
|
+
setWorking(true, status: title + "…")
|
|
1472
|
+
owner.runSksCapture(args, title: title, stdinText: stdinText, notify: false) { [weak self] code, output in
|
|
1473
|
+
guard let self = self else { return }
|
|
1474
|
+
guard code == 0,
|
|
1475
|
+
let json = parseJsonObject(output),
|
|
1476
|
+
json["schema"] as? String == "sks.menubar-mcp-mutation.v1",
|
|
1477
|
+
json["ok"] as? Bool == true else {
|
|
1478
|
+
self.setWorking(false, status: title + " failed")
|
|
1479
|
+
showAlert(title + " failed", informative: humanizeSksFailure(output))
|
|
1480
|
+
return
|
|
1481
|
+
}
|
|
1482
|
+
self.setWorking(false, status: title + " complete · restart or open a new Codex session to apply")
|
|
1483
|
+
self.refresh()
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
@objc func closePanel() {
|
|
1488
|
+
if NSApp.modalWindow === panel { NSApp.stopModal() }
|
|
1489
|
+
panel.orderOut(nil)
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
|
1493
|
+
closePanel()
|
|
1494
|
+
return false
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1106
1498
|
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
1107
1499
|
var statusItem: NSStatusItem!
|
|
1108
1500
|
var statusLineItem: NSMenuItem!
|
|
@@ -1112,6 +1504,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1112
1504
|
var fastModeOffItem: NSMenuItem!
|
|
1113
1505
|
var codexCliVersionItem: NSMenuItem!
|
|
1114
1506
|
var codexCliUpdateItem: NSMenuItem!
|
|
1507
|
+
var mcpManager: McpManagerController?
|
|
1115
1508
|
var timer: Timer?
|
|
1116
1509
|
var busy = false
|
|
1117
1510
|
var lastFailure = false
|
|
@@ -1153,6 +1546,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1153
1546
|
codexCliVersionItem.isEnabled = false
|
|
1154
1547
|
menu.addItem(codexCliVersionItem)
|
|
1155
1548
|
codexCliUpdateItem = add(menu, "Update Codex CLI Now", #selector(updateCodexCliNow))
|
|
1549
|
+
add(menu, "Manage MCP Servers…", #selector(manageMcpServers))
|
|
1156
1550
|
add(menu, "Run sks doctor --fix", #selector(runDoctorFix))
|
|
1157
1551
|
menu.addItem(NSMenuItem.separator())
|
|
1158
1552
|
add(menu, "Open Dashboard", #selector(openDashboard))
|
|
@@ -1474,6 +1868,11 @@ ${codexLifecycleSource}
|
|
|
1474
1868
|
}
|
|
1475
1869
|
}
|
|
1476
1870
|
|
|
1871
|
+
@objc func manageMcpServers() {
|
|
1872
|
+
if mcpManager == nil { mcpManager = McpManagerController(owner: self) }
|
|
1873
|
+
mcpManager?.show()
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1477
1876
|
@objc func runDoctorFix() {
|
|
1478
1877
|
runSksBackground(["doctor", "--fix", "--global-only", "--json"], title: "Run sks doctor --fix") { [weak self] code, _ in
|
|
1479
1878
|
if code == 0 {
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { parseCodexConfigToml } from '../codex/codex-config-toml.js';
|
|
4
4
|
import { readText } from '../fsx.js';
|
|
5
5
|
import { codexLbToolOutputRecoveryNotChecked, codexLbToolOutputRecoveryNotSelected, codexLbToolOutputRecoveryOverrideAcknowledged, probeCodexLbToolOutputRecovery } from '../codex-lb/codex-lb-tool-output-recovery.js';
|
|
6
|
+
import { normalizeCodexLbBaseUrl } from '../codex-lb/codex-lb-env.js';
|
|
6
7
|
export async function inspectCodexLbSdkLaunchRecovery(input) {
|
|
7
8
|
if (String(input.config.model_provider || '') !== 'codex-lb') {
|
|
8
9
|
return codexLbToolOutputRecoveryNotSelected();
|
|
@@ -71,6 +72,9 @@ export async function inspectCodexLbCliLaunchRecovery(input) {
|
|
|
71
72
|
?? profileConfig.baseUrl
|
|
72
73
|
?? userConfig.baseUrl
|
|
73
74
|
?? String(env.CODEX_LB_BASE_URL || '');
|
|
75
|
+
const verifiedProbe = reusableVerifiedProbeForBase(input.verifiedProbe, baseUrl);
|
|
76
|
+
if (verifiedProbe)
|
|
77
|
+
return verifiedProbe;
|
|
74
78
|
return probeCodexLbToolOutputRecovery({
|
|
75
79
|
baseUrl,
|
|
76
80
|
...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
|
|
@@ -79,6 +83,17 @@ export async function inspectCodexLbCliLaunchRecovery(input) {
|
|
|
79
83
|
|| codexLbToolOutputRecoveryOverrideAcknowledged({ args, env })
|
|
80
84
|
});
|
|
81
85
|
}
|
|
86
|
+
function reusableVerifiedProbeForBase(value, selectedBaseUrl) {
|
|
87
|
+
if (!value || value.schema !== 'sks.codex-lb-tool-output-recovery.v1')
|
|
88
|
+
return null;
|
|
89
|
+
if (value.ok !== true || value.required !== true || value.verified !== true || value.supports_interrupted_tool_output_recovery !== true)
|
|
90
|
+
return null;
|
|
91
|
+
const verifiedBaseUrl = normalizeCodexLbBaseUrl(value.base_url);
|
|
92
|
+
const effectiveBaseUrl = normalizeCodexLbBaseUrl(selectedBaseUrl);
|
|
93
|
+
if (!verifiedBaseUrl || !effectiveBaseUrl || verifiedBaseUrl !== effectiveBaseUrl)
|
|
94
|
+
return null;
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
82
97
|
export async function withCodexLbCliLaunchRecovery(input, launch) {
|
|
83
98
|
const toolOutputRecovery = await inspectCodexLbCliLaunchRecovery(input);
|
|
84
99
|
if (!toolOutputRecovery.ok) {
|
|
@@ -22,7 +22,7 @@ const CORE_SKILL_DEFINITIONS = [
|
|
|
22
22
|
route: '$Naruto',
|
|
23
23
|
purpose: 'run a Codex official subagent workflow with official agent threads while parent integration remains owner.',
|
|
24
24
|
when: 'Use when the user explicitly invokes $Naruto or the selected route requires bounded parallel delegation.',
|
|
25
|
-
workflow: 'Use sks naruto run "<task>" [--agents N] [--max-threads N] [--json]. Automatic fan-out is
|
|
25
|
+
workflow: 'Use sks naruto run "<task>" [--agents N] [--max-threads N] [--json]. Automatic fan-out is two for non-trivial work and at most three for critical multi-domain risk; an explicit operator --agents N remains authoritative. The GPT-5.6 Sol Max parent selects the narrow project-scoped custom role whose description matches each defensible slice. Model policy is fixed: Luna Max only for tiny short-context mechanical work; Sol High for ordinary UI, logic, backend, and native implementation; Sol Max for review, debugging, planning, architecture, security, DB, research, release, ambiguity, and judgment; Terra Medium for long-context analysis plus direct Computer Use, Browser/Chrome, and image-generation execution. Split mixed work into execution and judgment slices when possible; if one slice cannot be split safely, Sol Max judgment wins. Read-only roles declare sandbox_mode="read-only"; write roles inherit the parent permission mode. Treat Codex [agents].max_threads as the concurrency authority, keep max_depth=1, avoid duplicate or overlapping write slices, wait for every requested subagent, reuse only four ordinary or six complex query-aware TriWiki trust/hydration anchors, treat SubagentStart/SubagentStop as lifecycle-only evidence, require subagent-parent-summary.json with one structured outcome per thread, and let the parent own integration and final judgment.',
|
|
26
26
|
safety: 'Preserve user-authored content, inherit the parent permission mode, do not spawn nested subagents, do not inject the full pack or the full TriWiki context into every child, and do not fall back to another model, process swarm, custom scheduler, or worker pool. The historical Naruto process runtime is removed; stop with explicit blocker evidence when the official path is unavailable.',
|
|
27
27
|
cli: 'sks naruto run "<task>" [--agents N] [--max-threads N] [--json]; sks naruto status|subagents|proof [--mission <id>] [--json]',
|
|
28
28
|
evidence: 'subagent-plan.json, subagent-events.jsonl, subagent-parent-summary.json, subagent-evidence.json, naruto-summary.json, and naruto-gate.json.',
|
|
@@ -142,7 +142,8 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
142
142
|
selfHealOnMissing: true,
|
|
143
143
|
autoApprove: rawArgs.includes('--yes') || rawArgs.includes('-y'),
|
|
144
144
|
installHomebrew: rawArgs.includes('--install-homebrew'),
|
|
145
|
-
allowHeadlessFallback: headlessZellij
|
|
145
|
+
allowHeadlessFallback: headlessZellij,
|
|
146
|
+
deferUpdateCheck: true
|
|
146
147
|
}).catch(() => ({ status: 'error', command: 'sks doctor --fix --yes' }))
|
|
147
148
|
: await repairZellijForSks({
|
|
148
149
|
root: launchRoot,
|
|
@@ -198,7 +199,10 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
198
199
|
sandbox: 'danger-full-access',
|
|
199
200
|
serviceTier: 'fast',
|
|
200
201
|
skipCodexLbToolOutputRecovery: glmMadLaunch,
|
|
201
|
-
allowUnverifiedToolOutputRecovery
|
|
202
|
+
allowUnverifiedToolOutputRecovery,
|
|
203
|
+
...(zellijUpdate.deferred === true && zellijUpdate.capability
|
|
204
|
+
? { zellijCapability: zellijUpdate.capability }
|
|
205
|
+
: {})
|
|
202
206
|
};
|
|
203
207
|
let launchPreflight = await runCodexLaunchPreflight(launchRoot, launchPreflightOpts);
|
|
204
208
|
// Fresh-project bootstrap: when the ONLY blocker is that the managed Codex config does
|
|
@@ -300,6 +304,12 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
300
304
|
};
|
|
301
305
|
const explicitWorkspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', null));
|
|
302
306
|
const launchProfile = glmRuntime?.profile || profile;
|
|
307
|
+
const verifiedCodexLbToolOutputRecovery = launchPreflight.codex_lb_tool_output_recovery?.selected === true
|
|
308
|
+
&& launchPreflight.codex_lb_tool_output_recovery?.tool_output_recovery?.ok === true
|
|
309
|
+
&& launchPreflight.codex_lb_tool_output_recovery?.tool_output_recovery?.verified === true
|
|
310
|
+
&& launchPreflight.codex_lb_tool_output_recovery?.tool_output_recovery?.supports_interrupted_tool_output_recovery === true
|
|
311
|
+
? launchPreflight.codex_lb_tool_output_recovery.tool_output_recovery
|
|
312
|
+
: undefined;
|
|
303
313
|
const launchOpts = glmRuntime
|
|
304
314
|
? buildGlmMadLaunchOpts(cleanArgs, {
|
|
305
315
|
codexArgs: launchProfile.launch_args,
|
|
@@ -318,7 +328,18 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
318
328
|
const workspace = explicitWorkspace || launchOpts.session || `sks-mad-${sanitizeZellijSessionName(process.cwd())}`;
|
|
319
329
|
const launch = headlessZellij
|
|
320
330
|
? await writeMadHeadlessZellijFallback(madLaunch, workspace)
|
|
321
|
-
: await launchMadZellijUi([...cleanArgs, '--workspace', workspace], {
|
|
331
|
+
: await launchMadZellijUi([...cleanArgs, '--workspace', workspace], {
|
|
332
|
+
...launchOpts,
|
|
333
|
+
missionId: madLaunch.mission_id,
|
|
334
|
+
root: madLaunch.root,
|
|
335
|
+
cwd: process.cwd(),
|
|
336
|
+
ledgerRoot: path.join(madLaunch.dir, 'agents'),
|
|
337
|
+
slotCount: 0,
|
|
338
|
+
freshSession: autoDerivedMadSession,
|
|
339
|
+
requireZellij: process.env.SKS_REQUIRE_ZELLIJ === '1',
|
|
340
|
+
...(launchPreflight.zellij_capability ? { zellijCapability: launchPreflight.zellij_capability } : {}),
|
|
341
|
+
...(verifiedCodexLbToolOutputRecovery ? { verifiedCodexLbToolOutputRecovery } : {})
|
|
342
|
+
});
|
|
322
343
|
const afterLaunchUi = beforeUi ? await writeCodexAppUiSnapshot(launchRoot, `mad-after-launch-${uiSnapshotId}`).catch(() => null) : null;
|
|
323
344
|
const launchUiDiff = beforeUi && afterLaunchUi ? diffCodexAppUiSnapshots(beforeUi, afterLaunchUi) : null;
|
|
324
345
|
if (launchUiDiff) {
|
|
@@ -346,6 +367,26 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
346
367
|
ui_architecture: 'monitor_plus_viewports',
|
|
347
368
|
right_column_mode: 'monitor-plus-viewports'
|
|
348
369
|
});
|
|
370
|
+
if (zellijUpdate.deferred === true && deps.maybePromptZellijUpdateForLaunch) {
|
|
371
|
+
const zellijUpdateNoticePromise = deps.maybePromptZellijUpdateForLaunch([...args, '--no-question'], {
|
|
372
|
+
label: glmMadLaunch ? 'GLM MAD launch' : 'MAD launch',
|
|
373
|
+
root: launchRoot,
|
|
374
|
+
missionDir: madLaunch.dir,
|
|
375
|
+
selfHealOnMissing: false,
|
|
376
|
+
allowHeadlessFallback: headlessZellij
|
|
377
|
+
}).then((notice) => appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), {
|
|
378
|
+
ts: nowIso(),
|
|
379
|
+
type: 'mad_sks.zellij_update_refreshed_background',
|
|
380
|
+
non_blocking: true,
|
|
381
|
+
status: notice?.status || 'unknown',
|
|
382
|
+
update_available: notice?.status === 'available'
|
|
383
|
+
})).catch((err) => appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), {
|
|
384
|
+
ts: nowIso(),
|
|
385
|
+
type: 'mad_sks.zellij_update_background_failed',
|
|
386
|
+
error: err?.message || String(err)
|
|
387
|
+
}));
|
|
388
|
+
launchLifecycleTasks.push(zellijUpdateNoticePromise);
|
|
389
|
+
}
|
|
349
390
|
const madNativeSwarmPromise = startMadNativeSwarm(madLaunch.root, madLaunch, args, launchProfile, {
|
|
350
391
|
env: {
|
|
351
392
|
...madSksEnv,
|