Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b9cb9587e | ||
|
|
420e7ce999 | ||
|
|
7889f1e26e | ||
|
|
71448013a7 | ||
|
|
b365cbd21c | ||
|
|
dc6f88b4c0 | ||
|
|
0fd29a65ac | ||
|
|
7e7d13c6c5 | ||
|
|
c3ed7eb907 |
20
.github/workflows/macos-ci.yml
vendored
20
.github/workflows/macos-ci.yml
vendored
@ -359,7 +359,15 @@ jobs:
|
||||
- name: Build Peekaboo app (Xcode)
|
||||
working-directory: Apps
|
||||
run: |
|
||||
/usr/bin/env -u DYLD_LIBRARY_PATH -u DYLD_FRAMEWORK_PATH -u DYLD_FALLBACK_FRAMEWORK_PATH \
|
||||
/usr/bin/env \
|
||||
-u DYLD_LIBRARY_PATH \
|
||||
-u DYLD_FRAMEWORK_PATH \
|
||||
-u DYLD_FALLBACK_FRAMEWORK_PATH \
|
||||
-u DYLD_ROOT_PATH \
|
||||
-u DYLD_INSERT_LIBRARIES \
|
||||
-u DYLD_IMAGE_SUFFIX \
|
||||
-u DYLD_VERSIONED_LIBRARY_PATH \
|
||||
-u DYLD_VERSIONED_FRAMEWORK_PATH \
|
||||
xcodebuild -workspace Peekaboo.xcworkspace \
|
||||
-scheme Peekaboo \
|
||||
-configuration Debug \
|
||||
@ -370,7 +378,15 @@ jobs:
|
||||
- name: Build Inspector app (Xcode)
|
||||
working-directory: Apps/PeekabooInspector
|
||||
run: |
|
||||
/usr/bin/env -u DYLD_LIBRARY_PATH -u DYLD_FRAMEWORK_PATH -u DYLD_FALLBACK_FRAMEWORK_PATH \
|
||||
/usr/bin/env \
|
||||
-u DYLD_LIBRARY_PATH \
|
||||
-u DYLD_FRAMEWORK_PATH \
|
||||
-u DYLD_FALLBACK_FRAMEWORK_PATH \
|
||||
-u DYLD_ROOT_PATH \
|
||||
-u DYLD_INSERT_LIBRARIES \
|
||||
-u DYLD_IMAGE_SUFFIX \
|
||||
-u DYLD_VERSIONED_LIBRARY_PATH \
|
||||
-u DYLD_VERSIONED_FRAMEWORK_PATH \
|
||||
xcodebuild -project Inspector.xcodeproj \
|
||||
-scheme Inspector \
|
||||
-configuration Debug \
|
||||
|
||||
@ -324,7 +324,8 @@ final class AgentChatUI {
|
||||
MarkdownComponent(
|
||||
text: text,
|
||||
padding: .init(horizontal: 1, vertical: 0),
|
||||
defaultTextStyle: .init(color: color))
|
||||
defaultTextStyle: .init(color: color)
|
||||
)
|
||||
}
|
||||
|
||||
private func removeLoader() {
|
||||
@ -556,11 +557,11 @@ final class AgentChatEventDelegate: AgentEventDelegate {
|
||||
|
||||
private func valuesEqual(_ lhs: Any, _ rhs: Any) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (l as String, r as String): return l == r
|
||||
case let (l as Int, r as Int): return l == r
|
||||
case let (l as Double, r as Double): return l == r
|
||||
case let (l as Bool, r as Bool): return l == r
|
||||
default: return false
|
||||
case let (l as String, r as String): l == r
|
||||
case let (l as Int, r as Int): l == r
|
||||
case let (l as Double, r as Double): l == r
|
||||
case let (l as Bool, r as Bool): l == r
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -326,7 +326,6 @@ extension AgentCommand {
|
||||
}
|
||||
|
||||
let runTask = Task { () throws -> AgentExecutionResult in
|
||||
|
||||
if let existingSessionId = startingSessionId {
|
||||
let outputDelegate = self.makeDisplayDelegate(for: batchedInput)
|
||||
let streamingDelegate = self.makeStreamingDelegate(using: outputDelegate)
|
||||
|
||||
@ -3,9 +3,9 @@ import Darwin
|
||||
import Dispatch
|
||||
import Foundation
|
||||
import Logging
|
||||
import PeekabooAgentRuntime
|
||||
import PeekabooCore
|
||||
import PeekabooFoundation
|
||||
import PeekabooAgentRuntime
|
||||
import Spinner
|
||||
import Tachikoma
|
||||
import TachikomaMCP
|
||||
|
||||
@ -141,7 +141,7 @@ extension AgentOutputDelegate {
|
||||
return // no change; avoid spamming the log
|
||||
}
|
||||
let diffSummary = self.diffSummary(for: name, newArgs: args)
|
||||
let (formatter, _ /* toolType */) = self.toolFormatter(for: name)
|
||||
let (formatter, _ /* toolType */ ) = self.toolFormatter(for: name)
|
||||
|
||||
switch self.outputMode {
|
||||
case .minimal:
|
||||
@ -498,12 +498,12 @@ extension AgentOutputDelegate {
|
||||
|
||||
private func valuesEqual(_ lhs: Any, _ rhs: Any) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (l as String, r as String): return l == r
|
||||
case let (l as Int, r as Int): return l == r
|
||||
case let (l as Double, r as Double): return l == r
|
||||
case let (l as Bool, r as Bool): return l == r
|
||||
case let (l as String, r as String): l == r
|
||||
case let (l as Int, r as Int): l == r
|
||||
case let (l as Double, r as Double): l == r
|
||||
case let (l as Bool, r as Bool): l == r
|
||||
default:
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -530,7 +530,8 @@ extension AgentOutputDelegate {
|
||||
default:
|
||||
if let data = try? JSONSerialization.data(withJSONObject: ["v": value], options: []),
|
||||
let text = String(data: data, encoding: .utf8) {
|
||||
return text.replacingOccurrences(of: "{\"v\":", with: "").trimmingCharacters(in: CharacterSet(charactersIn: "}"))
|
||||
return text.replacingOccurrences(of: "{\"v\":", with: "")
|
||||
.trimmingCharacters(in: CharacterSet(charactersIn: "}"))
|
||||
}
|
||||
return "…"
|
||||
}
|
||||
|
||||
@ -587,8 +587,7 @@ enum MenuServiceBridge {
|
||||
}
|
||||
|
||||
static func listMenuBarItems(menu: any MenuServiceProtocol, includeRaw: Bool = false) async throws
|
||||
-> [MenuBarItemInfo]
|
||||
{
|
||||
-> [MenuBarItemInfo] {
|
||||
try await Task { @MainActor in
|
||||
try await menu.listMenuBarItems(includeRaw: includeRaw)
|
||||
}.value
|
||||
|
||||
@ -321,7 +321,10 @@ extension TypeCommand: CommanderBindableCommand {
|
||||
if let delay: Int = try values.decodeOption("delay", as: Int.self) {
|
||||
self.delay = delay
|
||||
}
|
||||
if let wpm: Int = try values.decodeOption("wordsPerMinute", as: Int.self) ?? values.decodeOption("wpm", as: Int.self) {
|
||||
if let wpm: Int = try values.decodeOption("wordsPerMinute", as: Int.self) ?? values.decodeOption(
|
||||
"wpm",
|
||||
as: Int.self
|
||||
) {
|
||||
self.wordsPerMinute = wpm
|
||||
}
|
||||
if let profile = values.singleOption("profileOption") ?? values.singleOption("profile") {
|
||||
|
||||
@ -60,6 +60,7 @@ struct CleanCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
// to the parsed runtime options so flags like --json-output are visible.
|
||||
return self.runtimeOptions.makeConfiguration()
|
||||
}
|
||||
|
||||
var jsonOutput: Bool { self.configuration.jsonOutput }
|
||||
|
||||
@MainActor
|
||||
|
||||
@ -20,7 +20,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
restore Restore a previously saved slot.
|
||||
load Shortcut for set with --file-path.
|
||||
""",
|
||||
showHelpOnEmptyInvocation: true)
|
||||
showHelpOnEmptyInvocation: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +122,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
size: result.data.count,
|
||||
filePath: output,
|
||||
slot: nil,
|
||||
textPreview: result.textPreview)
|
||||
textPreview: result.textPreview
|
||||
)
|
||||
|
||||
self.output(payload) {
|
||||
if let text = String(data: result.data, encoding: .utf8) {
|
||||
@ -129,7 +131,9 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
} else if let output {
|
||||
print("📋 Saved \(result.data.count) bytes (\(result.utiIdentifier)) to \(output)")
|
||||
} else {
|
||||
print("📋 Clipboard contains \(result.data.count) bytes of \(result.utiIdentifier); use --output to save.")
|
||||
print(
|
||||
"📋 Clipboard contains \(result.data.count) bytes of \(result.utiIdentifier); use --output to save."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,7 +147,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
size: result.data.count,
|
||||
filePath: nil,
|
||||
slot: nil,
|
||||
textPreview: result.textPreview)
|
||||
textPreview: result.textPreview
|
||||
)
|
||||
|
||||
self.output(payload) {
|
||||
print("✅ Set clipboard (\(result.utiIdentifier), \(result.data.count) bytes)")
|
||||
@ -162,7 +167,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
size: result.data.count,
|
||||
filePath: path,
|
||||
slot: nil,
|
||||
textPreview: result.textPreview)
|
||||
textPreview: result.textPreview
|
||||
)
|
||||
|
||||
self.output(payload) {
|
||||
print("✅ Loaded \(result.data.count) bytes (\(result.utiIdentifier)) from \(path) into clipboard")
|
||||
@ -171,7 +177,14 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
|
||||
private func handleClear() {
|
||||
self.services.clipboard.clear()
|
||||
let payload = ClipboardCommandResult(action: "clear", uti: nil, size: nil, filePath: nil, slot: nil, textPreview: nil)
|
||||
let payload = ClipboardCommandResult(
|
||||
action: "clear",
|
||||
uti: nil,
|
||||
size: nil,
|
||||
filePath: nil,
|
||||
slot: nil,
|
||||
textPreview: nil
|
||||
)
|
||||
self.output(payload) {
|
||||
print("🧹 Cleared clipboard")
|
||||
}
|
||||
@ -180,7 +193,14 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
private func handleSave() throws {
|
||||
let slotName = self.slot ?? "0"
|
||||
try self.services.clipboard.save(slot: slotName)
|
||||
let payload = ClipboardCommandResult(action: "save", uti: nil, size: nil, filePath: nil, slot: slotName, textPreview: nil)
|
||||
let payload = ClipboardCommandResult(
|
||||
action: "save",
|
||||
uti: nil,
|
||||
size: nil,
|
||||
filePath: nil,
|
||||
slot: slotName,
|
||||
textPreview: nil
|
||||
)
|
||||
self.output(payload) {
|
||||
print("💾 Saved clipboard to slot \"\(slotName)\"")
|
||||
}
|
||||
@ -195,7 +215,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
size: result.data.count,
|
||||
filePath: nil,
|
||||
slot: slotName,
|
||||
textPreview: result.textPreview)
|
||||
textPreview: result.textPreview
|
||||
)
|
||||
self.output(payload) {
|
||||
print("♻️ Restored slot \"\(slotName)\" (\(result.utiIdentifier), \(result.data.count) bytes)")
|
||||
}
|
||||
@ -210,7 +231,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
ClipboardRepresentation(utiIdentifier: UTType.plainText.identifier, data: Data(text.utf8)),
|
||||
],
|
||||
alsoText: self.alsoText,
|
||||
allowLarge: self.allowLarge)
|
||||
allowLarge: self.allowLarge
|
||||
)
|
||||
}
|
||||
|
||||
if let path = overridePath ?? self.filePath ?? self.imagePath {
|
||||
@ -220,7 +242,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
return ClipboardWriteRequest(
|
||||
representations: [ClipboardRepresentation(utiIdentifier: uti.identifier, data: data)],
|
||||
alsoText: self.alsoText,
|
||||
allowLarge: self.allowLarge)
|
||||
allowLarge: self.allowLarge
|
||||
)
|
||||
}
|
||||
|
||||
if let b64 = self.dataBase64, let utiId = self.uti {
|
||||
@ -230,7 +253,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
|
||||
return ClipboardWriteRequest(
|
||||
representations: [ClipboardRepresentation(utiIdentifier: utiId, data: data)],
|
||||
alsoText: self.alsoText,
|
||||
allowLarge: self.allowLarge)
|
||||
allowLarge: self.allowLarge
|
||||
)
|
||||
}
|
||||
|
||||
throw ValidationError("Provide --text, --file-path/--image-path, or --data-base64 with --uti")
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import AXorcist
|
||||
import CoreGraphics
|
||||
import Commander
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import PeekabooCore
|
||||
import PeekabooFoundation
|
||||
@ -95,7 +95,8 @@ struct MenuBarCommand: ParsableCommand, OutputFormattable {
|
||||
self.logger.debug("Listing menu bar items includeRawDebug=\(self.includeRawDebug)")
|
||||
let menuBarItems = try await MenuServiceBridge.listMenuBarItems(
|
||||
menu: self.services.menu,
|
||||
includeRaw: self.includeRawDebug)
|
||||
includeRaw: self.includeRawDebug
|
||||
)
|
||||
|
||||
if self.jsonOutput {
|
||||
let output = ListJSONOutput(
|
||||
@ -106,18 +107,18 @@ struct MenuBarCommand: ParsableCommand, OutputFormattable {
|
||||
raw_title: item.rawTitle,
|
||||
bundle_id: item.bundleIdentifier,
|
||||
owner_name: item.ownerName,
|
||||
identifier: item.identifier,
|
||||
ax_identifier: item.axIdentifier,
|
||||
ax_description: item.axDescription,
|
||||
raw_window_id: item.rawWindowID,
|
||||
raw_window_layer: item.rawWindowLayer,
|
||||
raw_owner_pid: item.rawOwnerPID,
|
||||
raw_source: item.rawSource,
|
||||
index: item.index,
|
||||
isVisible: item.isVisible,
|
||||
description: item.description
|
||||
)
|
||||
},
|
||||
identifier: item.identifier,
|
||||
ax_identifier: item.axIdentifier,
|
||||
ax_description: item.axDescription,
|
||||
raw_window_id: item.rawWindowID,
|
||||
raw_window_layer: item.rawWindowLayer,
|
||||
raw_owner_pid: item.rawOwnerPID,
|
||||
raw_source: item.rawSource,
|
||||
index: item.index,
|
||||
isVisible: item.isVisible,
|
||||
description: item.description
|
||||
)
|
||||
},
|
||||
executionTime: Date().timeIntervalSince(startTime)
|
||||
)
|
||||
outputSuccessCodable(data: output, logger: self.outputLogger)
|
||||
|
||||
@ -36,7 +36,7 @@ private enum VersionMetadata {
|
||||
gitCommit: "unknown",
|
||||
gitCommitDate: "unknown",
|
||||
gitBranch: "unknown",
|
||||
buildDate: iso8601Now()
|
||||
buildDate: self.iso8601Now()
|
||||
)
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ private enum VersionMetadata {
|
||||
let commit = info["PeekabooGitCommit"] as? String ?? "unknown"
|
||||
let commitDate = info["PeekabooGitCommitDate"] as? String ?? "unknown"
|
||||
let branch = info["PeekabooGitBranch"] as? String ?? "unknown"
|
||||
let buildDate = info["PeekabooBuildDate"] as? String ?? iso8601Now()
|
||||
let buildDate = info["PeekabooBuildDate"] as? String ?? self.iso8601Now()
|
||||
|
||||
return Values(
|
||||
current: display,
|
||||
@ -63,25 +63,25 @@ private enum VersionMetadata {
|
||||
}
|
||||
|
||||
private static func valuesFromWorkingCopy() -> Values? {
|
||||
let root = repositoryRoot()
|
||||
let root = self.repositoryRoot()
|
||||
guard FileManager.default.fileExists(atPath: root.path) else { return nil }
|
||||
|
||||
let versionString = workingCopyVersion(root: root) ?? "0.0.0"
|
||||
var commit = git(["rev-parse", "--short", "HEAD"], root: root) ?? "unknown"
|
||||
let diffStatus = git(["status", "--porcelain"], root: root) ?? ""
|
||||
let versionString = self.workingCopyVersion(root: root) ?? "0.0.0"
|
||||
var commit = self.git(["rev-parse", "--short", "HEAD"], root: root) ?? "unknown"
|
||||
let diffStatus = self.git(["status", "--porcelain"], root: root) ?? ""
|
||||
if !diffStatus.isEmpty {
|
||||
commit += "-dirty"
|
||||
}
|
||||
|
||||
let commitDate = git(["show", "-s", "--format=%ci", "HEAD"], root: root) ?? "unknown"
|
||||
let branch = git(["rev-parse", "--abbrev-ref", "HEAD"], root: root) ?? "unknown"
|
||||
let commitDate = self.git(["show", "-s", "--format=%ci", "HEAD"], root: root) ?? "unknown"
|
||||
let branch = self.git(["rev-parse", "--abbrev-ref", "HEAD"], root: root) ?? "unknown"
|
||||
|
||||
return Values(
|
||||
current: "Peekaboo \(versionString)",
|
||||
gitCommit: commit,
|
||||
gitCommitDate: commitDate,
|
||||
gitBranch: branch,
|
||||
buildDate: iso8601Now()
|
||||
buildDate: self.iso8601Now()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import Commander
|
||||
import Foundation
|
||||
import PeekabooCore
|
||||
import Commander
|
||||
import Testing
|
||||
|
||||
@testable import PeekabooCLI
|
||||
|
||||
@ -189,7 +189,7 @@ struct MenuCommandIntegrationTests {
|
||||
|
||||
extension MenuCommandIntegrationTests {
|
||||
/// Trim any progress/preamble characters emitted by the test runner and decode from the first JSON token.
|
||||
fileprivate func decodeJSON<T: Decodable>(_ type: T.Type, from output: String) throws -> T {
|
||||
private func decodeJSON<T: Decodable>(_ type: T.Type, from output: String) throws -> T {
|
||||
guard let start = output.firstIndex(where: { $0 == "{" || $0 == "[" }) else {
|
||||
throw DecodingError.dataCorrupted(
|
||||
.init(codingPath: [], debugDescription: "No JSON object found in output: \(output)")
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
import Darwin
|
||||
import Foundation
|
||||
import PeekabooCore
|
||||
@testable import PeekabooCLI
|
||||
|
||||
|
||||
@ -798,7 +798,8 @@ final class StubClipboardService: ClipboardServiceProtocol {
|
||||
let result = ClipboardReadResult(
|
||||
utiIdentifier: primary.utiIdentifier,
|
||||
data: primary.data,
|
||||
textPreview: request.alsoText)
|
||||
textPreview: request.alsoText
|
||||
)
|
||||
self.current = result
|
||||
return result
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ struct TTYCommandRunner {
|
||||
}
|
||||
let waitDeadline = Date().addingTimeInterval(1.5)
|
||||
while proc.isRunning, Date() < waitDeadline {
|
||||
usleep(80_000)
|
||||
usleep(80000)
|
||||
}
|
||||
if proc.isRunning {
|
||||
if let pgid = processGroup {
|
||||
@ -128,7 +128,7 @@ struct TTYCommandRunner {
|
||||
if let byteDeadline = afterFirstByteDeadline, now >= byteDeadline { break }
|
||||
if afterFirstByteDeadline == nil, now >= primaryDeadline { break }
|
||||
|
||||
usleep(60_000)
|
||||
usleep(60000)
|
||||
}
|
||||
|
||||
guard let text = String(data: buffer, encoding: .utf8), !text.isEmpty else {
|
||||
@ -137,6 +137,7 @@ struct TTYCommandRunner {
|
||||
|
||||
return Result(text: text)
|
||||
}
|
||||
|
||||
// swiftlint:enable cyclomatic_complexity function_body_length
|
||||
|
||||
static func which(_ tool: String) -> String? {
|
||||
@ -166,7 +167,7 @@ struct TTYCommandRunner {
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
guard let path = String(data: data, encoding: .utf8)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!path.isEmpty else { return nil }
|
||||
!path.isEmpty else { return nil }
|
||||
return path
|
||||
}
|
||||
|
||||
|
||||
@ -26,7 +26,8 @@ struct TTYCommandRunnerTests {
|
||||
let result = try runner.run(
|
||||
binary: scriptURL.path,
|
||||
send: "",
|
||||
options: .init(rows: 5, cols: 40, timeout: 0.8, extraArgs: []))
|
||||
options: .init(rows: 5, cols: 40, timeout: 0.8, extraArgs: [])
|
||||
)
|
||||
|
||||
guard let childPID = Self.extractChildPID(result.text) else {
|
||||
Issue.record("Did not capture child PID from PTY output. Output: \(result.text)")
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Peekaboo will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit 9c000eb4a6d27b56a952a72ddab260f1b757e108
|
||||
Subproject commit 960f2f0f9ce992f14af367510274f28ca4e763f5
|
||||
@ -65,7 +65,7 @@ extension PeekabooAgentService {
|
||||
|
||||
// If queue mode is "all" and we have queued messages, inject them
|
||||
// before the next turn so the model sees them together.
|
||||
if queueMode == .all && !queuedMessages.isEmpty {
|
||||
if queueMode == .all, !queuedMessages.isEmpty {
|
||||
state.messages.append(contentsOf: queuedMessages)
|
||||
queuedMessages.removeAll()
|
||||
}
|
||||
@ -332,7 +332,7 @@ extension PeekabooAgentService {
|
||||
"(?i)bearer\\s+[a-z0-9._-]{8,}",
|
||||
"(?i)api[_-]?key\\s*[:=]\\s*[a-z0-9._-]{6,}",
|
||||
"(?i)sess[a-z0-9]{12,}",
|
||||
"(?i)token\\s*[:=]\\s*[a-z0-9._-]{12,}"
|
||||
"(?i)token\\s*[:=]\\s*[a-z0-9._-]{12,}",
|
||||
]
|
||||
|
||||
var output = text
|
||||
|
||||
@ -2,6 +2,5 @@
|
||||
// either one at a time per turn, or all queued together before the next turn.
|
||||
public enum QueueMode: String, Sendable {
|
||||
case oneAtATime = "one-at-a-time"
|
||||
case all = "all"
|
||||
case all
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,8 @@ public struct ClipboardTool: MCPTool {
|
||||
"dataBase64": SchemaBuilder.string(description: "Base64-encoded data to copy"),
|
||||
"uti": SchemaBuilder.string(description: "Uniform Type Identifier for dataBase64 or to force type"),
|
||||
"prefer": SchemaBuilder.string(description: "Preferred UTI when reading clipboard"),
|
||||
"outputPath": SchemaBuilder.string(description: "When reading, path to write binary data. Use '-' for stdout."),
|
||||
"outputPath": SchemaBuilder
|
||||
.string(description: "When reading, path to write binary data. Use '-' for stdout."),
|
||||
"slot": SchemaBuilder.string(description: "Save/restore slot name (default: \"0\")"),
|
||||
"alsoText": SchemaBuilder.string(description: "Optional plain text companion when setting binary data"),
|
||||
"allowLarge": SchemaBuilder.boolean(description: "Allow writes larger than the 10 MB guard"),
|
||||
@ -113,7 +114,7 @@ public struct ClipboardTool: MCPTool {
|
||||
@MainActor
|
||||
private func handleLoad(arguments: ToolArguments) throws -> ToolResponse {
|
||||
// Alias for set; validation occurs in makeWriteRequest.
|
||||
return try self.handleSet(arguments: arguments)
|
||||
try self.handleSet(arguments: arguments)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@ -104,7 +104,8 @@ public final class ConfigurationManager: @unchecked Sendable {
|
||||
public func migrateIfNeeded() throws {
|
||||
// Allow tests or automation to disable migration to isolate temporary config roots.
|
||||
if let disable = ProcessInfo.processInfo.environment["PEEKABOO_CONFIG_DISABLE_MIGRATION"],
|
||||
disable.lowercased() == "1" || disable.lowercased() == "true" {
|
||||
disable.lowercased() == "1" || disable.lowercased() == "true"
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -91,20 +91,24 @@ public final class ClipboardService: ClipboardServiceProtocol {
|
||||
public func get(prefer uti: UTType?) throws -> ClipboardReadResult? {
|
||||
guard let types = self.pasteboard.types, !types.isEmpty else { return nil }
|
||||
|
||||
let targetType: NSPasteboard.PasteboardType
|
||||
if let uti, let preferred = types.first(where: { $0.rawValue == uti.identifier }) {
|
||||
targetType = preferred
|
||||
let targetType: NSPasteboard.PasteboardType = if let uti,
|
||||
let preferred = types
|
||||
.first(where: { $0.rawValue == uti.identifier })
|
||||
{
|
||||
preferred
|
||||
} else if let stringType = types.first(where: { $0 == .string || $0 == .init("public.utf8-plain-text") }) {
|
||||
targetType = stringType
|
||||
stringType
|
||||
} else {
|
||||
targetType = types[0]
|
||||
types[0]
|
||||
}
|
||||
|
||||
let data: Data?
|
||||
var textPreview: String?
|
||||
|
||||
if targetType == .string, let string = self.pasteboard.string(forType: .string) {
|
||||
let normalized = string.replacingOccurrences(of: "\r\n", with: "\n").replacingOccurrences(of: "\r", with: "\n")
|
||||
let normalized = string.replacingOccurrences(of: "\r\n", with: "\n").replacingOccurrences(
|
||||
of: "\r",
|
||||
with: "\n")
|
||||
data = normalized.data(using: .utf8)
|
||||
textPreview = Self.makePreview(normalized)
|
||||
} else {
|
||||
@ -147,15 +151,14 @@ public final class ClipboardService: ClipboardServiceProtocol {
|
||||
}
|
||||
|
||||
let primary = request.representations.first!
|
||||
let preview: String?
|
||||
if let text = request.alsoText {
|
||||
preview = Self.makePreview(text)
|
||||
let preview: String? = if let text = request.alsoText {
|
||||
Self.makePreview(text)
|
||||
} else if primary.utiIdentifier == UTType.plainText.identifier,
|
||||
let string = String(data: primary.data, encoding: .utf8)
|
||||
{
|
||||
preview = Self.makePreview(string)
|
||||
Self.makePreview(string)
|
||||
} else {
|
||||
preview = nil
|
||||
nil
|
||||
}
|
||||
|
||||
return ClipboardReadResult(
|
||||
|
||||
@ -8,12 +8,13 @@ import Foundation
|
||||
// Option bits (mirrored from Ice)
|
||||
private struct CGSWindowListOption: OptionSet {
|
||||
let rawValue: UInt32
|
||||
static let onScreen = CGSWindowListOption(rawValue: 1 << 0)
|
||||
static let onScreen = CGSWindowListOption(rawValue: 1 << 0)
|
||||
static let menuBarItems = CGSWindowListOption(rawValue: 1 << 1)
|
||||
static let activeSpace = CGSWindowListOption(rawValue: 1 << 2)
|
||||
static let activeSpace = CGSWindowListOption(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
// MARK: - Dynamic loading helpers
|
||||
|
||||
// MARK: - Dynamic loading helpers
|
||||
|
||||
private func loadCGSHandle() -> UnsafeMutableRawPointer? {
|
||||
@ -71,10 +72,14 @@ func cgsMenuBarWindowIDs(onScreen: Bool = false, activeSpace: Bool = false) -> [
|
||||
func cgsProcessMenuBarWindowIDs(onScreenOnly: Bool = true) -> [CGWindowID] {
|
||||
typealias CGSConnectionID = Int32
|
||||
typealias CGSMainConnectionFunc = @convention(c) () -> CGSConnectionID
|
||||
typealias CGSGetWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>) -> Int32
|
||||
typealias CGSGetWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>)
|
||||
-> Int32
|
||||
typealias CGSGetProcessMenuBarWindowListFunc = @convention(c) (
|
||||
CGSConnectionID, CGSConnectionID, Int32, UnsafeMutablePointer<CGWindowID>, UnsafeMutablePointer<Int32>) -> Int32
|
||||
typealias CGSGetOnScreenWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>) -> Int32
|
||||
typealias CGSGetOnScreenWindowCountFunc = @convention(c) (
|
||||
CGSConnectionID,
|
||||
CGSConnectionID,
|
||||
UnsafeMutablePointer<Int32>) -> Int32
|
||||
typealias CGSGetOnScreenWindowListFunc = @convention(c) (
|
||||
CGSConnectionID, CGSConnectionID, Int32, UnsafeMutablePointer<CGWindowID>, UnsafeMutablePointer<Int32>) -> Int32
|
||||
typealias CGSCopySpacesForWindowsFunc = @convention(c) (CGSConnectionID, UInt32, CFArray) -> Unmanaged<CFArray>?
|
||||
@ -115,7 +120,7 @@ func cgsProcessMenuBarWindowIDs(onScreenOnly: Bool = true) -> [CGWindowID] {
|
||||
// Active space filter to mirror Ice.
|
||||
let activeSpace = getActiveSpace(mainConn())
|
||||
ids = ids.filter { windowID in
|
||||
guard let spaces = copySpaces(mainConn(), 1 << 0 /*includes current*/, [windowID] as CFArray)?
|
||||
guard let spaces = copySpaces(mainConn(), 1 << 0 /* includes current */, [windowID] as CFArray)?
|
||||
.takeRetainedValue() as? [UInt32]
|
||||
else { return true }
|
||||
return spaces.contains(activeSpace)
|
||||
@ -142,7 +147,7 @@ private func cgsIsWindowOnActiveSpace(_ windowID: CGWindowID) -> Bool {
|
||||
|
||||
let cid = mainConn()
|
||||
let activeSpace = getActiveSpace(cid)
|
||||
guard let spaces = copySpaces(cid, 1 << 0 /*includes current*/, [windowID] as CFArray)?
|
||||
guard let spaces = copySpaces(cid, 1 << 0 /* includes current */, [windowID] as CFArray)?
|
||||
.takeRetainedValue() as? [UInt32]
|
||||
else { return true }
|
||||
return spaces.contains(activeSpace)
|
||||
|
||||
@ -192,7 +192,9 @@ extension MenuService {
|
||||
let cgsIDs = cgsMenuBarWindowIDs(onScreen: true, activeSpace: true)
|
||||
let legacyIDs = cgsProcessMenuBarWindowIDs(onScreenOnly: true)
|
||||
let combinedIDs = Array(Set(cgsIDs + legacyIDs))
|
||||
self.logger.debug("CGS menuBarItems returned \(cgsIDs.count) ids; processMenuBar returned \(legacyIDs.count); combined \(combinedIDs.count)")
|
||||
self.logger
|
||||
.debug(
|
||||
"CGS menuBarItems returned \(cgsIDs.count) ids; processMenuBar returned \(legacyIDs.count); combined \(combinedIDs.count)")
|
||||
if !combinedIDs.isEmpty {
|
||||
// Use CGWindow metadata per window ID to resolve owner/bundle.
|
||||
for id in combinedIDs {
|
||||
@ -227,7 +229,8 @@ extension MenuService {
|
||||
if let info {
|
||||
windowInfo = info
|
||||
} else if let refreshed = CGWindowListCopyWindowInfo([.optionIncludingWindow], windowID) as? [[String: Any]],
|
||||
let first = refreshed.first {
|
||||
let first = refreshed.first
|
||||
{
|
||||
windowInfo = first
|
||||
} else {
|
||||
return nil
|
||||
@ -329,7 +332,7 @@ extension MenuService {
|
||||
let identifier = extra.identifier()
|
||||
let hasIdentifier = identifier?.isEmpty == false
|
||||
let hasNonPlaceholderTitle = !isPlaceholderMenuTitle(baseTitle)
|
||||
if !hasIdentifier && !hasNonPlaceholderTitle {
|
||||
if !hasIdentifier, !hasNonPlaceholderTitle {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -401,8 +404,7 @@ extension MenuService {
|
||||
.first(where: { !isPlaceholderMenuTitle($0) })
|
||||
{
|
||||
effectiveTitle = childDerived
|
||||
}
|
||||
else if let ident = sanitizedMenuText(extra.identifier()), !ident.isEmpty {
|
||||
} else if let ident = sanitizedMenuText(extra.identifier()), !ident.isEmpty {
|
||||
effectiveTitle = ident
|
||||
}
|
||||
}
|
||||
@ -431,7 +433,7 @@ extension MenuService {
|
||||
var results: [MenuExtraInfo] = []
|
||||
let commonMenuTitles: Set<String> = [
|
||||
"apple", "file", "edit", "view", "window", "help", "history", "bookmarks", "navigate", "tab", "tools",
|
||||
"cut", "copy", "paste", "format"
|
||||
"cut", "copy", "paste", "format",
|
||||
]
|
||||
|
||||
func collectElements(from element: Element, depth: Int = 0, limit: Int = 4) -> [Element] {
|
||||
@ -469,17 +471,18 @@ extension MenuService {
|
||||
// Fallbacks to app name when placeholder/short/common menu words.
|
||||
if isPlaceholderMenuTitle(effectiveTitle) ||
|
||||
effectiveTitle.count <= 2 ||
|
||||
commonMenuTitles.contains(effectiveTitle.lowercased()) {
|
||||
commonMenuTitles.contains(effectiveTitle.lowercased())
|
||||
{
|
||||
effectiveTitle = app.localizedName ?? effectiveTitle
|
||||
}
|
||||
|
||||
let position = extra.position() ?? .zero
|
||||
// Restrict to top-of-screen positions to avoid stray elements.
|
||||
if position != .zero && position.y > 100 { continue }
|
||||
if position != .zero, position.y > 100 { continue }
|
||||
|
||||
// Avoid duplicating children of a status item: require that this element itself is status-like.
|
||||
let childrenRoles = (extra.children() ?? []).compactMap { $0.role() }
|
||||
if !isStatusLike && childrenRoles.contains(where: { $0 == "AXMenuItem" }) {
|
||||
if !isStatusLike, childrenRoles.contains(where: { $0 == "AXMenuItem" }) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -506,9 +509,10 @@ extension MenuService {
|
||||
|
||||
/// Hit-test window extras to attach AX identifiers/titles when CGS gives only placeholders.
|
||||
private func enrichWindowExtrasWithAXHitTest(_ extras: [MenuExtraInfo]) -> [MenuExtraInfo] {
|
||||
return extras.map { extra in
|
||||
guard extra.identifier == nil || isPlaceholderMenuTitle(extra.title) || isPlaceholderMenuTitle(extra.rawTitle),
|
||||
extra.position != .zero
|
||||
extras.map { extra in
|
||||
guard extra
|
||||
.identifier == nil || isPlaceholderMenuTitle(extra.title) || isPlaceholderMenuTitle(extra.rawTitle),
|
||||
extra.position != .zero
|
||||
else { return extra }
|
||||
|
||||
guard let hit = Element.elementAtPoint(extra.position) else {
|
||||
@ -520,12 +524,12 @@ extension MenuService {
|
||||
let isStatusLike = role == "AXStatusItem" || subrole == "AXStatusItem" || subrole == "AXMenuExtra"
|
||||
if !isStatusLike { return extra }
|
||||
|
||||
let hitTitle = sanitizedMenuText(hit.identifier())
|
||||
?? sanitizedMenuText(hit.help())
|
||||
?? sanitizedMenuText(hit.title())
|
||||
?? hit.descriptionText()
|
||||
?? extra.title
|
||||
?? extra.rawTitle
|
||||
let hitTitle = sanitizedMenuText(hit.identifier())
|
||||
?? sanitizedMenuText(hit.help())
|
||||
?? sanitizedMenuText(hit.title())
|
||||
?? hit.descriptionText()
|
||||
?? extra.title
|
||||
?? extra.rawTitle
|
||||
let hitIdentifier = hit.identifier() ?? extra.identifier
|
||||
|
||||
return MenuExtraInfo(
|
||||
|
||||
2
TauTUI
2
TauTUI
@ -1 +1 @@
|
||||
Subproject commit fc4f9d53a64b7a97aa3d46fad8b24ae2629e8ff8
|
||||
Subproject commit 94765bd2d017db1cc2b4754a3215c32ab34a3e80
|
||||
@ -9,7 +9,5 @@ let package = Package(
|
||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
.executableTarget(
|
||||
name: "cgs-menu-probe"
|
||||
),
|
||||
]
|
||||
)
|
||||
name: "cgs-menu-probe"),
|
||||
])
|
||||
|
||||
@ -6,13 +6,14 @@ import Foundation
|
||||
// Run as plain CLI; to test GUI privilege, re-run after wrapping in an LSUIElement app
|
||||
// or via an inspector helper. Outputs counts from both private APIs and CGWindowList.
|
||||
|
||||
struct CGSMenuProbe {
|
||||
enum CGSMenuProbe {
|
||||
typealias CGSConnectionID = UInt32
|
||||
typealias CGSMainConnectionFunc = @convention(c) () -> CGSConnectionID
|
||||
typealias CGSCopyWindowsFunc = @convention(c) (CGSConnectionID, Int32, UInt32) -> CFArray?
|
||||
typealias CGSGetProcessMenuBarWindowListFunc = @convention(c) (
|
||||
CGSConnectionID, CGSConnectionID, Int32, UnsafeMutablePointer<CGWindowID>, UnsafeMutablePointer<Int32>) -> Int32
|
||||
typealias CGSGetWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>) -> Int32
|
||||
typealias CGSGetWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>)
|
||||
-> Int32
|
||||
|
||||
private static func loadSymbol<T>(_ name: String, handle: UnsafeMutableRawPointer?) -> T? {
|
||||
guard let sym = dlsym(handle, name) else { return nil }
|
||||
@ -22,17 +23,21 @@ struct CGSMenuProbe {
|
||||
static func run() {
|
||||
let handles = [
|
||||
"/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight",
|
||||
"/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics"
|
||||
"/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics",
|
||||
]
|
||||
var chosen: UnsafeMutableRawPointer?
|
||||
for h in handles { if let ptr = dlopen(h, RTLD_NOW) { chosen = ptr; break } }
|
||||
for h in handles {
|
||||
if let ptr = dlopen(h, RTLD_NOW) { chosen = ptr; break }
|
||||
}
|
||||
guard let handle = chosen else { print("could not load CGS symbols"); return }
|
||||
|
||||
guard
|
||||
let mainConn: CGSMainConnectionFunc = loadSymbol("CGSMainConnectionID", handle: handle),
|
||||
let copyWindows: CGSCopyWindowsFunc = loadSymbol("CGSCopyWindowsWithOptions", handle: handle),
|
||||
let getCount: CGSGetWindowCountFunc = loadSymbol("CGSGetWindowCount", handle: handle),
|
||||
let getMenuBarList: CGSGetProcessMenuBarWindowListFunc = loadSymbol("CGSGetProcessMenuBarWindowList", handle: handle)
|
||||
let getMenuBarList: CGSGetProcessMenuBarWindowListFunc = loadSymbol(
|
||||
"CGSGetProcessMenuBarWindowList",
|
||||
handle: handle)
|
||||
else { print("missing symbols"); return }
|
||||
|
||||
let cid = mainConn()
|
||||
@ -52,7 +57,9 @@ struct CGSMenuProbe {
|
||||
let ids3 = Array(buf.prefix(Int(out)))
|
||||
|
||||
// Public CGWindowList fallback
|
||||
let cgList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
|
||||
let cgList = CGWindowListCopyWindowInfo(
|
||||
[.optionAll, .excludeDesktopElements],
|
||||
kCGNullWindowID) as? [[String: Any]] ?? []
|
||||
let layer25 = cgList.filter { ($0[kCGWindowLayer as String] as? Int) == 25 }
|
||||
|
||||
print("CGSCopyWindows menuBar: count=\(ids1.count) ids=\(ids1)")
|
||||
|
||||
@ -273,7 +273,7 @@
|
||||
{
|
||||
"label": "Changelog",
|
||||
"placement": "summary",
|
||||
"command": "node -e \"const fs=require('fs');const md=fs.readFileSync('CHANGELOG.md','utf8').split(/\\\\r?\\\\n/);const start=md.findIndex((l)=>l.startsWith('## '));const end=md.findIndex((l,i)=>i>start&&l.startsWith('## '));const chunk=md.slice(start,end>0?end:md.length);const out=chunk.map((l)=>{if(/^#+\\\\s*/.test(l))return l.replace(/^#+\\\\s*/,'').toUpperCase();if(/^\\\\s*-\\\\s+/.test(l))return '• '+l.replace(/^\\\\s*-\\\\s+/,'');return l.trim();}).filter(Boolean);console.log(out.slice(0,50).join('\\\\n'));\"",
|
||||
"command": "node -e \"const fs=require('fs');const lines=fs.readFileSync('CHANGELOG.md','utf8').split(/\\\\r?\\\\n/);const start=lines.findIndex((l)=>l.startsWith('## '));if(start<0){process.exit(0);}const end=lines.findIndex((l,i)=>i>start&&l.startsWith('## '));const slice=lines.slice(start,end>0?end:lines.length);const headingLine=slice[0]||'## Changelog';const heading=headingLine.replace(/^##\\\\s*/,'').trim();const bullets=slice.filter((l)=>l.trim().startsWith('- ')).length;console.log('@count: '+heading+' · '+bullets);console.log(slice.slice(0,50).join('\\\\n'));\"",
|
||||
"refreshSeconds": 600,
|
||||
"timeoutSeconds": 5,
|
||||
"maxLines": 50,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user