Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ SwiftASB helps Swift apps work with the local Codex app-server without making ap

### Status

SwiftASB has a supported v1 public API for the core local Codex app-server lifecycle. `v1.2.0` is the current released baseline.
SwiftASB has a supported v1 public API for the core local Codex app-server lifecycle. `v1.2.1` is the current released baseline.

### What This Project Is

Expand All @@ -33,9 +33,9 @@ Add SwiftASB from the GitHub package URL:

https://github.com/gaelic-ghost/SwiftASB

Use release `v1.2.0` or newer unless your project intentionally pins an older version.
Use release `v1.2.1` or newer unless your project intentionally pins an older version.

You also need a local Codex CLI installation with app-server support. SwiftASB looks for `codex` in the usual command-line locations, and apps can provide an exact executable path when they need stricter control.
You also need a local Codex CLI installation with app-server support. SwiftASB currently reviews against the `0.130.x` Codex CLI app-server schema window, looks for `codex` in the usual command-line locations, and apps can provide an exact executable path when they need stricter control.

For copy-pasteable startup code, open the DocC getting-started guide:

Expand Down
53 changes: 31 additions & 22 deletions ROADMAP.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Sources/SwiftASB/Protocol/CodexAppServerProtocol+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,14 @@ struct CodexProtocolThreadListResponse: Decodable, Equatable, Sendable {

struct CodexProtocolThreadTurnsListParams: Encodable, Equatable, Sendable {
let cursor: String?
let itemsView: CodexWireTurnItemsView?
let limit: Int?
let sortDirection: CodexProtocolThreadTurnsSortDirection?
let threadID: String

enum CodingKeys: String, CodingKey {
case cursor
case itemsView
case limit
case sortDirection
case threadID = "threadId"
Expand Down
23 changes: 23 additions & 0 deletions Sources/SwiftASB/Protocol/CodexAppServerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct CodexAppServerProtocol {
case threadStart = "thread/start"
case threadMetadataUpdate = "thread/metadata/update"
case threadTurnsList = "thread/turns/list"
case threadTurnsItemsList = "thread/turns/items/list"
case threadLoadedList = "thread/loaded/list"
case threadUnarchive = "thread/unarchive"
case threadGoalGet = "thread/goal/get"
Expand Down Expand Up @@ -190,6 +191,16 @@ struct CodexAppServerProtocol {
)
}

func makeThreadTurnsItemsListRequest(
id: CodexRPCRequestID,
params: CodexWireThreadTurnsItemsListParams
) throws -> Data {
try encodeRequest(
JSONRPCRequestEnvelope(id: id, method: .threadTurnsItemsList, params: params),
method: .threadTurnsItemsList
)
}

func makeThreadLoadedListRequest(
id: CodexRPCRequestID,
params: CodexWireThreadLoadedListParams
Expand Down Expand Up @@ -609,6 +620,18 @@ struct CodexAppServerProtocol {
)
}

func decodeThreadTurnsItemsListResponse(
_ responsePayload: Data,
expectedID: CodexRPCRequestID
) throws -> CodexWireThreadTurnsItemsListResponse {
try decodeResponse(
responsePayload,
expectedID: expectedID,
method: .threadTurnsItemsList,
resultType: CodexWireThreadTurnsItemsListResponse.self
)
}

func decodeThreadLoadedListResponse(
_ responsePayload: Data,
expectedID: CodexRPCRequestID
Expand Down
43 changes: 43 additions & 0 deletions Sources/SwiftASB/Public/CodexAppServer+CodexExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public extension CodexAppServer {

public var currentDirectoryPaths: [String]?
public var forceReload: Bool?
/// Deprecated by Codex CLI 0.130.0. The app-server no longer accepts
/// per-cwd extra skill roots on `skills/list`.
public var perCurrentDirectoryExtraUserRoots: [ExtraUserRootsForCurrentDirectory]?

public init(
Expand Down Expand Up @@ -220,13 +222,21 @@ public extension CodexAppServer {
public struct PluginDetail: Sendable, Equatable {
public let apps: [AppSummary]
public let description: String?
public let hooks: [PluginHookSummary]
public let marketplaceName: String
public let marketplacePath: String?
public let mcpServers: [String]
public let skills: [SkillSummary]
public let summary: PluginSummary
}

public struct PluginHookSummary: Sendable, Equatable, Identifiable {
public var id: String { key }

public let eventName: HookMetadata.EventName
public let key: String
}

public struct AppSummary: Sendable, Equatable, Identifiable {
public let description: String?
public let id: String
Expand Down Expand Up @@ -500,6 +510,7 @@ extension CodexAppServer.CodexExtensions.PluginDetail {
self.init(
apps: wireValue.apps.map(CodexAppServer.CodexExtensions.AppSummary.init),
description: wireValue.description,
hooks: wireValue.hooks.map(CodexAppServer.CodexExtensions.PluginHookSummary.init),
marketplaceName: wireValue.marketplaceName,
marketplacePath: wireValue.marketplacePath,
mcpServers: wireValue.mcpServers,
Expand All @@ -509,6 +520,38 @@ extension CodexAppServer.CodexExtensions.PluginDetail {
}
}

extension CodexAppServer.CodexExtensions.PluginHookSummary {
init(wireValue: CodexWirePluginHookSummary) {
self.init(
eventName: .init(wireValue: wireValue.eventName),
key: wireValue.key
)
}
}

extension CodexAppServer.HookMetadata.EventName {
init(wireValue: CodexWireHookEventName) {
switch wireValue {
case .permissionRequest:
self = .permissionRequest
case .postCompact:
self = .postCompact
case .postToolUse:
self = .postToolUse
case .preCompact:
self = .preCompact
case .preToolUse:
self = .preToolUse
case .sessionStart:
self = .sessionStart
case .stop:
self = .stop
case .userPromptSubmit:
self = .userPromptSubmit
}
}
}

extension CodexAppServer.CodexExtensions.AppSummary {
init(wireValue: CodexWireAppSummary) {
self.init(
Expand Down
48 changes: 46 additions & 2 deletions Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -407,25 +407,36 @@ extension CodexAppServer {
case desc
}

/// Amount of item detail to include when listing stored turns.
public enum TurnItemsView: String, Sendable, Equatable {
case full
case notLoaded
case summary
}

public struct ThreadTurnsListRequest: Sendable, Equatable {
public var cursor: String?
public var itemsView: TurnItemsView?
public var limit: Int?
public var sortDirection: ThreadTurnsSortDirection?
public var threadID: String

/// Creates a paged turn-list request for a stored thread.
///
/// Nil pagination and sort fields are omitted, which keeps the
/// app-server in charge of its default page size and ordering.
/// Nil pagination, item-view, and sort fields are omitted, which keeps
/// the app-server in charge of its default page size, item detail, and
/// ordering.
public init(
threadID: String,
limit: Int? = nil,
cursor: String? = nil,
itemsView: TurnItemsView? = nil,
sortDirection: ThreadTurnsSortDirection? = nil
) {
self.threadID = threadID
self.limit = limit
self.cursor = cursor
self.itemsView = itemsView
self.sortDirection = sortDirection
}
}
Expand All @@ -437,6 +448,39 @@ extension CodexAppServer {
public let turns: [TurnInfo]
}

public struct ThreadTurnsItemsListRequest: Sendable, Equatable {
public var cursor: String?
public var limit: Int?
public var sortDirection: ThreadTurnsSortDirection?
public var threadID: String
public var turnID: String

/// Creates a paged item-list request for one stored turn.
///
/// Nil pagination and sort fields are omitted, which keeps the
/// app-server in charge of its default page size and ordering.
public init(
threadID: String,
turnID: String,
limit: Int? = nil,
cursor: String? = nil,
sortDirection: ThreadTurnsSortDirection? = nil
) {
self.threadID = threadID
self.turnID = turnID
self.limit = limit
self.cursor = cursor
self.sortDirection = sortDirection
}
}

/// One page of stored item-history results for a turn.
public struct ThreadTurnsItemsPage: Sendable, Equatable {
public let backwardsCursor: String?
public let items: [CodexTurnItem]
public let nextCursor: String?
}

/// Current app-server status for a thread.
public struct ThreadStatus: Sendable, Equatable {
public let type: ThreadStatusType
Expand Down
24 changes: 24 additions & 0 deletions Sources/SwiftASB/Public/CodexAppServer+WireMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,30 @@ extension CodexProtocolThreadTurnsSortDirection {
}
}

extension CodexWireSortDirection {
init(_ direction: CodexAppServer.ThreadTurnsSortDirection) {
switch direction {
case .asc:
self = .asc
case .desc:
self = .desc
}
}
}

extension CodexWireTurnItemsView {
init(_ itemsView: CodexAppServer.TurnItemsView) {
switch itemsView {
case .full:
self = .full
case .notLoaded:
self = .notLoaded
case .summary:
self = .summary
}
}
}

extension CodexProtocolThreadListSortKey {
init(_ key: CodexAppServer.ThreadListSortKey) {
switch key {
Expand Down
53 changes: 46 additions & 7 deletions Sources/SwiftASB/Public/CodexAppServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,11 @@ public actor CodexAppServer {
_ request: CodexExtensions.SkillListRequest
) async throws -> CodexExtensions.SkillListSnapshot {
try requireInitialized(for: "skills/list")
if request.perCurrentDirectoryExtraUserRoots != nil {
throw CodexAppServerError.invalidState(
reason: "Codex CLI 0.130.0 removed per-cwd extra user roots from skills/list; pass currentDirectoryPaths and forceReload only."
)
}

let requestID = CodexRPCRequestID.generated()

Expand All @@ -1108,13 +1113,7 @@ public actor CodexAppServer {
id: requestID,
params: .init(
cwds: request.currentDirectoryPaths,
forceReload: request.forceReload,
perCwdExtraUserRoots: request.perCurrentDirectoryExtraUserRoots?.map {
.init(
cwd: $0.currentDirectoryPath,
extraUserRoots: $0.extraUserRoots
)
}
forceReload: request.forceReload
)
)
let responsePayload = try await transport.send(requestPayload, id: requestID)
Expand Down Expand Up @@ -1263,6 +1262,7 @@ public actor CodexAppServer {
id: requestID,
params: .init(
cursor: request.cursor,
itemsView: request.itemsView.map(CodexWireTurnItemsView.init),
limit: request.limit,
sortDirection: request.sortDirection.map(CodexProtocolThreadTurnsSortDirection.init),
threadID: request.threadID
Expand Down Expand Up @@ -1294,6 +1294,45 @@ public actor CodexAppServer {
}
}

/// Reads a page of stored items for one turn directly from the app-server.
///
/// This low-level paging API returns app-server item snapshots without
/// assuming the caller has loaded the full containing turn. Paged item reads
/// do not mutate SwiftASB's local history store because a single item page
/// does not carry enough information to safely reconcile whole-turn item
/// ordering.
public func listThreadTurnItems(_ request: ThreadTurnsItemsListRequest) async throws -> ThreadTurnsItemsPage {
try requireInitialized(for: "thread/turns/items/list")

let requestID = CodexRPCRequestID.generated()

do {
let requestPayload = try protocolLayer.makeThreadTurnsItemsListRequest(
id: requestID,
params: .init(
cursor: request.cursor,
limit: request.limit,
sortDirection: request.sortDirection.map(CodexWireSortDirection.init),
threadID: request.threadID,
turnID: request.turnID
)
)
let responsePayload = try await transport.send(requestPayload, id: requestID)
let response = try protocolLayer.decodeThreadTurnsItemsListResponse(
responsePayload,
expectedID: requestID
)

return .init(
backwardsCursor: response.backwardsCursor,
items: response.data.map(CodexTurnItem.init(wireValue:)),
nextCursor: response.nextCursor
)
} catch {
throw CodexAppServerError.wrap(error, operation: "thread/turns/items/list")
}
}

/// Starts a turn from an app-server-owned request.
///
/// Most consumers should prefer `CodexThread.startTurn(_:)` or
Expand Down
5 changes: 5 additions & 0 deletions Sources/SwiftASB/SwiftASB.docc/CodexExtensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ The namespace is read-only. Plugin install, uninstall, marketplace mutation, and
skill config writes remain unpromoted until SwiftASB has a clearer permission
and user-review story for those operations.

Plugin detail reads include app, skill, MCP server, and hook summaries so an
extension inspector can show which entry points a plugin contributes without
reading plugin files directly.

## Topics

### Reads
Expand Down Expand Up @@ -55,6 +59,7 @@ and user-review story for those operations.
- ``PluginInterface``
- ``PluginReadRequest``
- ``PluginDetail``
- ``PluginHookSummary``
- ``MarketplaceLoadError``

### Collaboration Modes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal struct CodexCLIExecutableResolver {
internal let patch: Int

private static let regex = try! NSRegularExpression(pattern: #"(\d+)\.(\d+)\.(\d+)"#)
internal static let latestSupportedPublicRelease = Version(major: 0, minor: 129, patch: 0)
internal static let latestSupportedPublicRelease = Version(major: 0, minor: 130, patch: 0)

internal static var documentedWindowDescription: String {
let latest = latestSupportedPublicRelease
Expand Down
Loading