From dd8604b2d16a07cefcb6f837e5e8b3e039d273de Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Mon, 4 May 2026 20:12:26 -0400 Subject: [PATCH 01/12] Add ASC TestFlight release workflow --- .asc/workflow.json | 144 ++++++++++++++++++++++++++++++++++++++ .gitignore | 2 + README.md | 25 +++++++ Tools/set_build_number.rb | 24 +++++++ 4 files changed, 195 insertions(+) create mode 100644 .asc/workflow.json create mode 100644 Tools/set_build_number.rb diff --git a/.asc/workflow.json b/.asc/workflow.json new file mode 100644 index 0000000..3874bbc --- /dev/null +++ b/.asc/workflow.json @@ -0,0 +1,144 @@ +{ + "env": { + "ASC_PROFILE": "Runline", + "APP_ID": "6766299514", + "BUNDLE_ID": "com.matthewparris.runline", + "VERSION": "1.0", + "PROJECT": "CursorMobile.xcodeproj", + "SCHEME": "CursorMobile", + "SIM_DESTINATION": "platform=iOS Simulator,name=iPhone 17,OS=26.4.1", + "TESTFLIGHT_GROUP_ID": "9ddf92fa-5b17-48ca-9833-5d83c3e31d3d", + "INTERNAL_TESTFLIGHT_GROUP_ID": "a252108c-c4bc-4b6b-94cd-519b7be04c68", + "LOCALE": "en-US", + "TEST_NOTES": "Verify Cloud Agent and SDK Agent workflows, iPhone and iPad layouts, chat composer, attachments, model and MCP controls, Settings, light mode, and dark mode." + }, + "before_all": "asc --profile $ASC_PROFILE auth status --validate --output json >/dev/null", + "after_all": "echo workflow_done >&2", + "error": "echo workflow_failed >&2", + "workflows": { + "asc-health": { + "description": "Verify ASC auth, app resolution, TestFlight groups, and the latest visible builds.", + "steps": [ + { + "name": "auth_status", + "run": "asc --profile $ASC_PROFILE auth status --validate --verbose" + }, + { + "name": "app", + "run": "asc --profile $ASC_PROFILE apps view --id $APP_ID --output json" + }, + { + "name": "groups", + "run": "asc --profile $ASC_PROFILE testflight groups list --app $APP_ID --output json" + }, + { + "name": "builds", + "run": "asc --profile $ASC_PROFILE builds list --app $APP_ID --version $VERSION --platform IOS --limit 10 --sort -uploadedDate --output json" + } + ] + }, + "preflight": { + "description": "Run local checks before archiving a TestFlight build.", + "steps": [ + { + "name": "repo_clean", + "run": "test -z \"$(git status --short)\" || { git status --short >&2; echo 'Working tree must be clean before release.' >&2; exit 1; }" + }, + { + "name": "diff_check", + "run": "git diff --check" + }, + { + "name": "orchestrator_typecheck", + "run": "npm --prefix orchestrator run typecheck" + }, + { + "name": "ios_tests", + "run": "xcodebuild test -project $PROJECT -scheme $SCHEME -destination \"$SIM_DESTINATION\"" + } + ] + }, + "testflight": { + "description": "Set an explicit build number, run checks, archive, upload, add What to Test notes, and distribute to TestFlight.", + "env": { + "BUILD_NUMBER": "" + }, + "steps": [ + { + "name": "require_build_number", + "run": "test -n \"$BUILD_NUMBER\" || { echo 'Pass BUILD_NUMBER, for example: asc workflow run testflight BUILD_NUMBER:9' >&2; exit 2; }" + }, + { + "workflow": "preflight" + }, + { + "name": "set_build_number", + "run": "ruby Tools/set_build_number.rb \"$BUILD_NUMBER\"" + }, + { + "name": "generate_project", + "run": "xcodegen generate" + }, + { + "name": "archive", + "run": "asc --profile $ASC_PROFILE xcode archive --project $PROJECT --scheme $SCHEME --configuration Release --clean --overwrite --archive-path \".asc/artifacts/Runline-${VERSION}-${BUILD_NUMBER}.xcarchive\" --xcodebuild-flag=-destination --xcodebuild-flag=generic/platform=iOS --xcodebuild-flag=-allowProvisioningUpdates --output json" + }, + { + "name": "upload", + "run": "asc --profile $ASC_PROFILE xcode export --archive-path \".asc/artifacts/Runline-${VERSION}-${BUILD_NUMBER}.xcarchive\" --export-options ExportOptions-TestFlightUpload.plist --ipa-path \".asc/artifacts/Runline-${VERSION}-${BUILD_NUMBER}.ipa\" --overwrite --wait --xcodebuild-flag=-allowProvisioningUpdates --output json" + }, + { + "name": "resolve_uploaded_build", + "run": "asc --profile $ASC_PROFILE builds info --app $APP_ID --build-number \"$BUILD_NUMBER\" --version \"$VERSION\" --platform IOS --output json", + "outputs": { + "BUILD_ID": "$.data.id" + } + }, + { + "name": "test_notes", + "run": "if [ -n \"$TEST_NOTES\" ]; then asc --profile $ASC_PROFILE builds test-notes create --build-id \"${steps.resolve_uploaded_build.BUILD_ID}\" --locale \"$LOCALE\" --whats-new \"$TEST_NOTES\" || asc --profile $ASC_PROFILE builds test-notes update --build-id \"${steps.resolve_uploaded_build.BUILD_ID}\" --locale \"$LOCALE\" --whats-new \"$TEST_NOTES\"; else echo 'Skipping TestFlight notes because TEST_NOTES is empty.' >&2; fi" + }, + { + "name": "distribute", + "run": "if [ -n \"$TESTFLIGHT_GROUP_ID\" ]; then asc --profile $ASC_PROFILE builds add-groups --build-id \"${steps.resolve_uploaded_build.BUILD_ID}\" --group \"$TESTFLIGHT_GROUP_ID\"; else echo 'Skipping TestFlight group distribution because TESTFLIGHT_GROUP_ID is empty.' >&2; fi" + }, + { + "name": "summary", + "run": "asc --profile $ASC_PROFILE builds info --build-id \"${steps.resolve_uploaded_build.BUILD_ID}\" --output json" + } + ] + }, + "distribute-existing": { + "description": "Attach notes and distribute an already uploaded build by BUILD_ID or BUILD_NUMBER.", + "env": { + "BUILD_ID": "", + "BUILD_NUMBER": "" + }, + "steps": [ + { + "name": "require_build", + "run": "test -n \"$BUILD_ID\" -o -n \"$BUILD_NUMBER\" || { echo 'Pass BUILD_ID or BUILD_NUMBER, for example: asc workflow run distribute-existing BUILD_NUMBER:8' >&2; exit 2; }" + }, + { + "name": "resolve_existing_build", + "run": "if [ -n \"$BUILD_ID\" ]; then asc --profile $ASC_PROFILE builds info --build-id \"$BUILD_ID\" --output json; else asc --profile $ASC_PROFILE builds info --app $APP_ID --build-number \"$BUILD_NUMBER\" --version \"$VERSION\" --platform IOS --output json; fi", + "outputs": { + "BUILD_ID": "$.data.id" + } + }, + { + "name": "test_notes", + "run": "if [ -n \"$TEST_NOTES\" ]; then asc --profile $ASC_PROFILE builds test-notes create --build-id \"${steps.resolve_existing_build.BUILD_ID}\" --locale \"$LOCALE\" --whats-new \"$TEST_NOTES\" || asc --profile $ASC_PROFILE builds test-notes update --build-id \"${steps.resolve_existing_build.BUILD_ID}\" --locale \"$LOCALE\" --whats-new \"$TEST_NOTES\"; else echo 'Skipping TestFlight notes because TEST_NOTES is empty.' >&2; fi" + }, + { + "name": "distribute", + "run": "if [ -n \"$TESTFLIGHT_GROUP_ID\" ]; then asc --profile $ASC_PROFILE builds add-groups --build-id \"${steps.resolve_existing_build.BUILD_ID}\" --group \"$TESTFLIGHT_GROUP_ID\"; else echo 'Skipping TestFlight group distribution because TESTFLIGHT_GROUP_ID is empty.' >&2; fi" + }, + { + "name": "summary", + "run": "asc --profile $ASC_PROFILE builds info --build-id \"${steps.resolve_existing_build.BUILD_ID}\" --output json" + } + ] + } + } +} diff --git a/.gitignore b/.gitignore index 800ff8b..340be82 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ node_modules/ **/node_modules/ # Local secrets and generated config +.asc/artifacts/ +.asc/runs/ .env .env.* *.local.xcconfig diff --git a/README.md b/README.md index 51a5af4..ef52ef8 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,28 @@ Runline is independent and is not affiliated with, endorsed by, or connected to - Keychain-backed Cursor API key storage - Local cache for account, repositories, models, agents, runs, stream events, artifacts, notification preferences, and launch draft - Unit tests for Cursor v1 request contracts, SSE parsing, cache persistence, app routing, push payloads, chat event cleanup, file attachment loading, and SDK bridge request mapping + +## Release Workflow + +ASC is configured through the local `Runline` keychain profile. Release automation lives in `.asc/workflow.json`. + +Validate the workflow: + +```bash +asc workflow validate +asc workflow list +``` + +Run local release checks: + +```bash +asc workflow run preflight +``` + +Upload the next TestFlight build with an explicit build number: + +```bash +asc workflow run testflight BUILD_NUMBER:9 +``` + +Use explicit build numbers so release numbering stays aligned with the active Runline sequence. The current TestFlight build is `1.0 (8)`, so the next upload should use build `9`. diff --git a/Tools/set_build_number.rb b/Tools/set_build_number.rb new file mode 100644 index 0000000..4e28b6e --- /dev/null +++ b/Tools/set_build_number.rb @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +build_number = ARGV.fetch(0, "").strip + +unless build_number.match?(/\A[1-9][0-9]*\z/) + warn "Usage: ruby Tools/set_build_number.rb BUILD_NUMBER" + warn "BUILD_NUMBER must be a positive integer." + exit 2 +end + +project_path = File.expand_path("../project.yml", __dir__) +contents = File.read(project_path) + +pattern = /^(\s*CURRENT_PROJECT_VERSION:\s*)"?[0-9]+"?$/ + +unless contents.match?(pattern) + warn "Could not find CURRENT_PROJECT_VERSION in #{project_path}." + exit 1 +end + +updated = contents.sub(pattern, "\\1\"#{build_number}\"") +File.write(project_path, updated) +warn "Set CURRENT_PROJECT_VERSION to #{build_number} in project.yml." From 8c07052c7c1636a8937f197f5a64399df02fb9a8 Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Mon, 4 May 2026 20:33:17 -0400 Subject: [PATCH 02/12] Gate SDK agent on bridge readiness --- CursorMobile/App/AppState.swift | 112 ++++++++++++++- CursorMobile/App/RootView.swift | 25 +++- CursorMobile/Features/NewChatSheet.swift | 20 ++- CursorMobile/Features/SettingsView.swift | 136 +++++++++++------- CursorMobile/Networking/SDKBridgeClient.swift | 25 ++++ CursorMobileTests/CursorMobileTests.swift | 66 +++++++++ CursorMobileTests/SDKBridgeClientTests.swift | 10 ++ orchestrator/README.md | 8 ++ 8 files changed, 338 insertions(+), 64 deletions(-) diff --git a/CursorMobile/App/AppState.swift b/CursorMobile/App/AppState.swift index 4ffae82..fc8e1ce 100644 --- a/CursorMobile/App/AppState.swift +++ b/CursorMobile/App/AppState.swift @@ -32,6 +32,7 @@ final class AppState { var loadingEndpointIDs: Set = [] var endpointPageOverrides: [CursorAPIEndpoint.ID: Int] = [:] var sdkBridgeProfiles: [SDKBridgeMCPProfile] = [] + var sdkBridgeConnectionState: SDKBridgeConnectionState = SDKBridgePreferences.isEnabled() ? .unchecked : .disabled var focusedAgentID: Agent.ID? var notificationPreferences = NotificationPreferences() var notificationAuthorizationStatus: UNAuthorizationStatus = .notDetermined @@ -121,18 +122,37 @@ final class AppState { return true } - var sdkBridgeLaunchIssue: String? { - guard launchDraft.runMode == .sdkBridge else { return nil } + var isSDKBridgeReadyForLaunch: Bool { + sdkBridgeReadinessIssue == nil + } + + var sdkBridgeReadinessIssue: String? { guard account != nil else { return "Connect a Cursor API key before using SDK Agent." } guard SDKBridgePreferences.isEnabled() else { - return "Enable the SDK bridge in Settings before launching SDK Agent." + return "Enable the SDK bridge in Settings before using SDK Agent." } guard SDKBridgePreferences.configuredBaseURL() != nil else { return "Enter a valid SDK bridge URL in Settings." } - return nil + switch sdkBridgeConnectionState { + case .connected: + return nil + case .disabled: + return "Enable the SDK bridge in Settings before using SDK Agent." + case .unchecked: + return "Check the SDK bridge connection in Settings before using SDK Agent." + case .checking: + return "Runline is checking the SDK bridge connection." + case .failed(let message): + return "SDK bridge is unavailable. \(message)" + } + } + + var sdkBridgeLaunchIssue: String? { + guard launchDraft.runMode == .sdkBridge else { return nil } + return sdkBridgeReadinessIssue } func restoreConnectionIfAvailable() async { @@ -171,6 +191,7 @@ final class AppState { provider = cursorProvider enterpriseProvider = cursorProvider as? EnterpriseDataProvider account = validatedAccount + syncSDKBridgeConfiguration() } catch { provider = nil enterpriseProvider = nil @@ -209,6 +230,7 @@ final class AppState { sdkBridgeRunIDs = [] sdkBridgeMCPProfileIDsByAgentID = [:] sdkBridgeProfiles = [] + sdkBridgeConnectionState = SDKBridgePreferences.isEnabled() ? .unchecked : .disabled endpointResults = [:] loadingEndpointIDs = [] endpointPageOverrides = [:] @@ -337,10 +359,22 @@ final class AppState { } func applyDefaultRunMode(_ mode: AgentRunMode) { + if mode == .sdkBridge, !isSDKBridgeReadyForLaunch { + launchDraft.runMode = .cloudAgent + saveCachedState() + return + } launchDraft.runMode = mode saveCachedState() } + func ensureLaunchRunModeIsAvailable() { + if launchDraft.runMode == .sdkBridge, !isSDKBridgeReadyForLaunch { + launchDraft.runMode = .cloudAgent + saveCachedState() + } + } + func isSDKBridgeRun(runID: AgentRun.ID) -> Bool { sdkBridgeRunIDs.contains(runID) } @@ -358,6 +392,58 @@ final class AppState { sdkBridgeMCPProfileIDsByAgentID[agent.id] } + func syncSDKBridgeConfiguration(resetConnection: Bool = false) { + guard SDKBridgePreferences.isEnabled() else { + sdkBridgeConnectionState = .disabled + sdkBridgeProfiles = [] + ensureLaunchRunModeIsAvailable() + return + } + guard SDKBridgePreferences.configuredBaseURL() != nil else { + sdkBridgeConnectionState = .failed("The bridge URL is invalid.") + sdkBridgeProfiles = [] + ensureLaunchRunModeIsAvailable() + return + } + if resetConnection || !sdkBridgeConnectionState.isConnected { + sdkBridgeConnectionState = .unchecked + sdkBridgeProfiles = [] + ensureLaunchRunModeIsAvailable() + } + } + + func checkSDKBridgeConnection() async { + guard SDKBridgePreferences.isEnabled() else { + sdkBridgeConnectionState = .disabled + sdkBridgeProfiles = [] + ensureLaunchRunModeIsAvailable() + return + } + guard let baseURL = SDKBridgePreferences.configuredBaseURL() else { + sdkBridgeConnectionState = .failed("The bridge URL is invalid.") + sdkBridgeProfiles = [] + ensureLaunchRunModeIsAvailable() + return + } + + sdkBridgeConnectionState = .checking + do { + let health = try await sdkBridgeClientFactory(baseURL, nil).health() + if health.ok { + sdkBridgeConnectionState = .connected("\(health.service) - \(health.sdk)") + await reloadSDKBridgeProfiles() + } else { + sdkBridgeConnectionState = .failed("The bridge responded but reported an unhealthy status.") + sdkBridgeProfiles = [] + ensureLaunchRunModeIsAvailable() + } + } catch { + sdkBridgeConnectionState = .failed(sdkBridgeConnectionFailureMessage(for: error, baseURL: baseURL)) + sdkBridgeProfiles = [] + ensureLaunchRunModeIsAvailable() + } + } + func reloadSDKBridgeProfiles() async { guard SDKBridgePreferences.isEnabled(), let baseURL = SDKBridgePreferences.configuredBaseURL() else { @@ -1143,10 +1229,28 @@ final class AppState { return String(firstLine.prefix(45)) + "..." } + private func sdkBridgeConnectionFailureMessage(for error: Error, baseURL: URL) -> String { + if SDKBridgePreferences.isLoopback(baseURL) { + return "Runline cannot reach \(baseURL.absoluteString). On a physical iPhone, localhost points to the phone. Use your Mac LAN URL or a hosted HTTPS bridge." + } + if let urlError = error as? URLError { + switch urlError.code { + case .cannotConnectToHost, .cannotFindHost, .networkConnectionLost, .notConnectedToInternet, .timedOut: + return "Runline cannot reach \(baseURL.absoluteString). Start the bridge server or update the URL." + default: + break + } + } + return error.localizedDescription + } + private func userMessage(from error: Error) -> String { if let apiError = error as? CursorAPIError { return apiError.userMessage } + if let bridgeError = error as? SDKBridgeUnavailableError { + return bridgeError.errorDescription ?? error.localizedDescription + } return error.localizedDescription } diff --git a/CursorMobile/App/RootView.swift b/CursorMobile/App/RootView.swift index aff546a..333f54d 100644 --- a/CursorMobile/App/RootView.swift +++ b/CursorMobile/App/RootView.swift @@ -82,6 +82,7 @@ struct RootView: View { private struct WorkflowModeChooserSheet: View { @Environment(\.dismiss) private var dismiss + @Environment(AppState.self) private var appState let selectedMode: AgentRunMode let choose: (AgentRunMode) -> Void @@ -91,7 +92,7 @@ private struct WorkflowModeChooserSheet: View { Section { WorkflowModeButton( mode: .cloudAgent, - isSelected: selectedMode == .cloudAgent, + isSelected: effectiveSelectedMode == .cloudAgent, symbolName: "icloud", detail: "Start a cloud run, monitor progress, preview artifacts, and follow up after completion.", choose: select @@ -99,9 +100,10 @@ private struct WorkflowModeChooserSheet: View { WorkflowModeButton( mode: .sdkBridge, - isSelected: selectedMode == .sdkBridge, + isSelected: effectiveSelectedMode == .sdkBridge, + isEnabled: appState.isSDKBridgeReadyForLaunch, symbolName: "point.3.connected.trianglepath.dotted", - detail: "Use the SDK bridge for a richer conversation with models, MCP profiles, files, images, planning, and execution.", + detail: sdkAgentDetail, choose: select ) } footer: { @@ -113,7 +115,21 @@ private struct WorkflowModeChooserSheet: View { } } + private var sdkAgentDetail: String { + appState.isSDKBridgeReadyForLaunch + ? "Use the SDK bridge for a richer conversation with models, MCP profiles, files, images, planning, and execution." + : "Requires a connected SDK bridge. Choose Cloud Agent now, then enable and check the bridge in Settings." + } + + private var effectiveSelectedMode: AgentRunMode { + selectedMode == .sdkBridge && !appState.isSDKBridgeReadyForLaunch ? .cloudAgent : selectedMode + } + private func select(_ mode: AgentRunMode) { + if mode == .sdkBridge, !appState.isSDKBridgeReadyForLaunch { + appState.errorMessage = appState.sdkBridgeReadinessIssue + return + } choose(mode) dismiss() } @@ -122,6 +138,7 @@ private struct WorkflowModeChooserSheet: View { private struct WorkflowModeButton: View { let mode: AgentRunMode let isSelected: Bool + var isEnabled = true let symbolName: String let detail: String let choose: (AgentRunMode) -> Void @@ -155,8 +172,10 @@ private struct WorkflowModeButton: View { } } .padding(.vertical, 4) + .opacity(isEnabled ? 1 : 0.55) } .buttonStyle(.plain) + .disabled(!isEnabled) } } diff --git a/CursorMobile/Features/NewChatSheet.swift b/CursorMobile/Features/NewChatSheet.swift index 698b9a5..5aa08f5 100644 --- a/CursorMobile/Features/NewChatSheet.swift +++ b/CursorMobile/Features/NewChatSheet.swift @@ -68,7 +68,7 @@ struct NewChatForm: View { Form { Section("Run Mode") { Picker("Run Mode", selection: runModeBinding) { - ForEach(AgentRunMode.allCases) { mode in + ForEach(availableRunModes) { mode in Text(mode.title).tag(mode) } } @@ -76,8 +76,8 @@ struct NewChatForm: View { LabeledContent("Mode", value: appState.launchDraft.runMode.detail) - if let issue = appState.sdkBridgeLaunchIssue { - Text(issue) + if let issue = appState.sdkBridgeReadinessIssue { + Label(issue, systemImage: "point.3.connected.trianglepath.dotted") .font(.footnote) .foregroundStyle(.secondary) } @@ -253,11 +253,16 @@ struct NewChatForm: View { } } .task { + appState.syncSDKBridgeConfiguration() + appState.ensureLaunchRunModeIsAvailable() seedSourceFields() if appState.launchDraft.runMode == .sdkBridge { await appState.reloadSDKBridgeProfiles() } } + .onChange(of: appState.sdkBridgeConnectionState) { _, _ in + appState.ensureLaunchRunModeIsAvailable() + } .onChange(of: appState.launchDraft.runMode) { _, mode in guard mode == .sdkBridge else { return } Task { @@ -326,10 +331,19 @@ struct NewChatForm: View { Binding { appState.launchDraft.runMode } set: { mode in + if mode == .sdkBridge, !appState.isSDKBridgeReadyForLaunch { + appState.errorMessage = appState.sdkBridgeReadinessIssue + appState.launchDraft.runMode = .cloudAgent + return + } appState.launchDraft.runMode = mode } } + private var availableRunModes: [AgentRunMode] { + appState.isSDKBridgeReadyForLaunch ? AgentRunMode.allCases : [.cloudAgent] + } + private var modelSelectionBinding: Binding { Binding { NewChatModelPickerOptions.selection(from: appState.launchDraft.modelID) diff --git a/CursorMobile/Features/SettingsView.swift b/CursorMobile/Features/SettingsView.swift index e57cb00..60561a1 100644 --- a/CursorMobile/Features/SettingsView.swift +++ b/CursorMobile/Features/SettingsView.swift @@ -18,7 +18,6 @@ struct SettingsFormContent: View { @AppStorage(SDKBridgePreferences.isEnabledKey) private var isSDKBridgeEnabled = SDKBridgePreferences.defaultIsEnabled @AppStorage(SDKBridgePreferences.baseURLKey) private var sdkBridgeBaseURL = SDKBridgePreferences.defaultBaseURLString @State private var enterpriseAPIKey = "" - @State private var sdkBridgeHealth: SDKBridgeHealthCheckState = .idle @FocusState private var focusedField: Field? private enum Field { @@ -80,30 +79,46 @@ struct SettingsFormContent: View { .disabled(!isSDKBridgeEnabled) if isSDKBridgeEnabled { - LabeledContent("Status") { - Label(sdkBridgeHealth.title, systemImage: sdkBridgeHealth.systemImage) - .foregroundStyle(sdkBridgeHealth.tint) + HStack(spacing: 12) { + Text("Status") + Spacer() + Label(sdkBridgeConnectionTitle, systemImage: appState.sdkBridgeConnectionState.systemImage) + .foregroundStyle(appState.sdkBridgeConnectionState.tint) + .labelStyle(.titleAndIcon) + .multilineTextAlignment(.trailing) } LabeledContent("Profiles", value: "\(appState.sdkBridgeProfiles.count)") + if let detail = sdkBridgeConnectionDetail { + Text(detail) + .font(.footnote) + .foregroundStyle(.secondary) + } + + if let loopbackHelp = SDKBridgePreferences.deviceLoopbackHelp(for: SDKBridgePreferences.baseURL(from: sdkBridgeBaseURL)) { + Text(loopbackHelp) + .font(.footnote) + .foregroundStyle(.secondary) + } + Button { Task { await checkSDKBridgeHealth() } } label: { - if sdkBridgeHealth == .checking { + if appState.sdkBridgeConnectionState == .checking { ProgressView() } else { - Text("Check Connection") + Text(appState.sdkBridgeConnectionState.isConnected ? "Recheck Connection" : "Check Connection") } } - .disabled(sdkBridgeHealth == .checking) + .disabled(appState.sdkBridgeConnectionState == .checking) } } header: { Text("Cursor SDK Agent Bridge") } footer: { - Text("Cloud Agent stays direct from iOS. SDK Agent uses this bridge for resumable SDK sessions, MCP profiles, and subagents.") + Text("Cloud Agent stays direct from iOS. SDK Agent is available only after this bridge connects.") } Section("Enterprise API") { @@ -165,17 +180,19 @@ struct SettingsFormContent: View { .scrollDismissesKeyboard(.interactively) .task { await appState.refreshNotificationStatus() + appState.syncSDKBridgeConfiguration() + ensureDefaultWorkflowSelectionIsAvailable() } .onChange(of: sdkBridgeBaseURL) { _, _ in - sdkBridgeHealth = .idle + appState.syncSDKBridgeConfiguration(resetConnection: true) + ensureDefaultWorkflowSelectionIsAvailable() } .onChange(of: isSDKBridgeEnabled) { _, _ in - sdkBridgeHealth = .idle - if isSDKBridgeEnabled { - Task { - await appState.reloadSDKBridgeProfiles() - } - } + appState.syncSDKBridgeConfiguration(resetConnection: true) + ensureDefaultWorkflowSelectionIsAvailable() + } + .onChange(of: appState.sdkBridgeConnectionState) { _, _ in + ensureDefaultWorkflowSelectionIsAvailable() } } @@ -183,9 +200,17 @@ struct SettingsFormContent: View { Binding { defaultRunModeRawValue } set: { rawValue in + let mode = RunlineWorkflowPreferences.runMode(from: rawValue) + if mode == .sdkBridge, !appState.isSDKBridgeReadyForLaunch { + appState.errorMessage = appState.sdkBridgeReadinessIssue + defaultRunModeRawValue = AgentRunMode.cloudAgent.rawValue + didChooseDefaultRunMode = true + appState.applyDefaultRunMode(.cloudAgent) + return + } defaultRunModeRawValue = rawValue didChooseDefaultRunMode = true - appState.applyDefaultRunMode(RunlineWorkflowPreferences.runMode(from: rawValue)) + appState.applyDefaultRunMode(mode) } } @@ -206,6 +231,41 @@ struct SettingsFormContent: View { } } + private var sdkBridgeConnectionTitle: String { + switch appState.sdkBridgeConnectionState { + case .disabled: + "Disabled" + case .unchecked: + "Not Checked" + case .checking: + "Checking" + case .connected: + "Connected" + case .failed: + "Unavailable" + } + } + + private var sdkBridgeConnectionDetail: String? { + switch appState.sdkBridgeConnectionState { + case .connected(let message), .failed(let message): + message + case .unchecked: + "Check the bridge before selecting SDK Agent. The bridge must be reachable from this device." + case .disabled, .checking: + nil + } + } + + private func ensureDefaultWorkflowSelectionIsAvailable() { + guard RunlineWorkflowPreferences.runMode(from: defaultRunModeRawValue) == .sdkBridge, + !appState.isSDKBridgeReadyForLaunch else { + return + } + defaultRunModeRawValue = AgentRunMode.cloudAgent.rawValue + appState.applyDefaultRunMode(.cloudAgent) + } + private func saveEnterpriseKey() { let key = enterpriseAPIKey enterpriseAPIKey = "" @@ -217,23 +277,7 @@ struct SettingsFormContent: View { private func checkSDKBridgeHealth() async { focusedField = nil - guard let baseURL = SDKBridgePreferences.baseURL(from: sdkBridgeBaseURL) else { - sdkBridgeHealth = .failed("Invalid URL") - return - } - - sdkBridgeHealth = .checking - do { - let health = try await SDKBridgeClient(baseURL: baseURL).health() - sdkBridgeHealth = health.ok - ? .healthy("\(health.service) - \(health.sdk)") - : .failed("Bridge responded unhealthy") - if health.ok { - await appState.reloadSDKBridgeProfiles() - } - } catch { - sdkBridgeHealth = .failed(error.localizedDescription) - } + await appState.checkSDKBridgeConnection() } private func notificationBinding(_ keyPath: WritableKeyPath) -> Binding { @@ -247,30 +291,14 @@ struct SettingsFormContent: View { } } -private enum SDKBridgeHealthCheckState: Equatable { - case idle - case checking - case healthy(String) - case failed(String) - - var title: String { - switch self { - case .idle: - "Not Checked" - case .checking: - "Checking" - case .healthy(let message), .failed(let message): - message - } - } - +private extension SDKBridgeConnectionState { var systemImage: String { switch self { - case .idle: + case .disabled, .unchecked: "circle" case .checking: "clock" - case .healthy: + case .connected: "checkmark.circle" case .failed: "exclamationmark.circle" @@ -279,11 +307,11 @@ private enum SDKBridgeHealthCheckState: Equatable { var tint: Color { switch self { - case .healthy: + case .connected: .green case .failed: .red - case .checking, .idle: + case .checking, .disabled, .unchecked: .secondary } } diff --git a/CursorMobile/Networking/SDKBridgeClient.swift b/CursorMobile/Networking/SDKBridgeClient.swift index 613ec88..9b012be 100644 --- a/CursorMobile/Networking/SDKBridgeClient.swift +++ b/CursorMobile/Networking/SDKBridgeClient.swift @@ -20,6 +20,21 @@ struct SDKBridgeHealthResponse: Decodable, Equatable { var sdk: String } +enum SDKBridgeConnectionState: Equatable { + case disabled + case unchecked + case checking + case connected(String) + case failed(String) + + var isConnected: Bool { + if case .connected = self { + return true + } + return false + } +} + enum SDKBridgePreferences { static let isEnabledKey = "sdkBridge.isEnabled" static let baseURLKey = "sdkBridge.baseURL" @@ -48,6 +63,16 @@ enum SDKBridgePreferences { } return url } + + static func isLoopback(_ url: URL?) -> Bool { + guard let host = url?.host?.lowercased() else { return false } + return host == "localhost" || host == "127.0.0.1" || host == "::1" + } + + static func deviceLoopbackHelp(for url: URL?) -> String? { + guard isLoopback(url) else { return nil } + return "On a physical iPhone, localhost points to the phone. For device testing, run the bridge on your Mac and use your Mac LAN URL, for example http://192.168.1.10:8787." + } } struct SDKBridgePromptImageDimensionRequest: Encodable, Equatable { diff --git a/CursorMobileTests/CursorMobileTests.swift b/CursorMobileTests/CursorMobileTests.swift index f0dbcf7..69d028a 100644 --- a/CursorMobileTests/CursorMobileTests.swift +++ b/CursorMobileTests/CursorMobileTests.swift @@ -124,6 +124,47 @@ final class CursorMobileTests: XCTestCase { XCTAssertTrue(appState.canLaunchAgent) } + @MainActor + func testSDKLaunchRequiresConnectedBridge() { + withSDKBridgeDefaults(enabled: true, baseURL: "http://localhost:8787") { + let appState = AppState(provider: MockAgentProvider(), apiKeyStore: InMemoryAPIKeyStore(apiKey: "cursor-test-key")) + appState.account = ProviderAccount( + apiKeyName: "Runline Test Key", + userEmail: "test@example.com", + createdAt: .now + ) + appState.launchDraft.prompt.text = "Plan the settings cleanup" + appState.launchDraft.runMode = .sdkBridge + appState.sdkBridgeConnectionState = .unchecked + + XCTAssertFalse(appState.canLaunchAgent) + XCTAssertEqual(appState.sdkBridgeLaunchIssue, "Check the SDK bridge connection in Settings before using SDK Agent.") + + appState.sdkBridgeConnectionState = .connected("runline-orchestrator - @cursor/sdk") + + XCTAssertTrue(appState.canLaunchAgent) + XCTAssertNil(appState.sdkBridgeLaunchIssue) + } + } + + @MainActor + func testUnavailableSDKModeFallsBackToCloudAgent() { + withSDKBridgeDefaults(enabled: true, baseURL: "http://localhost:8787") { + let appState = AppState(provider: MockAgentProvider(), apiKeyStore: InMemoryAPIKeyStore(apiKey: "cursor-test-key")) + appState.account = ProviderAccount( + apiKeyName: "Runline Test Key", + userEmail: "test@example.com", + createdAt: .now + ) + appState.launchDraft.runMode = .sdkBridge + appState.sdkBridgeConnectionState = .failed("Runline cannot reach the bridge.") + + appState.ensureLaunchRunModeIsAvailable() + + XCTAssertEqual(appState.launchDraft.runMode, .cloudAgent) + } + } + @MainActor func testWorkspaceRefreshCancellationDoesNotShowGlobalAlert() async { let appState = AppState(provider: CancellingAgentProvider(), apiKeyStore: InMemoryAPIKeyStore()) @@ -185,6 +226,31 @@ final class CursorMobileTests: XCTestCase { let remaining = try await provider.listAgents() XCTAssertFalse(remaining.contains { $0.id == agent.id }) } + + @MainActor + private func withSDKBridgeDefaults(enabled: Bool, baseURL: String, run test: () -> Void) { + let defaults = UserDefaults.standard + let previousEnabled = defaults.object(forKey: SDKBridgePreferences.isEnabledKey) + let previousBaseURL = defaults.object(forKey: SDKBridgePreferences.baseURLKey) + + defaults.set(enabled, forKey: SDKBridgePreferences.isEnabledKey) + defaults.set(baseURL, forKey: SDKBridgePreferences.baseURLKey) + defer { + if let previousEnabled { + defaults.set(previousEnabled, forKey: SDKBridgePreferences.isEnabledKey) + } else { + defaults.removeObject(forKey: SDKBridgePreferences.isEnabledKey) + } + + if let previousBaseURL { + defaults.set(previousBaseURL, forKey: SDKBridgePreferences.baseURLKey) + } else { + defaults.removeObject(forKey: SDKBridgePreferences.baseURLKey) + } + } + + test() + } } @MainActor diff --git a/CursorMobileTests/SDKBridgeClientTests.swift b/CursorMobileTests/SDKBridgeClientTests.swift index 8cb3a11..304a70c 100644 --- a/CursorMobileTests/SDKBridgeClientTests.swift +++ b/CursorMobileTests/SDKBridgeClientTests.swift @@ -33,6 +33,16 @@ final class SDKBridgeClientTests: XCTestCase { XCTAssertEqual(request.header("Accept"), "application/json") } + func testLoopbackBridgeURLShowsDeviceGuidance() throws { + let localhost = try XCTUnwrap(URL(string: "http://localhost:8787")) + let lanURL = try XCTUnwrap(URL(string: "http://192.168.1.10:8787")) + + XCTAssertTrue(SDKBridgePreferences.isLoopback(localhost)) + XCTAssertNotNil(SDKBridgePreferences.deviceLoopbackHelp(for: localhost)) + XCTAssertFalse(SDKBridgePreferences.isLoopback(lanURL)) + XCTAssertNil(SDKBridgePreferences.deviceLoopbackHelp(for: lanURL)) + } + func testCreateCloudRunSendsBearerKeyAndLaunchBody() async throws { MockBridgeURLProtocol.handler = { request in try Self.jsonResponse(for: request, body: [ diff --git a/orchestrator/README.md b/orchestrator/README.md index b1a3015..c772a26 100644 --- a/orchestrator/README.md +++ b/orchestrator/README.md @@ -18,6 +18,14 @@ npm install CURSOR_API_KEY=your-cursor-key npm run dev ``` +For Simulator testing, `http://localhost:8787` is usually enough. For a physical iPhone or TestFlight build on the same Wi-Fi network, use your Mac's LAN address instead: + +```bash +ipconfig getifaddr en0 +``` + +Then set the app's bridge URL to `http://:8787`. For broader TestFlight use, deploy the bridge behind HTTPS and use that hosted URL. + The iOS app can also send a per-request `Authorization: Bearer ` header. Prefer that for user-owned keys; the bridge does not need to store keys server-side. ## MCP Profiles From 54afa1a62a52cd362d62ccc0e507bf857a8b14b5 Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Mon, 4 May 2026 20:40:55 -0400 Subject: [PATCH 03/12] Bump build number to 9 --- CursorMobile.xcodeproj/project.pbxproj | 4 ++-- project.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CursorMobile.xcodeproj/project.pbxproj b/CursorMobile.xcodeproj/project.pbxproj index 5aae0d9..e5cc798 100644 --- a/CursorMobile.xcodeproj/project.pbxproj +++ b/CursorMobile.xcodeproj/project.pbxproj @@ -489,7 +489,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -555,7 +555,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; diff --git a/project.yml b/project.yml index 4701104..d79f3ad 100644 --- a/project.yml +++ b/project.yml @@ -10,7 +10,7 @@ settings: base: SWIFT_VERSION: "6.0" MARKETING_VERSION: "1.0" - CURRENT_PROJECT_VERSION: "8" + CURRENT_PROJECT_VERSION: "9" ENABLE_USER_SCRIPT_SANDBOXING: YES targets: From 8599d7b71b99cdbcd5978c05037ff746c5a51332 Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Mon, 4 May 2026 21:21:43 -0400 Subject: [PATCH 04/12] Release build 10 --- CursorMobile.xcodeproj/project.pbxproj | 4 +- CursorMobile/Features/NativeRows.swift | 240 ++++++++++++++++++++++++- README.md | 4 +- project.yml | 2 +- 4 files changed, 241 insertions(+), 9 deletions(-) diff --git a/CursorMobile.xcodeproj/project.pbxproj b/CursorMobile.xcodeproj/project.pbxproj index e5cc798..b633e86 100644 --- a/CursorMobile.xcodeproj/project.pbxproj +++ b/CursorMobile.xcodeproj/project.pbxproj @@ -489,7 +489,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -555,7 +555,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; diff --git a/CursorMobile/Features/NativeRows.swift b/CursorMobile/Features/NativeRows.swift index 98b2fa3..2761f72 100644 --- a/CursorMobile/Features/NativeRows.swift +++ b/CursorMobile/Features/NativeRows.swift @@ -176,10 +176,12 @@ struct StreamEventListRow: View { .foregroundStyle(.tertiary) } - Text(item.message) - .font(isTechnical ? .caption.monospaced() : .callout) - .foregroundStyle(.secondary) - .textSelection(.enabled) + TimelineMessageText( + message: item.message, + isTechnical: isTechnical, + rendersMarkdown: rendersMarkdown, + foregroundColor: messageColor + ) } } .accessibilityElement(children: .combine) @@ -189,6 +191,24 @@ struct StreamEventListRow: View { item.kind == .toolCall || item.kind == .result || item.kind == .error } + private var rendersMarkdown: Bool { + switch item.kind { + case .assistant, .thinking, .task, .request: + true + default: + false + } + } + + private var messageColor: Color { + switch item.kind { + case .assistant, .user: + .primary + default: + .secondary + } + } + private var symbolName: String { switch item.kind { case .system: @@ -236,6 +256,218 @@ struct StreamEventListRow: View { } } +private struct TimelineMessageText: View { + var message: String + var isTechnical: Bool + var rendersMarkdown: Bool + var foregroundColor: Color + + var body: some View { + Group { + if isTechnical || !rendersMarkdown { + Text(message) + .font(isTechnical ? .caption.monospaced() : .callout) + } else { + VStack(alignment: .leading, spacing: 8) { + ForEach(Array(TimelineMarkdownParser.blocks(from: message).enumerated()), id: \.offset) { _, block in + blockView(block) + } + } + } + } + .foregroundStyle(foregroundColor) + .textSelection(.enabled) + } + + @ViewBuilder + private func blockView(_ block: TimelineMarkdownBlock) -> some View { + switch block { + case .heading(let level, let text): + InlineMarkdownText(text: text, font: headingFont(level)) + .foregroundStyle(.primary) + .padding(.top, level == 1 ? 4 : 2) + case .paragraph(let text): + InlineMarkdownText(text: text, font: .callout) + case .bullet(let text): + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text("•") + .font(.callout) + .foregroundStyle(.tertiary) + InlineMarkdownText(text: text, font: .callout) + } + case .numbered(let marker, let text): + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(marker) + .font(.callout.monospacedDigit()) + .foregroundStyle(.tertiary) + InlineMarkdownText(text: text, font: .callout) + } + case .code(let text): + Text(text) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + .padding(.vertical, 2) + } + } + + private func headingFont(_ level: Int) -> Font { + switch level { + case 1: + .headline + case 2: + .subheadline.weight(.semibold) + default: + .callout.weight(.semibold) + } + } +} + +private struct InlineMarkdownText: View { + var text: String + var font: Font + + var body: some View { + if let attributedText { + Text(attributedText) + .font(font) + } else { + Text(text) + .font(font) + } + } + + private var attributedText: AttributedString? { + try? AttributedString( + markdown: text, + options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace) + ) + } +} + +private enum TimelineMarkdownBlock: Hashable { + case heading(level: Int, text: String) + case paragraph(String) + case bullet(String) + case numbered(marker: String, text: String) + case code(String) +} + +private enum TimelineMarkdownParser { + static func blocks(from message: String) -> [TimelineMarkdownBlock] { + var blocks: [TimelineMarkdownBlock] = [] + var paragraphLines: [String] = [] + var codeLines: [String] = [] + var isInCodeBlock = false + + func flushParagraph() { + let paragraph = paragraphLines + .joined(separator: " ") + .trimmingCharacters(in: .whitespacesAndNewlines) + if !paragraph.isEmpty { + blocks.append(.paragraph(paragraph)) + } + paragraphLines.removeAll() + } + + for line in normalizedLines(from: message) { + let trimmed = line.trimmingCharacters(in: .whitespaces) + + if trimmed.hasPrefix("```") { + if isInCodeBlock { + blocks.append(.code(codeLines.joined(separator: "\n"))) + codeLines.removeAll() + isInCodeBlock = false + } else { + flushParagraph() + isInCodeBlock = true + } + continue + } + + if isInCodeBlock { + codeLines.append(line) + continue + } + + guard !trimmed.isEmpty else { + flushParagraph() + continue + } + + if let heading = heading(from: trimmed) { + flushParagraph() + blocks.append(heading) + continue + } + + if let bullet = bullet(from: trimmed) { + flushParagraph() + blocks.append(.bullet(bullet)) + continue + } + + if let numbered = numberedItem(from: trimmed) { + flushParagraph() + blocks.append(numbered) + continue + } + + paragraphLines.append(trimmed) + } + + if isInCodeBlock, !codeLines.isEmpty { + blocks.append(.code(codeLines.joined(separator: "\n"))) + } + flushParagraph() + + return blocks.isEmpty ? [.paragraph(message)] : blocks + } + + private static func normalizedLines(from message: String) -> [String] { + message + .replacingOccurrences(of: "\r\n", with: "\n") + .replacingOccurrences(of: "\r", with: "\n") + .components(separatedBy: "\n") + } + + private static func heading(from line: String) -> TimelineMarkdownBlock? { + let marker = line.prefix { $0 == "#" } + guard !marker.isEmpty, marker.count <= 6 else { return nil } + + let textStart = line.index(line.startIndex, offsetBy: marker.count) + guard textStart < line.endIndex, line[textStart] == " " else { return nil } + + let text = String(line[line.index(after: textStart)...]) + .trimmingCharacters(in: .whitespaces) + guard !text.isEmpty else { return nil } + return .heading(level: marker.count, text: text) + } + + private static func bullet(from line: String) -> String? { + for marker in ["- ", "* "] where line.hasPrefix(marker) { + let text = String(line.dropFirst(marker.count)) + .trimmingCharacters(in: .whitespaces) + return text.isEmpty ? nil : text + } + return nil + } + + private static func numberedItem(from line: String) -> TimelineMarkdownBlock? { + guard let dotIndex = line.firstIndex(of: ".") else { return nil } + + let number = line[.. Date: Wed, 6 May 2026 18:20:33 -0400 Subject: [PATCH 05/12] Add Cursor SDK setup flow --- CursorMobile.xcodeproj/project.pbxproj | 4 + CursorMobile/App/AppState.swift | 30 +- CursorMobile/App/RootView.swift | 27 +- CursorMobile/Core/DomainModels.swift | 4 +- CursorMobile/Features/ChatDetailView.swift | 2 +- .../Features/CursorSDKOnboardingView.swift | 324 ++++++++++++++++++ CursorMobile/Features/NewChatSheet.swift | 33 +- CursorMobile/Features/SettingsView.swift | 31 +- CursorMobile/Networking/SDKBridgeClient.swift | 6 +- CursorMobileTests/CursorMobileTests.swift | 16 +- README.md | 6 +- orchestrator/README.md | 2 +- 12 files changed, 438 insertions(+), 47 deletions(-) create mode 100644 CursorMobile/Features/CursorSDKOnboardingView.swift diff --git a/CursorMobile.xcodeproj/project.pbxproj b/CursorMobile.xcodeproj/project.pbxproj index b633e86..324925d 100644 --- a/CursorMobile.xcodeproj/project.pbxproj +++ b/CursorMobile.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 31CFAA632F08E7E74221BC7D /* GitProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E7F5C698839855157D73D4F /* GitProvider.swift */; }; 347AD57ABCA4D15CF3C2FCFE /* AgentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEB0AB871CBC28B9BF2D3A0 /* AgentProvider.swift */; }; 36EA754A5E9A03C86836B932 /* AppAppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9E967D0B11FCF0E6A17FAC /* AppAppearanceMode.swift */; }; + 3CC886B6A4F9CC05223F1C84 /* CursorSDKOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15284E1FD2559981B42965C6 /* CursorSDKOnboardingView.swift */; }; 407A501808983E7BD28EFE97 /* ChatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55065E328E23820ECB0F8000 /* ChatsView.swift */; }; 5536A71994566F3564313B92 /* CursorMobileApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BB60A17971111EFF2DDECE /* CursorMobileApp.swift */; }; 56965BD0AAEB4B9A7DE5CC15 /* MockAgentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5ABA55E3CC9F99D9F6E7A3 /* MockAgentProvider.swift */; }; @@ -70,6 +71,7 @@ 0E028E12BF4EB0F89573EF17 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 0E76DFB71C686BF12AA61EF2 /* NativeRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeRows.swift; sourceTree = ""; }; 119E1E9B68CE534FEDE4DBB9 /* SDKBridgeClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKBridgeClientTests.swift; sourceTree = ""; }; + 15284E1FD2559981B42965C6 /* CursorSDKOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CursorSDKOnboardingView.swift; sourceTree = ""; }; 1C3B54D881A589AF2BBA2E3E /* PromptFileLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptFileLoader.swift; sourceTree = ""; }; 1D2351C5637FA72A1A5A2E04 /* CursorMobile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CursorMobile.entitlements; sourceTree = ""; }; 1ED862484103E40E496ADCA7 /* AppTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTab.swift; sourceTree = ""; }; @@ -178,6 +180,7 @@ 8F67359EDFE1E8E08AFC560C /* ChatDetailView.swift */, 55065E328E23820ECB0F8000 /* ChatsView.swift */, F412BDC57CF3ADDCC67E69BE /* ChatTimelineBuilder.swift */, + 15284E1FD2559981B42965C6 /* CursorSDKOnboardingView.swift */, 0E76DFB71C686BF12AA61EF2 /* NativeRows.swift */, CF697C1FF562FFB0BCDAEE8C /* NewChatSheet.swift */, 1C3B54D881A589AF2BBA2E3E /* PromptFileLoader.swift */, @@ -381,6 +384,7 @@ 75B29E76152396217BC0F7C8 /* CursorAgentProvider.swift in Sources */, D42DB9396C9BAA67A304C010 /* CursorDTOs.swift in Sources */, 5536A71994566F3564313B92 /* CursorMobileApp.swift in Sources */, + 3CC886B6A4F9CC05223F1C84 /* CursorSDKOnboardingView.swift in Sources */, 58C224CABDAC81AD077EAD8A /* CursorSSEParser.swift in Sources */, 223994AFA50ECB6343B85BD4 /* DomainModels.swift in Sources */, EFECB3F1EBAC7D11BC5EDCA6 /* EnterpriseDTOs.swift in Sources */, diff --git a/CursorMobile/App/AppState.swift b/CursorMobile/App/AppState.swift index fc8e1ce..443984a 100644 --- a/CursorMobile/App/AppState.swift +++ b/CursorMobile/App/AppState.swift @@ -128,25 +128,25 @@ final class AppState { var sdkBridgeReadinessIssue: String? { guard account != nil else { - return "Connect a Cursor API key before using SDK Agent." + return "Connect a Cursor API key before using Cursor SDK." } guard SDKBridgePreferences.isEnabled() else { - return "Enable the SDK bridge in Settings before using SDK Agent." + return "Enable Runline Bridge in Settings before using Cursor SDK." } guard SDKBridgePreferences.configuredBaseURL() != nil else { - return "Enter a valid SDK bridge URL in Settings." + return "Enter a valid Runline Bridge URL in Settings." } switch sdkBridgeConnectionState { case .connected: return nil case .disabled: - return "Enable the SDK bridge in Settings before using SDK Agent." + return "Enable Runline Bridge in Settings before using Cursor SDK." case .unchecked: - return "Check the SDK bridge connection in Settings before using SDK Agent." + return "Check the Runline Bridge connection in Settings before using Cursor SDK." case .checking: - return "Runline is checking the SDK bridge connection." + return "Runline is checking the bridge connection." case .failed(let message): - return "SDK bridge is unavailable. \(message)" + return "Runline Bridge is unavailable. \(message)" } } @@ -736,8 +736,8 @@ final class AppState { id: "\(result.run.id)-sdk-started", runID: result.run.id, kind: .status, - title: "SDK Agent", - message: "Session started through the Cursor SDK bridge.", + title: "Cursor SDK", + message: "Session started through Runline Bridge.", timestamp: "now" ) ] : [] @@ -853,8 +853,8 @@ final class AppState { id: "\(run.id)-sdk-\(intent.rawValue)", runID: run.id, kind: .status, - title: "SDK Agent", - message: "\(intent.title) message sent through the Cursor SDK bridge.", + title: "Cursor SDK", + message: "\(intent.title) message sent through Runline Bridge.", timestamp: "now" ), ] @@ -1236,7 +1236,7 @@ final class AppState { if let urlError = error as? URLError { switch urlError.code { case .cannotConnectToHost, .cannotFindHost, .networkConnectionLost, .notConnectedToInternet, .timedOut: - return "Runline cannot reach \(baseURL.absoluteString). Start the bridge server or update the URL." + return "Runline cannot reach \(baseURL.absoluteString). Start Runline Bridge or update the URL." default: break } @@ -1399,11 +1399,11 @@ private enum SDKBridgeUnavailableError: LocalizedError { var errorDescription: String? { switch self { case .bridgeDisabled: - "Enable the SDK bridge in Settings before using SDK Agent." + "Enable Runline Bridge in Settings before using Cursor SDK." case .invalidBridgeURL: - "Enter a valid SDK bridge URL in Settings." + "Enter a valid Runline Bridge URL in Settings." case .missingAPIKey: - "Reconnect your Cursor API key before using SDK Agent." + "Reconnect your Cursor API key before using Cursor SDK." } } } diff --git a/CursorMobile/App/RootView.swift b/CursorMobile/App/RootView.swift index 333f54d..370e81e 100644 --- a/CursorMobile/App/RootView.swift +++ b/CursorMobile/App/RootView.swift @@ -85,6 +85,7 @@ private struct WorkflowModeChooserSheet: View { @Environment(AppState.self) private var appState let selectedMode: AgentRunMode let choose: (AgentRunMode) -> Void + @State private var cursorSDKOnboardingSheet: CursorSDKOnboardingSheet? var body: some View { NavigationStack { @@ -101,24 +102,36 @@ private struct WorkflowModeChooserSheet: View { WorkflowModeButton( mode: .sdkBridge, isSelected: effectiveSelectedMode == .sdkBridge, - isEnabled: appState.isSDKBridgeReadyForLaunch, symbolName: "point.3.connected.trianglepath.dotted", detail: sdkAgentDetail, choose: select ) } footer: { - Text("You can change this later in Settings or switch modes in New Chat.") + Text("Cloud Agent is ready now. Cursor SDK needs Runline Bridge on your Mac, and you can set it up later from Settings.") } } - .navigationTitle("Choose Workflow") + .navigationTitle("Choose Runtime") .navigationBarTitleDisplayMode(.inline) } + .sheet(item: $cursorSDKOnboardingSheet) { _ in + CursorSDKOnboardingView( + onUseCloud: { + choose(.cloudAgent) + dismiss() + }, + onOpenSettings: { + choose(.cloudAgent) + appState.selectedTab = .settings + dismiss() + } + ) + } } private var sdkAgentDetail: String { appState.isSDKBridgeReadyForLaunch - ? "Use the SDK bridge for a richer conversation with models, MCP profiles, files, images, planning, and execution." - : "Requires a connected SDK bridge. Choose Cloud Agent now, then enable and check the bridge in Settings." + ? "Use Runline Bridge for Cursor SDK chat, MCP profiles, files, images, planning, and execution." + : "Pair a Mac with Runline Bridge to unlock Cursor SDK sessions, MCP profiles, and git-aware remote work." } private var effectiveSelectedMode: AgentRunMode { @@ -127,7 +140,7 @@ private struct WorkflowModeChooserSheet: View { private func select(_ mode: AgentRunMode) { if mode == .sdkBridge, !appState.isSDKBridgeReadyForLaunch { - appState.errorMessage = appState.sdkBridgeReadinessIssue + cursorSDKOnboardingSheet = .setup return } choose(mode) @@ -399,7 +412,7 @@ private struct SettingsColumnSummary: View { Label("Account", systemImage: "person.crop.circle") Label("Appearance", systemImage: "circle.lefthalf.filled") Label("Notifications", systemImage: "bell") - Label("SDK Agent Bridge", systemImage: "point.3.connected.trianglepath.dotted") + Label("Cursor SDK", systemImage: "point.3.connected.trianglepath.dotted") } Section { diff --git a/CursorMobile/Core/DomainModels.swift b/CursorMobile/Core/DomainModels.swift index e0d25e2..96b66c0 100644 --- a/CursorMobile/Core/DomainModels.swift +++ b/CursorMobile/Core/DomainModels.swift @@ -197,7 +197,7 @@ enum AgentRunMode: String, CaseIterable, Identifiable, Hashable, Codable { case .cloudAgent: "Cloud Agent" case .sdkBridge: - "SDK Agent" + "Cursor SDK" } } @@ -206,7 +206,7 @@ enum AgentRunMode: String, CaseIterable, Identifiable, Hashable, Codable { case .cloudAgent: "Direct Cursor Cloud Agents API from iOS." case .sdkBridge: - "Multi-turn Cursor SDK session with Cloud Agent runtime." + "Cursor SDK session through Runline Bridge." } } } diff --git a/CursorMobile/Features/ChatDetailView.swift b/CursorMobile/Features/ChatDetailView.swift index c25fd55..ba8b678 100644 --- a/CursorMobile/Features/ChatDetailView.swift +++ b/CursorMobile/Features/ChatDetailView.swift @@ -33,7 +33,7 @@ struct ChatDetailView: View { List { Section { - LabeledContent("Mode", value: appState.isSDKBridgeAgent(currentAgent) ? "SDK Agent" : "Cloud Agent") + LabeledContent("Mode", value: appState.isSDKBridgeAgent(currentAgent) ? "Cursor SDK" : "Cloud Agent") LabeledContent("Repository", value: currentAgent.repository.displayName) LabeledContent("Branch", value: currentAgent.branchName) LabeledContent("Model", value: currentAgent.modelID) diff --git a/CursorMobile/Features/CursorSDKOnboardingView.swift b/CursorMobile/Features/CursorSDKOnboardingView.swift new file mode 100644 index 0000000..e1bc47a --- /dev/null +++ b/CursorMobile/Features/CursorSDKOnboardingView.swift @@ -0,0 +1,324 @@ +import SwiftUI +import UIKit + +enum CursorSDKOnboardingSheet: String, Identifiable { + case setup + + var id: String { rawValue } +} + +struct RunlineBridgeBenefit: Identifiable, Equatable { + var id: String { title } + var symbolName: String + var title: String + var detail: String + + static let all: [RunlineBridgeBenefit] = [ + RunlineBridgeBenefit( + symbolName: "macbook.and.iphone", + title: "Mac runtime", + detail: "Your Mac runs the Cursor SDK while iPhone stays the native remote." + ), + RunlineBridgeBenefit( + symbolName: "message.badge.waveform", + title: "Session chat", + detail: "Plan, continue, and execute against the same SDK session." + ), + RunlineBridgeBenefit( + symbolName: "point.3.connected.trianglepath.dotted", + title: "MCP profiles", + detail: "Expose bridge-side tools and subagents without storing them on device." + ), + RunlineBridgeBenefit( + symbolName: "key", + title: "Per-request keys", + detail: "Runline sends your Cursor key only as a bearer token for each request." + ), + ] +} + +enum RunlineBridgeOnboardingStep: String, CaseIterable, Identifiable, Equatable { + case overview + case bridge + case start + case connect + + var id: String { rawValue } + + var eyebrow: String { + switch self { + case .overview: + "Cursor SDK" + case .bridge: + "Step 1" + case .start: + "Step 2" + case .connect: + "Step 3" + } + } + + var title: String { + switch self { + case .overview: + "Use Cursor SDK from your iPhone" + case .bridge: + "Prepare the bridge" + case .start: + "Start the bridge" + case .connect: + "Connect Runline" + } + } + + var subtitle: String { + switch self { + case .overview: + "Cloud Agent remains the default. Cursor SDK is an optional mode for local SDK sessions, MCP profiles, files, images, planning, and execution." + case .bridge: + "The current bridge runs from the Runline orchestrator folder. The packaged Runline Bridge and QR pairing flow are the next runtime layer." + case .start: + "Start the bridge on your Mac. For iPhone testing, use your Mac LAN address instead of localhost." + case .connect: + "Enable Cursor SDK in Settings, enter the bridge URL, then check the connection before selecting Cursor SDK in New Chat." + } + } + + var symbolName: String { + switch self { + case .overview: + "terminal" + case .bridge: + "shippingbox" + case .start: + "play.circle" + case .connect: + "qrcode.viewfinder" + } + } + + var command: String? { + switch self { + case .overview, .connect: + nil + case .bridge: + "cd cursor_mobile/orchestrator && npm install" + case .start: + "CURSOR_API_KEY=your-cursor-key npm run dev" + } + } + + var footnote: String? { + switch self { + case .overview: + nil + case .bridge: + "This is the bridge that is already wired to Runline. It will be replaced by the packaged Runline Bridge installer when pairing is ready." + case .start: + "Simulator can use http://localhost:8787. A physical iPhone needs a reachable Mac LAN URL such as http://192.168.1.10:8787." + case .connect: + "Cloud Agent does not require this setup and remains available even when Cursor SDK is unavailable." + } + } +} + +struct CursorSDKOnboardingView: View { + @Environment(\.dismiss) private var dismiss + var onUseCloud: (() -> Void)? + var onOpenSettings: (() -> Void)? + @State private var selectedStep: RunlineBridgeOnboardingStep = .overview + @State private var copiedStepID: RunlineBridgeOnboardingStep.ID? + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + TabView(selection: $selectedStep) { + ForEach(RunlineBridgeOnboardingStep.allCases) { step in + CursorSDKOnboardingPage( + step: step, + copiedStepID: $copiedStepID + ) + .tag(step) + } + } + .tabViewStyle(.page(indexDisplayMode: .always)) + + VStack(spacing: 10) { + Button(primaryButtonTitle) { + handlePrimaryAction() + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .frame(maxWidth: .infinity) + + Button("Use Cloud Agent for Now") { + onUseCloud?() + dismiss() + } + .buttonStyle(.borderless) + } + .padding(.horizontal) + .padding(.vertical, 14) + .background(.bar) + } + .navigationTitle("Cursor SDK") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + dismiss() + } + } + } + } + } + + private var primaryButtonTitle: String { + selectedStep == .connect ? "Open Bridge Settings" : "Continue" + } + + private func handlePrimaryAction() { + guard selectedStep == .connect else { + advance() + return + } + onOpenSettings?() + dismiss() + } + + private func advance() { + guard let currentIndex = RunlineBridgeOnboardingStep.allCases.firstIndex(of: selectedStep) else { + selectedStep = .connect + return + } + let nextIndex = RunlineBridgeOnboardingStep.allCases.index(after: currentIndex) + if RunlineBridgeOnboardingStep.allCases.indices.contains(nextIndex) { + withAnimation(.snappy(duration: 0.2)) { + selectedStep = RunlineBridgeOnboardingStep.allCases[nextIndex] + } + } else { + selectedStep = .connect + } + } +} + +private struct CursorSDKOnboardingPage: View { + var step: RunlineBridgeOnboardingStep + @Binding var copiedStepID: RunlineBridgeOnboardingStep.ID? + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Image(systemName: step.symbolName) + .font(.system(size: 44, weight: .semibold)) + .foregroundStyle(.tint) + .frame(width: 88, height: 88) + .background(Color(uiColor: .secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 22, style: .continuous)) + + VStack(spacing: 8) { + Text(step.eyebrow.uppercased()) + .font(.caption.weight(.semibold)) + .foregroundStyle(.tint) + + Text(step.title) + .font(.title2.weight(.semibold)) + .multilineTextAlignment(.center) + + Text(step.subtitle) + .font(.body) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + + if step == .overview { + CursorSDKBenefitsList() + } + + if let command = step.command { + CommandCopyRow( + command: command, + isCopied: copiedStepID == step.id + ) { + UIPasteboard.general.string = command + withAnimation(.snappy(duration: 0.2)) { + copiedStepID = step.id + } + } + } + + if let footnote = step.footnote { + Text(footnote) + .font(.footnote) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + } + .frame(maxWidth: 560) + .frame(maxWidth: .infinity) + .padding(.horizontal, 24) + .padding(.top, 52) + .padding(.bottom, 96) + } + .background(Color(uiColor: .systemGroupedBackground)) + } +} + +private struct CursorSDKBenefitsList: View { + var body: some View { + VStack(alignment: .leading, spacing: 14) { + ForEach(RunlineBridgeBenefit.all) { benefit in + HStack(alignment: .top, spacing: 12) { + Image(systemName: benefit.symbolName) + .font(.headline) + .foregroundStyle(.tint) + .frame(width: 28) + + VStack(alignment: .leading, spacing: 3) { + Text(benefit.title) + .font(.headline) + + Text(benefit.detail) + .font(.subheadline) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } + .padding(16) + .background(Color(uiColor: .secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 16, style: .continuous)) + } +} + +private struct CommandCopyRow: View { + var command: String + var isCopied: Bool + var copy: () -> Void + + var body: some View { + Button(action: copy) { + HStack(alignment: .center, spacing: 10) { + Text(command) + .font(.system(.footnote, design: .monospaced)) + .foregroundStyle(.primary) + .multilineTextAlignment(.leading) + .lineLimit(3) + .frame(maxWidth: .infinity, alignment: .leading) + + Image(systemName: isCopied ? "checkmark" : "doc.on.doc") + .font(.headline) + .foregroundStyle(.tint) + } + .padding(14) + .background(Color(uiColor: .tertiarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + } + .buttonStyle(.plain) + .accessibilityLabel(isCopied ? "Command copied" : "Copy command") + } +} + +#Preview { + CursorSDKOnboardingView() +} diff --git a/CursorMobile/Features/NewChatSheet.swift b/CursorMobile/Features/NewChatSheet.swift index 5aa08f5..02d858a 100644 --- a/CursorMobile/Features/NewChatSheet.swift +++ b/CursorMobile/Features/NewChatSheet.swift @@ -52,6 +52,7 @@ struct NewChatForm: View { @State private var isPromptFileImporterPresented = false @State private var isLoadingPromptFiles = false @State private var promptFileImportMessage: String? + @State private var cursorSDKOnboardingSheet: CursorSDKOnboardingSheet? @FocusState private var focusedField: Field? private enum Field { @@ -66,25 +67,31 @@ struct NewChatForm: View { @Bindable var appState = appState Form { - Section("Run Mode") { - Picker("Run Mode", selection: runModeBinding) { + Section("Runtime") { + Picker("Runtime", selection: runModeBinding) { ForEach(availableRunModes) { mode in Text(mode.title).tag(mode) } } .pickerStyle(.segmented) - LabeledContent("Mode", value: appState.launchDraft.runMode.detail) + LabeledContent("Selected", value: appState.launchDraft.runMode.detail) if let issue = appState.sdkBridgeReadinessIssue { Label(issue, systemImage: "point.3.connected.trianglepath.dotted") .font(.footnote) .foregroundStyle(.secondary) + + Button { + cursorSDKOnboardingSheet = .setup + } label: { + Label("Set Up Cursor SDK", systemImage: "point.3.connected.trianglepath.dotted") + } } } if appState.launchDraft.runMode == .sdkBridge { - Section("SDK Tools") { + Section("Cursor SDK Tools") { Picker("MCP Profile", selection: sdkMCPProfileBinding) { Text("None").tag(Optional.none) ForEach(appState.sdkBridgeProfiles) { profile in @@ -100,7 +107,7 @@ struct NewChatForm: View { .foregroundStyle(.secondary) } } else if appState.sdkBridgeProfiles.isEmpty { - Text("No bridge profiles are published. Add MCP/subagent profiles on the SDK bridge to make them available here.") + Text("No bridge profiles are published. Add MCP or subagent profiles on Runline Bridge to make them available here.") .font(.footnote) .foregroundStyle(.secondary) } @@ -278,6 +285,18 @@ struct NewChatForm: View { await loadPromptFiles(from: result) } } + .sheet(item: $cursorSDKOnboardingSheet) { _ in + CursorSDKOnboardingView( + onUseCloud: { + appState.launchDraft.runMode = .cloudAgent + }, + onOpenSettings: { + appState.launchDraft.runMode = .cloudAgent + appState.selectedTab = .settings + dismiss() + } + ) + } } @ViewBuilder @@ -332,7 +351,7 @@ struct NewChatForm: View { appState.launchDraft.runMode } set: { mode in if mode == .sdkBridge, !appState.isSDKBridgeReadyForLaunch { - appState.errorMessage = appState.sdkBridgeReadinessIssue + cursorSDKOnboardingSheet = .setup appState.launchDraft.runMode = .cloudAgent return } @@ -341,7 +360,7 @@ struct NewChatForm: View { } private var availableRunModes: [AgentRunMode] { - appState.isSDKBridgeReadyForLaunch ? AgentRunMode.allCases : [.cloudAgent] + AgentRunMode.allCases } private var modelSelectionBinding: Binding { diff --git a/CursorMobile/Features/SettingsView.swift b/CursorMobile/Features/SettingsView.swift index 60561a1..29baf5c 100644 --- a/CursorMobile/Features/SettingsView.swift +++ b/CursorMobile/Features/SettingsView.swift @@ -18,6 +18,7 @@ struct SettingsFormContent: View { @AppStorage(SDKBridgePreferences.isEnabledKey) private var isSDKBridgeEnabled = SDKBridgePreferences.defaultIsEnabled @AppStorage(SDKBridgePreferences.baseURLKey) private var sdkBridgeBaseURL = SDKBridgePreferences.defaultBaseURLString @State private var enterpriseAPIKey = "" + @State private var cursorSDKOnboardingSheet: CursorSDKOnboardingSheet? @FocusState private var focusedField: Field? private enum Field { @@ -63,13 +64,19 @@ struct SettingsFormContent: View { LabeledContent("Current default", value: RunlineWorkflowPreferences.runMode(from: defaultRunModeRawValue).detail) } header: { - Text("Default Workflow") + Text("Default Runtime") } footer: { - Text("New chats start in this mode. You can still switch between Cloud Agent and SDK Agent per chat.") + Text("Cloud Agent is the default runtime. Cursor SDK can be selected per chat once Runline Bridge is connected.") } Section { - Toggle("Enable SDK bridge", isOn: $isSDKBridgeEnabled) + Button { + cursorSDKOnboardingSheet = .setup + } label: { + Label("Cursor SDK Setup", systemImage: "point.3.connected.trianglepath.dotted") + } + + Toggle("Enable Runline Bridge", isOn: $isSDKBridgeEnabled) TextField("Bridge URL", text: $sdkBridgeBaseURL) .keyboardType(.URL) @@ -116,9 +123,9 @@ struct SettingsFormContent: View { .disabled(appState.sdkBridgeConnectionState == .checking) } } header: { - Text("Cursor SDK Agent Bridge") + Text("Cursor SDK") } footer: { - Text("Cloud Agent stays direct from iOS. SDK Agent is available only after this bridge connects.") + Text("Cloud Agent stays direct from iOS. Cursor SDK is available only after Runline Bridge connects.") } Section("Enterprise API") { @@ -194,6 +201,16 @@ struct SettingsFormContent: View { .onChange(of: appState.sdkBridgeConnectionState) { _, _ in ensureDefaultWorkflowSelectionIsAvailable() } + .sheet(item: $cursorSDKOnboardingSheet) { _ in + CursorSDKOnboardingView( + onUseCloud: { + defaultRunModeRawValue = AgentRunMode.cloudAgent.rawValue + didChooseDefaultRunMode = true + appState.applyDefaultRunMode(.cloudAgent) + }, + onOpenSettings: {} + ) + } } private var defaultRunModeBinding: Binding { @@ -202,7 +219,7 @@ struct SettingsFormContent: View { } set: { rawValue in let mode = RunlineWorkflowPreferences.runMode(from: rawValue) if mode == .sdkBridge, !appState.isSDKBridgeReadyForLaunch { - appState.errorMessage = appState.sdkBridgeReadinessIssue + cursorSDKOnboardingSheet = .setup defaultRunModeRawValue = AgentRunMode.cloudAgent.rawValue didChooseDefaultRunMode = true appState.applyDefaultRunMode(.cloudAgent) @@ -251,7 +268,7 @@ struct SettingsFormContent: View { case .connected(let message), .failed(let message): message case .unchecked: - "Check the bridge before selecting SDK Agent. The bridge must be reachable from this device." + "Check the bridge before selecting Cursor SDK. The bridge must be reachable from this device." case .disabled, .checking: nil } diff --git a/CursorMobile/Networking/SDKBridgeClient.swift b/CursorMobile/Networking/SDKBridgeClient.swift index 9b012be..d5d513b 100644 --- a/CursorMobile/Networking/SDKBridgeClient.swift +++ b/CursorMobile/Networking/SDKBridgeClient.swift @@ -7,9 +7,9 @@ enum SDKBridgeError: LocalizedError, Equatable { var errorDescription: String? { switch self { case .invalidURL: - "The SDK bridge URL could not be created." + "The Runline Bridge URL could not be created." case .requestFailed(let statusCode, let message): - "SDK bridge returned \(statusCode): \(message)" + "Runline Bridge returned \(statusCode): \(message)" } } } @@ -71,7 +71,7 @@ enum SDKBridgePreferences { static func deviceLoopbackHelp(for url: URL?) -> String? { guard isLoopback(url) else { return nil } - return "On a physical iPhone, localhost points to the phone. For device testing, run the bridge on your Mac and use your Mac LAN URL, for example http://192.168.1.10:8787." + return "On a physical iPhone, localhost points to the phone. For device testing, run Runline Bridge on your Mac and use your Mac LAN URL, for example http://192.168.1.10:8787." } } diff --git a/CursorMobileTests/CursorMobileTests.swift b/CursorMobileTests/CursorMobileTests.swift index 69d028a..e014ee8 100644 --- a/CursorMobileTests/CursorMobileTests.swift +++ b/CursorMobileTests/CursorMobileTests.swift @@ -59,6 +59,20 @@ final class CursorMobileTests: XCTestCase { XCTAssertEqual(RunlineWorkflowPreferences.runMode(from: "unknown"), .cloudAgent) } + func testRunModesExposeCloudDefaultAndCursorSDKCopy() { + XCTAssertEqual(AgentRunMode.cloudAgent.title, "Cloud Agent") + XCTAssertEqual(AgentRunMode.sdkBridge.title, "Cursor SDK") + XCTAssertTrue(AgentRunMode.sdkBridge.detail.contains("Runline Bridge")) + } + + func testCursorSDKOnboardingStepsExposeCurrentBridgeCommands() { + XCTAssertEqual(RunlineBridgeOnboardingStep.allCases.first, .overview) + XCTAssertEqual(RunlineBridgeOnboardingStep.allCases.last, .connect) + XCTAssertEqual(RunlineBridgeOnboardingStep.bridge.command, "cd cursor_mobile/orchestrator && npm install") + XCTAssertEqual(RunlineBridgeOnboardingStep.start.command, "CURSOR_API_KEY=your-cursor-key npm run dev") + XCTAssertNil(RunlineBridgeOnboardingStep.connect.command) + } + func testSDKMessageIntentsExposeBridgeValuesAndSymbols() { XCTAssertEqual(SDKMessageIntent.continueConversation.bridgeValue, "continue") XCTAssertEqual(SDKMessageIntent.plan.bridgeValue, "plan") @@ -138,7 +152,7 @@ final class CursorMobileTests: XCTestCase { appState.sdkBridgeConnectionState = .unchecked XCTAssertFalse(appState.canLaunchAgent) - XCTAssertEqual(appState.sdkBridgeLaunchIssue, "Check the SDK bridge connection in Settings before using SDK Agent.") + XCTAssertEqual(appState.sdkBridgeLaunchIssue, "Check the Runline Bridge connection in Settings before using Cursor SDK.") appState.sdkBridgeConnectionState = .connected("runline-orchestrator - @cursor/sdk") diff --git a/README.md b/README.md index 7e8b8bf..fef51f9 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ Runline is independent and is not affiliated with, endorsed by, or connected to - iOS 26+ SwiftUI app target - Chat-first Cloud Agents navigation - Cursor Cloud Agents v1 provider for account, repositories, models, agents, runs, streams, artifacts, archive, unarchive, and delete -- Optional `@cursor/sdk` bridge client for SDK Agent sessions, MCP profiles, subagents, and multi-turn follow-ups -- First-run and Settings workflow selection between Cloud Agent and SDK Agent defaults -- Native SDK Agent composer controls for intent, model, MCP profile, image context, and file context +- Optional `@cursor/sdk` bridge client for Cursor SDK sessions, MCP profiles, subagents, and multi-turn follow-ups +- First-run and Settings runtime selection between Cloud Agent and Cursor SDK defaults +- Native Cursor SDK composer controls for intent, model, MCP profile, image context, and file context - Keychain-backed Cursor API key storage - Local cache for account, repositories, models, agents, runs, stream events, artifacts, notification preferences, and launch draft - Unit tests for Cursor v1 request contracts, SSE parsing, cache persistence, app routing, push payloads, chat event cleanup, file attachment loading, and SDK bridge request mapping diff --git a/orchestrator/README.md b/orchestrator/README.md index c772a26..bc06af0 100644 --- a/orchestrator/README.md +++ b/orchestrator/README.md @@ -4,7 +4,7 @@ This is an optional TypeScript backend for Cursor SDK-only workflows. The iOS ap Use this service only for work that benefits from `@cursor/sdk`: -- Resumable SDK Agent sessions with multi-turn follow-up messages. +- Resumable Cursor SDK sessions with multi-turn follow-up messages. - SDK-normalized event streams and conversation state. - Launch payloads that include MCP server profiles or subagents. - Service-account workflows for teams. From adea88fc205f0adbd97ba873398ae28b456df0ef Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Wed, 6 May 2026 18:26:27 -0400 Subject: [PATCH 06/12] Update TestFlight notes for Cursor SDK --- .asc/workflow.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.asc/workflow.json b/.asc/workflow.json index 3874bbc..ad57e72 100644 --- a/.asc/workflow.json +++ b/.asc/workflow.json @@ -10,7 +10,7 @@ "TESTFLIGHT_GROUP_ID": "9ddf92fa-5b17-48ca-9833-5d83c3e31d3d", "INTERNAL_TESTFLIGHT_GROUP_ID": "a252108c-c4bc-4b6b-94cd-519b7be04c68", "LOCALE": "en-US", - "TEST_NOTES": "Verify Cloud Agent and SDK Agent workflows, iPhone and iPad layouts, chat composer, attachments, model and MCP controls, Settings, light mode, and dark mode." + "TEST_NOTES": "Verify Cloud Agent and Cursor SDK workflows, iPhone and iPad layouts, chat composer, attachments, model and MCP controls, Settings, light mode, and dark mode." }, "before_all": "asc --profile $ASC_PROFILE auth status --validate --output json >/dev/null", "after_all": "echo workflow_done >&2", From 4cadd0fbf1701b2ba8e151d93b2ee4e932d67832 Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Wed, 6 May 2026 18:31:22 -0400 Subject: [PATCH 07/12] Release build 11 --- CursorMobile.xcodeproj/project.pbxproj | 4 ++-- README.md | 4 ++-- project.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CursorMobile.xcodeproj/project.pbxproj b/CursorMobile.xcodeproj/project.pbxproj index 324925d..2574443 100644 --- a/CursorMobile.xcodeproj/project.pbxproj +++ b/CursorMobile.xcodeproj/project.pbxproj @@ -493,7 +493,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -559,7 +559,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; diff --git a/README.md b/README.md index fef51f9..f1bba2a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ asc workflow run preflight Upload the next TestFlight build with an explicit build number: ```bash -asc workflow run testflight BUILD_NUMBER:10 +asc workflow run testflight BUILD_NUMBER:11 ``` -Use explicit build numbers so release numbering stays aligned with the active Runline sequence. The current release target is `1.0 (10)`. +Use explicit build numbers so release numbering stays aligned with the active Runline sequence. The current release target is `1.0 (11)`. diff --git a/project.yml b/project.yml index eff7816..255c401 100644 --- a/project.yml +++ b/project.yml @@ -10,7 +10,7 @@ settings: base: SWIFT_VERSION: "6.0" MARKETING_VERSION: "1.0" - CURRENT_PROJECT_VERSION: "10" + CURRENT_PROJECT_VERSION: "11" ENABLE_USER_SCRIPT_SANDBOXING: YES targets: From 194f54ace3a4309500ac1e1a7931258f8cfa8de0 Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Wed, 6 May 2026 19:11:25 -0400 Subject: [PATCH 08/12] Add Runline Bridge pairing --- CursorMobile/App/AppState.swift | 133 ++++++++++++- .../Features/CursorSDKOnboardingView.swift | 8 +- CursorMobile/Features/SettingsView.swift | 91 +++++++++ CursorMobile/Networking/SDKBridgeClient.swift | 62 +++++- CursorMobile/Security/APIKeyStore.swift | 1 + CursorMobileTests/CursorMobileTests.swift | 23 ++- CursorMobileTests/SDKBridgeClientTests.swift | 67 ++++++- README.md | 1 + orchestrator/README.md | 14 ++ orchestrator/package.json | 1 + orchestrator/src/server.ts | 181 +++++++++++++++++- 11 files changed, 560 insertions(+), 22 deletions(-) diff --git a/CursorMobile/App/AppState.swift b/CursorMobile/App/AppState.swift index 443984a..df28012 100644 --- a/CursorMobile/App/AppState.swift +++ b/CursorMobile/App/AppState.swift @@ -10,6 +10,7 @@ final class AppState { private var enterpriseProvider: EnterpriseDataProvider? private let apiKeyStore: APIKeyStore private let enterpriseAPIKeyStore: APIKeyStore + private let sdkBridgeTokenStore: APIKeyStore private let appCache: LocalAppCache private let providerFactory: @MainActor (String) throws -> AgentProvider private var hasRestoredConnection = false @@ -17,6 +18,7 @@ final class AppState { private var streamExpiredRunIDs: Set = [] private var sdkBridgeRunIDs: Set = [] private var sdkBridgeMCPProfileIDsByAgentID: [Agent.ID: SDKBridgeMCPProfile.ID] = [:] + private var sdkBridgeToken: String? private var artifactDownloadsByKey: [String: ArtifactDownload] = [:] var selectedTab: AppTab = .chats @@ -33,6 +35,7 @@ final class AppState { var endpointPageOverrides: [CursorAPIEndpoint.ID: Int] = [:] var sdkBridgeProfiles: [SDKBridgeMCPProfile] = [] var sdkBridgeConnectionState: SDKBridgeConnectionState = SDKBridgePreferences.isEnabled() ? .unchecked : .disabled + var sdkBridgePairingState: SDKBridgePairingState = .idle var focusedAgentID: Agent.ID? var notificationPreferences = NotificationPreferences() var notificationAuthorizationStatus: UNAuthorizationStatus = .notDetermined @@ -45,25 +48,29 @@ final class AppState { var statusMessage: String? var launchDraft: AgentLaunchDraft - private let sdkBridgeClientFactory: @MainActor (URL, String?) -> SDKBridgeClient + private let sdkBridgeClientFactory: @MainActor (URL, String?, String?) -> SDKBridgeClient init( provider: AgentProvider? = nil, apiKeyStore: APIKeyStore = KeychainAPIKeyStore(), enterpriseAPIKeyStore: APIKeyStore = KeychainAPIKeyStore(account: .cursorEnterpriseAdmin), + sdkBridgeTokenStore: APIKeyStore = KeychainAPIKeyStore(account: .runlineBridgeToken), appCache: LocalAppCache = LocalAppCache(), providerFactory: @escaping @MainActor (String) throws -> AgentProvider = { try CursorAgentProvider(apiKey: $0) }, - sdkBridgeClientFactory: @escaping @MainActor (URL, String?) -> SDKBridgeClient = { baseURL, apiKey in - SDKBridgeClient(baseURL: baseURL, apiKey: apiKey) + sdkBridgeClientFactory: @escaping @MainActor (URL, String?, String?) -> SDKBridgeClient = { baseURL, apiKey, bridgeToken in + SDKBridgeClient(baseURL: baseURL, apiKey: apiKey, bridgeToken: bridgeToken) } ) { self.provider = provider enterpriseProvider = provider as? EnterpriseDataProvider self.apiKeyStore = apiKeyStore self.enterpriseAPIKeyStore = enterpriseAPIKeyStore + self.sdkBridgeTokenStore = sdkBridgeTokenStore self.appCache = appCache self.providerFactory = providerFactory self.sdkBridgeClientFactory = sdkBridgeClientFactory + let storedBridgeToken = try? sdkBridgeTokenStore.loadAPIKey()?.nilIfBlank + sdkBridgeToken = storedBridgeToken launchDraft = AgentLaunchDraft( prompt: AgentPrompt(text: ""), modelID: nil, @@ -73,6 +80,9 @@ final class AppState { autoCreatePullRequest: true, skipReviewerRequest: false ) + if storedBridgeToken != nil { + sdkBridgePairingState = .paired("Runline Bridge") + } } var capabilities: ProviderCapabilities { @@ -126,6 +136,10 @@ final class AppState { sdkBridgeReadinessIssue == nil } + var isSDKBridgePaired: Bool { + sdkBridgeToken?.nilIfBlank != nil + } + var sdkBridgeReadinessIssue: String? { guard account != nil else { return "Connect a Cursor API key before using Cursor SDK." @@ -136,6 +150,9 @@ final class AppState { guard SDKBridgePreferences.configuredBaseURL() != nil else { return "Enter a valid Runline Bridge URL in Settings." } + guard isSDKBridgePaired else { + return "Pair Runline Bridge in Settings before using Cursor SDK." + } switch sdkBridgeConnectionState { case .connected: return nil @@ -213,6 +230,7 @@ final class AppState { do { try apiKeyStore.deleteAPIKey() try enterpriseAPIKeyStore.deleteAPIKey() + try sdkBridgeTokenStore.deleteAPIKey() } catch { errorMessage = "Could not remove the local Cursor API key." } @@ -229,7 +247,9 @@ final class AppState { artifactsByAgentID = [:] sdkBridgeRunIDs = [] sdkBridgeMCPProfileIDsByAgentID = [:] + sdkBridgeToken = nil sdkBridgeProfiles = [] + sdkBridgePairingState = .idle sdkBridgeConnectionState = SDKBridgePreferences.isEnabled() ? .unchecked : .disabled endpointResults = [:] loadingEndpointIDs = [] @@ -412,6 +432,87 @@ final class AppState { } } + func startSDKBridgePairing() async { + guard SDKBridgePreferences.isEnabled() else { + sdkBridgePairingState = .failed("Enable Runline Bridge before pairing.") + return + } + guard let baseURL = SDKBridgePreferences.configuredBaseURL() else { + sdkBridgePairingState = .failed("Enter a valid Runline Bridge URL before pairing.") + return + } + + sdkBridgePairingState = .starting + do { + let response = try await sdkBridgeClientFactory(baseURL, nil, nil).startPairing(deviceName: UIDevice.current.name) + if response.pairingRequired == false { + sdkBridgePairingState = .paired("Runline Bridge") + sdkBridgeConnectionState = .unchecked + return + } + guard let pairingID = response.pairingId?.nilIfBlank else { + sdkBridgePairingState = .failed("Runline Bridge did not return a pairing session.") + return + } + sdkBridgePairingState = .waiting( + pairingID: pairingID, + expiresAt: response.expiresAt, + message: response.message + ) + } catch { + sdkBridgePairingState = .failed(sdkBridgeConnectionFailureMessage(for: error, baseURL: baseURL)) + } + } + + func completeSDKBridgePairing(code: String) async { + guard case .waiting(let pairingID, _, _) = sdkBridgePairingState else { + sdkBridgePairingState = .failed("Start pairing before entering a code.") + return + } + guard let baseURL = SDKBridgePreferences.configuredBaseURL() else { + sdkBridgePairingState = .failed("Enter a valid Runline Bridge URL before pairing.") + return + } + let pairingCode = code.trimmingCharacters(in: .whitespacesAndNewlines) + guard !pairingCode.isEmpty else { + sdkBridgePairingState = .failed("Enter the pairing code shown in the bridge terminal.") + return + } + + sdkBridgePairingState = .completing + do { + let response = try await sdkBridgeClientFactory(baseURL, nil, nil).completePairing( + pairingID: pairingID, + code: pairingCode, + deviceName: UIDevice.current.name + ) + guard let token = response.bridgeToken?.nilIfBlank else { + sdkBridgePairingState = .failed("Runline Bridge did not return a bridge token.") + return + } + try sdkBridgeTokenStore.saveAPIKey(token) + sdkBridgeToken = token + sdkBridgePairingState = .paired(response.bridgeName?.nilIfBlank ?? response.service?.nilIfBlank ?? "Runline Bridge") + sdkBridgeConnectionState = .unchecked + await checkSDKBridgeConnection() + } catch { + sdkBridgePairingState = .failed(sdkBridgeConnectionFailureMessage(for: error, baseURL: baseURL)) + } + } + + func forgetSDKBridgePairing() { + do { + try sdkBridgeTokenStore.deleteAPIKey() + } catch { + errorMessage = "Could not remove the Runline Bridge pairing token." + } + sdkBridgeToken = nil + sdkBridgeProfiles = [] + sdkBridgePairingState = .idle + sdkBridgeConnectionState = SDKBridgePreferences.isEnabled() ? .unchecked : .disabled + ensureLaunchRunModeIsAvailable() + } + func checkSDKBridgeConnection() async { guard SDKBridgePreferences.isEnabled() else { sdkBridgeConnectionState = .disabled @@ -425,15 +526,21 @@ final class AppState { ensureLaunchRunModeIsAvailable() return } + guard isSDKBridgePaired else { + sdkBridgeConnectionState = .failed("Pair Runline Bridge before checking Cursor SDK.") + sdkBridgeProfiles = [] + ensureLaunchRunModeIsAvailable() + return + } sdkBridgeConnectionState = .checking do { - let health = try await sdkBridgeClientFactory(baseURL, nil).health() - if health.ok { + let health = try await sdkBridgeClientFactory(baseURL, nil, sdkBridgeToken).health() + if health.ok, health.paired != false { sdkBridgeConnectionState = .connected("\(health.service) - \(health.sdk)") await reloadSDKBridgeProfiles() } else { - sdkBridgeConnectionState = .failed("The bridge responded but reported an unhealthy status.") + sdkBridgeConnectionState = .failed("The bridge responded but did not accept this pairing token.") sdkBridgeProfiles = [] ensureLaunchRunModeIsAvailable() } @@ -450,9 +557,13 @@ final class AppState { sdkBridgeProfiles = [] return } + guard isSDKBridgePaired else { + sdkBridgeProfiles = [] + return + } do { let apiKey = try apiKeyStore.loadAPIKey()?.nilIfBlank - sdkBridgeProfiles = try await sdkBridgeClientFactory(baseURL, apiKey).listMCPProfiles() + sdkBridgeProfiles = try await sdkBridgeClientFactory(baseURL, apiKey, sdkBridgeToken).listMCPProfiles() if let profileID = launchDraft.sdkMCPProfileID, !sdkBridgeProfiles.contains(where: { $0.id == profileID }) { launchDraft.sdkMCPProfileID = nil @@ -1085,10 +1196,13 @@ final class AppState { guard let baseURL = SDKBridgePreferences.configuredBaseURL() else { throw SDKBridgeUnavailableError.invalidBridgeURL } + guard let bridgeToken = sdkBridgeToken?.nilIfBlank else { + throw SDKBridgeUnavailableError.bridgeNotPaired + } guard let apiKey = try apiKeyStore.loadAPIKey()?.nilIfBlank else { throw SDKBridgeUnavailableError.missingAPIKey } - return sdkBridgeClientFactory(baseURL, apiKey) + return sdkBridgeClientFactory(baseURL, apiKey, bridgeToken) } private func updateAgent(_ agentID: Agent.ID, mutate: (inout Agent) -> Void) { @@ -1394,6 +1508,7 @@ final class AppState { private enum SDKBridgeUnavailableError: LocalizedError { case bridgeDisabled case invalidBridgeURL + case bridgeNotPaired case missingAPIKey var errorDescription: String? { @@ -1402,6 +1517,8 @@ private enum SDKBridgeUnavailableError: LocalizedError { "Enable Runline Bridge in Settings before using Cursor SDK." case .invalidBridgeURL: "Enter a valid Runline Bridge URL in Settings." + case .bridgeNotPaired: + "Pair Runline Bridge in Settings before using Cursor SDK." case .missingAPIKey: "Reconnect your Cursor API key before using Cursor SDK." } diff --git a/CursorMobile/Features/CursorSDKOnboardingView.swift b/CursorMobile/Features/CursorSDKOnboardingView.swift index e1bc47a..abc6cbf 100644 --- a/CursorMobile/Features/CursorSDKOnboardingView.swift +++ b/CursorMobile/Features/CursorSDKOnboardingView.swift @@ -76,11 +76,11 @@ enum RunlineBridgeOnboardingStep: String, CaseIterable, Identifiable, Equatable case .overview: "Cloud Agent remains the default. Cursor SDK is an optional mode for local SDK sessions, MCP profiles, files, images, planning, and execution." case .bridge: - "The current bridge runs from the Runline orchestrator folder. The packaged Runline Bridge and QR pairing flow are the next runtime layer." + "Runline Bridge runs from the orchestrator folder and keeps Cursor SDK execution on your Mac." case .start: "Start the bridge on your Mac. For iPhone testing, use your Mac LAN address instead of localhost." case .connect: - "Enable Cursor SDK in Settings, enter the bridge URL, then check the connection before selecting Cursor SDK in New Chat." + "Enable Cursor SDK in Settings, enter the bridge URL, start pairing, then type the code printed in your Mac terminal." } } @@ -104,7 +104,7 @@ enum RunlineBridgeOnboardingStep: String, CaseIterable, Identifiable, Equatable case .bridge: "cd cursor_mobile/orchestrator && npm install" case .start: - "CURSOR_API_KEY=your-cursor-key npm run dev" + "CURSOR_API_KEY=your-cursor-key npm run bridge" } } @@ -113,7 +113,7 @@ enum RunlineBridgeOnboardingStep: String, CaseIterable, Identifiable, Equatable case .overview: nil case .bridge: - "This is the bridge that is already wired to Runline. It will be replaced by the packaged Runline Bridge installer when pairing is ready." + "Runline Bridge now uses a one-time pairing code and stores the bridge token in the iOS Keychain." case .start: "Simulator can use http://localhost:8787. A physical iPhone needs a reachable Mac LAN URL such as http://192.168.1.10:8787." case .connect: diff --git a/CursorMobile/Features/SettingsView.swift b/CursorMobile/Features/SettingsView.swift index 29baf5c..70014fb 100644 --- a/CursorMobile/Features/SettingsView.swift +++ b/CursorMobile/Features/SettingsView.swift @@ -19,10 +19,12 @@ struct SettingsFormContent: View { @AppStorage(SDKBridgePreferences.baseURLKey) private var sdkBridgeBaseURL = SDKBridgePreferences.defaultBaseURLString @State private var enterpriseAPIKey = "" @State private var cursorSDKOnboardingSheet: CursorSDKOnboardingSheet? + @State private var pairingCode = "" @FocusState private var focusedField: Field? private enum Field { case bridgeURL + case pairingCode case enterpriseKey } @@ -96,6 +98,7 @@ struct SettingsFormContent: View { } LabeledContent("Profiles", value: "\(appState.sdkBridgeProfiles.count)") + LabeledContent("Pairing", value: appState.isSDKBridgePaired ? "Paired" : "Not Paired") if let detail = sdkBridgeConnectionDetail { Text(detail) @@ -121,6 +124,51 @@ struct SettingsFormContent: View { } } .disabled(appState.sdkBridgeConnectionState == .checking) + + if appState.isSDKBridgePaired { + Button("Forget Pairing", role: .destructive) { + appState.forgetSDKBridgePairing() + } + } else { + Button { + Task { + await startBridgePairing() + } + } label: { + if appState.sdkBridgePairingState == .starting { + ProgressView() + } else { + Label("Start Pairing", systemImage: "link.badge.plus") + } + } + .disabled(appState.sdkBridgePairingState == .starting || appState.sdkBridgePairingState == .completing) + + if case .waiting = appState.sdkBridgePairingState { + TextField("Pairing Code", text: $pairingCode) + .keyboardType(.numberPad) + .textContentType(.oneTimeCode) + .focused($focusedField, equals: .pairingCode) + + Button { + Task { + await completeBridgePairing() + } + } label: { + if appState.sdkBridgePairingState == .completing { + ProgressView() + } else { + Text("Complete Pairing") + } + } + .disabled(pairingCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || appState.sdkBridgePairingState == .completing) + } + + if let pairingDetail { + Text(pairingDetail) + .font(.footnote) + .foregroundStyle(pairingDetailIsError ? .red : .secondary) + } + } } } header: { Text("Cursor SDK") @@ -198,6 +246,12 @@ struct SettingsFormContent: View { appState.syncSDKBridgeConfiguration(resetConnection: true) ensureDefaultWorkflowSelectionIsAvailable() } + .onChange(of: appState.sdkBridgePairingState) { _, state in + if case .paired = state { + pairingCode = "" + focusedField = nil + } + } .onChange(of: appState.sdkBridgeConnectionState) { _, _ in ensureDefaultWorkflowSelectionIsAvailable() } @@ -274,6 +328,32 @@ struct SettingsFormContent: View { } } + private var pairingDetail: String? { + switch appState.sdkBridgePairingState { + case .idle: + "Start pairing, then enter the six-digit code printed in the Runline Bridge terminal." + case .starting: + "Starting a pairing session..." + case .waiting(_, let expiresAt, let message): + [message, expiresAt.map { "Expires at \($0)." }] + .compactMap { $0 } + .joined(separator: " ") + case .completing: + "Completing pairing..." + case .paired(let name): + "Paired with \(name)." + case .failed(let message): + message + } + } + + private var pairingDetailIsError: Bool { + if case .failed = appState.sdkBridgePairingState { + return true + } + return false + } + private func ensureDefaultWorkflowSelectionIsAvailable() { guard RunlineWorkflowPreferences.runMode(from: defaultRunModeRawValue) == .sdkBridge, !appState.isSDKBridgeReadyForLaunch else { @@ -297,6 +377,17 @@ struct SettingsFormContent: View { await appState.checkSDKBridgeConnection() } + private func startBridgePairing() async { + focusedField = nil + pairingCode = "" + await appState.startSDKBridgePairing() + } + + private func completeBridgePairing() async { + focusedField = nil + await appState.completeSDKBridgePairing(code: pairingCode) + } + private func notificationBinding(_ keyPath: WritableKeyPath) -> Binding { Binding { appState.notificationPreferences[keyPath: keyPath] diff --git a/CursorMobile/Networking/SDKBridgeClient.swift b/CursorMobile/Networking/SDKBridgeClient.swift index d5d513b..ad029df 100644 --- a/CursorMobile/Networking/SDKBridgeClient.swift +++ b/CursorMobile/Networking/SDKBridgeClient.swift @@ -18,6 +18,8 @@ struct SDKBridgeHealthResponse: Decodable, Equatable { var ok: Bool var service: String var sdk: String + var pairingRequired: Bool? + var paired: Bool? } enum SDKBridgeConnectionState: Equatable { @@ -35,6 +37,15 @@ enum SDKBridgeConnectionState: Equatable { } } +enum SDKBridgePairingState: Equatable { + case idle + case starting + case waiting(pairingID: String, expiresAt: String?, message: String?) + case completing + case paired(String) + case failed(String) +} + enum SDKBridgePreferences { static let isEnabledKey = "sdkBridge.isEnabled" static let baseURLKey = "sdkBridge.baseURL" @@ -111,6 +122,30 @@ struct SDKBridgeMCPProfilesResponse: Decodable, Equatable { var profiles: [SDKBridgeMCPProfile] } +struct SDKBridgePairingStartRequest: Encodable, Equatable { + var deviceName: String +} + +struct SDKBridgePairingStartResponse: Decodable, Equatable { + var pairingId: String? + var expiresAt: String? + var message: String? + var pairingRequired: Bool? +} + +struct SDKBridgePairingCompleteRequest: Encodable, Equatable { + var pairingId: String + var code: String + var deviceName: String +} + +struct SDKBridgePairingCompleteResponse: Decodable, Equatable { + var bridgeToken: String? + var bridgeName: String? + var service: String? + var sdk: String? +} + struct SDKBridgeRunStartResponse: Decodable, Equatable { var sessionId: String? var agentId: String @@ -146,13 +181,15 @@ final class SDKBridgeClient: @unchecked Sendable { private let baseURL: URL private let apiKey: String? + private let bridgeToken: String? private let session: URLSession private let encoder = JSONEncoder() private let decoder = JSONDecoder() - init(baseURL: URL, apiKey: String? = nil, session: URLSession = .shared) { + init(baseURL: URL, apiKey: String? = nil, bridgeToken: String? = nil, session: URLSession = .shared) { self.baseURL = baseURL self.apiKey = apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfBlank + self.bridgeToken = bridgeToken?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfBlank self.session = session } @@ -168,6 +205,26 @@ final class SDKBridgeClient: @unchecked Sendable { try await request("/sdk/sessions", method: .post, body: body) } + func startPairing(deviceName: String) async throws -> SDKBridgePairingStartResponse { + try await request( + "/pair/start", + method: .post, + body: SDKBridgePairingStartRequest(deviceName: deviceName) + ) + } + + func completePairing(pairingID: String, code: String, deviceName: String) async throws -> SDKBridgePairingCompleteResponse { + try await request( + "/pair/complete", + method: .post, + body: SDKBridgePairingCompleteRequest( + pairingId: pairingID, + code: code, + deviceName: deviceName + ) + ) + } + func sendSessionMessage( sessionID: String, body: SDKBridgeSessionMessageRequest @@ -307,6 +364,9 @@ final class SDKBridgeClient: @unchecked Sendable { if let apiKey { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") } + if let bridgeToken { + request.setValue(bridgeToken, forHTTPHeaderField: "X-Runline-Bridge-Token") + } if let body { request.httpBody = try encoder.encode(AnyEncodable(body)) request.setValue("application/json", forHTTPHeaderField: "Content-Type") diff --git a/CursorMobile/Security/APIKeyStore.swift b/CursorMobile/Security/APIKeyStore.swift index a474543..510cef0 100644 --- a/CursorMobile/Security/APIKeyStore.swift +++ b/CursorMobile/Security/APIKeyStore.swift @@ -10,6 +10,7 @@ protocol APIKeyStore { enum APIKeyStoreAccount: String { case cursorCloudAgent = "cursor-cloud-agent" case cursorEnterpriseAdmin = "cursor-enterprise-admin" + case runlineBridgeToken = "runline-bridge-token" } enum APIKeyStoreError: LocalizedError { diff --git a/CursorMobileTests/CursorMobileTests.swift b/CursorMobileTests/CursorMobileTests.swift index e014ee8..934f48b 100644 --- a/CursorMobileTests/CursorMobileTests.swift +++ b/CursorMobileTests/CursorMobileTests.swift @@ -69,7 +69,7 @@ final class CursorMobileTests: XCTestCase { XCTAssertEqual(RunlineBridgeOnboardingStep.allCases.first, .overview) XCTAssertEqual(RunlineBridgeOnboardingStep.allCases.last, .connect) XCTAssertEqual(RunlineBridgeOnboardingStep.bridge.command, "cd cursor_mobile/orchestrator && npm install") - XCTAssertEqual(RunlineBridgeOnboardingStep.start.command, "CURSOR_API_KEY=your-cursor-key npm run dev") + XCTAssertEqual(RunlineBridgeOnboardingStep.start.command, "CURSOR_API_KEY=your-cursor-key npm run bridge") XCTAssertNil(RunlineBridgeOnboardingStep.connect.command) } @@ -139,9 +139,26 @@ final class CursorMobileTests: XCTestCase { } @MainActor - func testSDKLaunchRequiresConnectedBridge() { + func testSDKLaunchRequiresPairingAndConnectedBridge() { withSDKBridgeDefaults(enabled: true, baseURL: "http://localhost:8787") { - let appState = AppState(provider: MockAgentProvider(), apiKeyStore: InMemoryAPIKeyStore(apiKey: "cursor-test-key")) + let unpairedAppState = AppState(provider: MockAgentProvider(), apiKeyStore: InMemoryAPIKeyStore(apiKey: "cursor-test-key")) + unpairedAppState.account = ProviderAccount( + apiKeyName: "Runline Test Key", + userEmail: "test@example.com", + createdAt: .now + ) + unpairedAppState.launchDraft.prompt.text = "Plan the settings cleanup" + unpairedAppState.launchDraft.runMode = .sdkBridge + unpairedAppState.sdkBridgeConnectionState = .connected("runline-bridge - @cursor/sdk") + + XCTAssertFalse(unpairedAppState.canLaunchAgent) + XCTAssertEqual(unpairedAppState.sdkBridgeLaunchIssue, "Pair Runline Bridge in Settings before using Cursor SDK.") + + let appState = AppState( + provider: MockAgentProvider(), + apiKeyStore: InMemoryAPIKeyStore(apiKey: "cursor-test-key"), + sdkBridgeTokenStore: InMemoryAPIKeyStore(apiKey: "bridge-token") + ) appState.account = ProviderAccount( apiKeyName: "Runline Test Key", userEmail: "test@example.com", diff --git a/CursorMobileTests/SDKBridgeClientTests.swift b/CursorMobileTests/SDKBridgeClientTests.swift index 304a70c..fb71584 100644 --- a/CursorMobileTests/SDKBridgeClientTests.swift +++ b/CursorMobileTests/SDKBridgeClientTests.swift @@ -17,8 +17,10 @@ final class SDKBridgeClientTests: XCTestCase { MockBridgeURLProtocol.handler = { request in try Self.jsonResponse(for: request, body: [ "ok": true, - "service": "runline-orchestrator", - "sdk": "@cursor/sdk" + "service": "runline-bridge", + "sdk": "@cursor/sdk", + "pairingRequired": true, + "paired": true ]) } @@ -27,6 +29,8 @@ final class SDKBridgeClientTests: XCTestCase { XCTAssertTrue(health.ok) XCTAssertEqual(health.sdk, "@cursor/sdk") + XCTAssertEqual(health.pairingRequired, true) + XCTAssertEqual(health.paired, true) let request = try XCTUnwrap(MockBridgeURLProtocol.capturedRequests.first) XCTAssertEqual(request.method, "GET") XCTAssertEqual(request.url?.path, "/health") @@ -87,6 +91,62 @@ final class SDKBridgeClientTests: XCTestCase { XCTAssertEqual(body["skipReviewerRequest"] as? Bool, false) } + func testBridgeTokenHeaderIsSentOnProtectedRequests() async throws { + MockBridgeURLProtocol.handler = { request in + try Self.jsonResponse(for: request, body: [ + "profiles": [] + ]) + } + + let client = makeClient(apiKey: "cursor-test-key", bridgeToken: "bridge-token") + _ = try await client.listMCPProfiles() + + let request = try XCTUnwrap(MockBridgeURLProtocol.capturedRequests.first) + XCTAssertEqual(request.header("Authorization"), "Bearer cursor-test-key") + XCTAssertEqual(request.header("X-Runline-Bridge-Token"), "bridge-token") + } + + func testPairingStartAndCompleteUsePairingEndpoints() async throws { + MockBridgeURLProtocol.handler = { request in + switch request.url?.path { + case "/pair/start": + return try Self.jsonResponse(for: request, body: [ + "pairingId": "pair-123", + "expiresAt": "2026-05-06T20:00:00Z", + "message": "Check the terminal running Runline Bridge for the pairing code." + ]) + case "/pair/complete": + return try Self.jsonResponse(for: request, body: [ + "bridgeToken": "bridge-token", + "bridgeName": "Runline Bridge", + "service": "runline-bridge", + "sdk": "@cursor/sdk" + ]) + default: + throw URLError(.badURL) + } + } + + let client = makeClient() + let start = try await client.startPairing(deviceName: "Matthew's iPhone") + let complete = try await client.completePairing( + pairingID: "pair-123", + code: "123456", + deviceName: "Matthew's iPhone" + ) + + XCTAssertEqual(start.pairingId, "pair-123") + XCTAssertEqual(complete.bridgeToken, "bridge-token") + XCTAssertEqual(MockBridgeURLProtocol.capturedRequests.map { $0.url?.path }, ["/pair/start", "/pair/complete"]) + + let startBody = try XCTUnwrap(MockBridgeURLProtocol.capturedRequests.first?.jsonBody) + XCTAssertEqual(startBody["deviceName"] as? String, "Matthew's iPhone") + + let completeBody = try XCTUnwrap(MockBridgeURLProtocol.capturedRequests.last?.jsonBody) + XCTAssertEqual(completeBody["pairingId"] as? String, "pair-123") + XCTAssertEqual(completeBody["code"] as? String, "123456") + } + func testCreateSessionUsesSDKSessionEndpointAndProfile() async throws { MockBridgeURLProtocol.handler = { request in try Self.jsonResponse(for: request, body: [ @@ -278,10 +338,11 @@ final class SDKBridgeClientTests: XCTestCase { XCTAssertEqual(request.header("Accept"), "text/event-stream") } - private func makeClient(apiKey: String? = nil) -> SDKBridgeClient { + private func makeClient(apiKey: String? = nil, bridgeToken: String? = nil) -> SDKBridgeClient { SDKBridgeClient( baseURL: URL(string: "http://localhost:8787")!, apiKey: apiKey, + bridgeToken: bridgeToken, session: makeSession() ) } diff --git a/README.md b/README.md index f1bba2a..cbc6245 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Runline is independent and is not affiliated with, endorsed by, or connected to - Chat-first Cloud Agents navigation - Cursor Cloud Agents v1 provider for account, repositories, models, agents, runs, streams, artifacts, archive, unarchive, and delete - Optional `@cursor/sdk` bridge client for Cursor SDK sessions, MCP profiles, subagents, and multi-turn follow-ups +- Runline Bridge pairing with one-time terminal codes and Keychain-backed bridge tokens - First-run and Settings runtime selection between Cloud Agent and Cursor SDK defaults - Native Cursor SDK composer controls for intent, model, MCP profile, image context, and file context - Keychain-backed Cursor API key storage diff --git a/orchestrator/README.md b/orchestrator/README.md index bc06af0..bd702d9 100644 --- a/orchestrator/README.md +++ b/orchestrator/README.md @@ -18,6 +18,8 @@ npm install CURSOR_API_KEY=your-cursor-key npm run dev ``` +`npm run bridge` is an alias for the same local bridge process and is the command shown in the iOS Cursor SDK setup flow. + For Simulator testing, `http://localhost:8787` is usually enough. For a physical iPhone or TestFlight build on the same Wi-Fi network, use your Mac's LAN address instead: ```bash @@ -28,6 +30,16 @@ Then set the app's bridge URL to `http://:8787`. For broader TestFli The iOS app can also send a per-request `Authorization: Bearer ` header. Prefer that for user-owned keys; the bridge does not need to store keys server-side. +## Pairing + +Runline Bridge requires a local pairing token by default. In the iOS app, open Settings, enable Runline Bridge, enter the bridge URL, and tap **Start Pairing**. The bridge prints a six-digit code in the terminal. Enter that code in the app to store a bridge token in the iOS Keychain. + +For local development only, you can disable pairing: + +```bash +RUNLINE_BRIDGE_DISABLE_PAIRING=true CURSOR_API_KEY=your-cursor-key npm run bridge +``` + ## MCP Profiles Publish SDK tool profiles with `RUNLINE_SDK_MCP_PROFILES`. The value is a JSON array. Each profile is exposed to iOS as metadata only, while the private MCP/subagent config stays on the bridge. @@ -56,6 +68,8 @@ export RUNLINE_SDK_MCP_PROFILES='[ ## Endpoints - `GET /health` +- `POST /pair/start` +- `POST /pair/complete` - `GET /sdk/mcp-profiles` - `POST /sdk/sessions` - `POST /sdk/sessions/:sessionId/messages` diff --git a/orchestrator/package.json b/orchestrator/package.json index f51bd59..636168f 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "bridge": "tsx src/server.ts", "dev": "tsx src/server.ts", "typecheck": "tsc --noEmit" }, diff --git a/orchestrator/src/server.ts b/orchestrator/src/server.ts index a28f88e..147964d 100644 --- a/orchestrator/src/server.ts +++ b/orchestrator/src/server.ts @@ -1,7 +1,11 @@ +import { randomBytes, randomInt, randomUUID } from "node:crypto"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { Agent, type McpServerConfig } from "@cursor/sdk"; const port = Number(process.env.PORT ?? 8787); +const serviceName = "runline-bridge"; +const sdkName = "@cursor/sdk"; +const pairingTTLMs = Number(process.env.RUNLINE_BRIDGE_PAIRING_TTL_MS ?? 5 * 60 * 1000); type CloudRunRequest = { prompt?: string; @@ -43,6 +47,30 @@ type SDKMCPProfile = { agents?: unknown; }; +type PairingSession = { + id: string; + code: string; + deviceName?: string; + expiresAt: number; +}; + +type PairingStartRequest = { + deviceName?: string; +}; + +type PairingCompleteRequest = { + pairingId?: string; + code?: string; + deviceName?: string; +}; + +const pairingSessions = new Map(); +const issuedBridgeTokens = new Set( + emptyToUndefined(process.env.RUNLINE_BRIDGE_TOKEN) + ? [process.env.RUNLINE_BRIDGE_TOKEN!.trim()] + : [] +); + const server = createServer(async (request, response) => { try { const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`); @@ -50,34 +78,51 @@ const server = createServer(async (request, response) => { if (request.method === "GET" && url.pathname === "/health") { sendJSON(response, 200, { ok: true, - service: "runline-orchestrator", - sdk: "@cursor/sdk", + service: serviceName, + sdk: sdkName, + pairingRequired: isBridgeAuthRequired(), + paired: isAuthorizedBridgeRequest(request), }); return; } + if (request.method === "POST" && url.pathname === "/pair/start") { + await startPairing(request, response); + return; + } + + if (request.method === "POST" && url.pathname === "/pair/complete") { + await completePairing(request, response); + return; + } + if (request.method === "GET" && url.pathname === "/sdk/mcp-profiles") { + if (!requireBridgeAuth(request, response)) { return; } sendJSON(response, 200, { profiles: publicMCPProfiles() }); return; } if (request.method === "POST" && url.pathname === "/sdk/sessions") { + if (!requireBridgeAuth(request, response)) { return; } await createSDKSession(request, response); return; } const sessionParams = matchSessionRoute(url.pathname); if (sessionParams && request.method === "POST" && url.pathname.endsWith("/messages")) { + if (!requireBridgeAuth(request, response)) { return; } await sendSDKSessionMessage(request, response, sessionParams); return; } if (sessionParams && request.method === "GET" && url.pathname.endsWith("/state")) { + if (!requireBridgeAuth(request, response)) { return; } await getSDKSessionState(request, response, sessionParams, url); return; } if (sessionParams && request.method === "GET" && sessionParams.runId && url.pathname.endsWith("/events")) { + if (!requireBridgeAuth(request, response)) { return; } await streamRunEvents(request, response, { agentId: sessionParams.sessionId, runId: sessionParams.runId, @@ -86,17 +131,20 @@ const server = createServer(async (request, response) => { } if (request.method === "POST" && url.pathname === "/runs/cloud") { + if (!requireBridgeAuth(request, response)) { return; } await createSDKSession(request, response); return; } const runParams = matchRunRoute(url.pathname); if (request.method === "GET" && runParams && url.pathname.endsWith("/state")) { + if (!requireBridgeAuth(request, response)) { return; } await getRunState(request, response, runParams); return; } if (request.method === "GET" && runParams && url.pathname.endsWith("/events")) { + if (!requireBridgeAuth(request, response)) { return; } await streamRunEvents(request, response, runParams); return; } @@ -108,9 +156,96 @@ const server = createServer(async (request, response) => { }); server.listen(port, () => { - console.log(`Runline orchestrator listening on http://localhost:${port}`); + console.log(`Runline Bridge listening on http://localhost:${port}`); + if (isBridgeAuthRequired()) { + console.log("Pairing is enabled. Start pairing from Runline Settings to print a one-time code here."); + } }); +async function startPairing(request: IncomingMessage, response: ServerResponse) { + if (!isBridgeAuthRequired()) { + sendJSON(response, 200, { + pairingRequired: false, + message: "Runline Bridge pairing is disabled by environment configuration.", + }); + return; + } + + cleanupExpiredPairings(); + const body = await readJSON(request); + const session: PairingSession = { + id: randomUUID(), + code: String(randomInt(100000, 1_000_000)), + deviceName: emptyToUndefined(body.deviceName), + expiresAt: Date.now() + pairingTTLMs, + }; + pairingSessions.set(session.id, session); + + const device = session.deviceName ? ` for ${session.deviceName}` : ""; + console.log(`\nRunline pairing code${device}: ${session.code}`); + console.log(`This code expires at ${new Date(session.expiresAt).toLocaleTimeString()}.\n`); + + sendJSON(response, 202, { + pairingId: session.id, + expiresAt: new Date(session.expiresAt).toISOString(), + message: "Check the terminal running Runline Bridge for the pairing code.", + }); +} + +async function completePairing(request: IncomingMessage, response: ServerResponse) { + cleanupExpiredPairings(); + const body = await readJSON(request); + const pairingId = emptyToUndefined(body.pairingId); + const submittedCode = emptyToUndefined(body.code); + if (!pairingId || !submittedCode) { + sendJSON(response, 400, { + error: "pairing_id_and_code_required", + message: "Pairing ID and code are required.", + }); + return; + } + + const session = pairingSessions.get(pairingId); + if (!session) { + sendJSON(response, 404, { + error: "pairing_not_found", + message: "Start a new pairing session from Runline Settings.", + }); + return; + } + + if (session.expiresAt <= Date.now()) { + pairingSessions.delete(pairingId); + sendJSON(response, 410, { + error: "pairing_expired", + message: "The pairing code expired. Start pairing again.", + }); + return; + } + + if (session.code !== submittedCode.trim()) { + sendJSON(response, 401, { + error: "invalid_pairing_code", + message: "The pairing code did not match.", + }); + return; + } + + const token = randomBytes(32).toString("base64url"); + issuedBridgeTokens.add(token); + pairingSessions.delete(pairingId); + + const deviceName = emptyToUndefined(body.deviceName) ?? session.deviceName ?? "Runline device"; + console.log(`Runline paired ${deviceName}.`); + + sendJSON(response, 200, { + bridgeToken: token, + bridgeName: "Runline Bridge", + service: serviceName, + sdk: sdkName, + }); +} + async function createSDKSession(request: IncomingMessage, response: ServerResponse) { const body = await readJSON(request); const apiKey = apiKeyFrom(request); @@ -401,6 +536,46 @@ function apiKeyFrom(request: IncomingMessage): string | undefined { return emptyToUndefined(process.env.CURSOR_API_KEY); } +function requireBridgeAuth(request: IncomingMessage, response: ServerResponse) { + if (isAuthorizedBridgeRequest(request)) { + return true; + } + sendJSON(response, 401, { + error: "bridge_pairing_required", + message: "Pair Runline Bridge from Settings before using Cursor SDK.", + }); + return false; +} + +function isAuthorizedBridgeRequest(request: IncomingMessage) { + if (!isBridgeAuthRequired()) { + return true; + } + const token = bridgeTokenFrom(request); + return Boolean(token && issuedBridgeTokens.has(token)); +} + +function bridgeTokenFrom(request: IncomingMessage): string | undefined { + const raw = request.headers["x-runline-bridge-token"]; + if (Array.isArray(raw)) { + return emptyToUndefined(raw[0]); + } + return emptyToUndefined(raw); +} + +function isBridgeAuthRequired() { + return process.env.RUNLINE_BRIDGE_DISABLE_PAIRING?.toLowerCase() !== "true"; +} + +function cleanupExpiredPairings() { + const now = Date.now(); + for (const [id, session] of pairingSessions) { + if (session.expiresAt <= now) { + pairingSessions.delete(id); + } + } +} + function mcpProfile(id: string | undefined): SDKMCPProfile | undefined { const profileId = emptyToUndefined(id); if (!profileId) { From 9b8fb8dc4889e5c65f1ff23ce7c740b2e3f4e6dc Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Wed, 6 May 2026 22:50:08 -0400 Subject: [PATCH 09/12] Prepare Runline Bridge npm package --- .gitignore | 1 + .../Features/CursorSDKOnboardingView.swift | 8 +- CursorMobileTests/CursorMobileTests.swift | 4 +- README.md | 11 +++ orchestrator/README.md | 22 +++-- orchestrator/bin/runline-bridge.js | 85 +++++++++++++++++++ orchestrator/package-lock.json | 9 +- orchestrator/package.json | 21 ++++- orchestrator/tsconfig.json | 2 + 9 files changed, 143 insertions(+), 20 deletions(-) create mode 100755 orchestrator/bin/runline-bridge.js diff --git a/.gitignore b/.gitignore index 340be82..e1695d9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ Package.resolved # Node node_modules/ **/node_modules/ +orchestrator/dist/ # Local secrets and generated config .asc/artifacts/ diff --git a/CursorMobile/Features/CursorSDKOnboardingView.swift b/CursorMobile/Features/CursorSDKOnboardingView.swift index abc6cbf..55f9d74 100644 --- a/CursorMobile/Features/CursorSDKOnboardingView.swift +++ b/CursorMobile/Features/CursorSDKOnboardingView.swift @@ -76,7 +76,7 @@ enum RunlineBridgeOnboardingStep: String, CaseIterable, Identifiable, Equatable case .overview: "Cloud Agent remains the default. Cursor SDK is an optional mode for local SDK sessions, MCP profiles, files, images, planning, and execution." case .bridge: - "Runline Bridge runs from the orchestrator folder and keeps Cursor SDK execution on your Mac." + "Install Runline Bridge from npm. It keeps Cursor SDK execution on your Mac." case .start: "Start the bridge on your Mac. For iPhone testing, use your Mac LAN address instead of localhost." case .connect: @@ -102,9 +102,9 @@ enum RunlineBridgeOnboardingStep: String, CaseIterable, Identifiable, Equatable case .overview, .connect: nil case .bridge: - "cd cursor_mobile/orchestrator && npm install" + "npm install -g runline-bridge@beta" case .start: - "CURSOR_API_KEY=your-cursor-key npm run bridge" + "CURSOR_API_KEY=your-cursor-key runline-bridge up" } } @@ -113,7 +113,7 @@ enum RunlineBridgeOnboardingStep: String, CaseIterable, Identifiable, Equatable case .overview: nil case .bridge: - "Runline Bridge now uses a one-time pairing code and stores the bridge token in the iOS Keychain." + "Only install this if you want Cursor SDK mode. Cloud Agent mode works without the bridge." case .start: "Simulator can use http://localhost:8787. A physical iPhone needs a reachable Mac LAN URL such as http://192.168.1.10:8787." case .connect: diff --git a/CursorMobileTests/CursorMobileTests.swift b/CursorMobileTests/CursorMobileTests.swift index 934f48b..4ddac99 100644 --- a/CursorMobileTests/CursorMobileTests.swift +++ b/CursorMobileTests/CursorMobileTests.swift @@ -68,8 +68,8 @@ final class CursorMobileTests: XCTestCase { func testCursorSDKOnboardingStepsExposeCurrentBridgeCommands() { XCTAssertEqual(RunlineBridgeOnboardingStep.allCases.first, .overview) XCTAssertEqual(RunlineBridgeOnboardingStep.allCases.last, .connect) - XCTAssertEqual(RunlineBridgeOnboardingStep.bridge.command, "cd cursor_mobile/orchestrator && npm install") - XCTAssertEqual(RunlineBridgeOnboardingStep.start.command, "CURSOR_API_KEY=your-cursor-key npm run bridge") + XCTAssertEqual(RunlineBridgeOnboardingStep.bridge.command, "npm install -g runline-bridge@beta") + XCTAssertEqual(RunlineBridgeOnboardingStep.start.command, "CURSOR_API_KEY=your-cursor-key runline-bridge up") XCTAssertNil(RunlineBridgeOnboardingStep.connect.command) } diff --git a/README.md b/README.md index cbc6245..9f14d24 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,17 @@ Runline is independent and is not affiliated with, endorsed by, or connected to - Local cache for account, repositories, models, agents, runs, stream events, artifacts, notification preferences, and launch draft - Unit tests for Cursor v1 request contracts, SSE parsing, cache persistence, app routing, push payloads, chat event cleanup, file attachment loading, and SDK bridge request mapping +## Cursor SDK Mode + +Cloud Agent mode works directly from iOS. Cursor SDK mode is optional and requires Runline Bridge on the user's Mac: + +```bash +npm install -g runline-bridge@beta +CURSOR_API_KEY=your-cursor-key runline-bridge up +``` + +Runline Bridge prints the iPhone-reachable URL and pairing instructions. The iOS app stores the bridge token in Keychain after pairing. + ## Release Workflow ASC is configured through the local `Runline` keychain profile. Release automation lives in `.asc/workflow.json`. diff --git a/orchestrator/README.md b/orchestrator/README.md index bd702d9..9d4d4c7 100644 --- a/orchestrator/README.md +++ b/orchestrator/README.md @@ -1,6 +1,6 @@ -# Runline Orchestrator +# Runline Bridge -This is an optional TypeScript backend for Cursor SDK-only workflows. The iOS app does not depend on this service for the core Cloud Agent path; Runline keeps using Cursor's v1 REST API directly for account, repository, model, agent, run, stream, lifecycle, and artifact basics. +Runline Bridge is the optional Mac-side CLI for Cursor SDK workflows. The iOS app does not depend on this service for the core Cloud Agent path; Runline keeps using Cursor's v1 REST API directly for account, repository, model, agent, run, stream, lifecycle, and artifact basics. Use this service only for work that benefits from `@cursor/sdk`: @@ -10,16 +10,24 @@ Use this service only for work that benefits from `@cursor/sdk`: - Service-account workflows for teams. - Future automation or APNs backend jobs that should not run in the iOS app. -## Run Locally +## Install + +```bash +npm install -g runline-bridge@beta +CURSOR_API_KEY=your-cursor-key runline-bridge up +``` + +Only install Runline Bridge if you want Cursor SDK mode. Cloud Agent mode in the iOS app works without this package. + +## Run From This Repo ```bash cd orchestrator npm install -CURSOR_API_KEY=your-cursor-key npm run dev +npm run build +CURSOR_API_KEY=your-cursor-key npm run bridge ``` -`npm run bridge` is an alias for the same local bridge process and is the command shown in the iOS Cursor SDK setup flow. - For Simulator testing, `http://localhost:8787` is usually enough. For a physical iPhone or TestFlight build on the same Wi-Fi network, use your Mac's LAN address instead: ```bash @@ -37,7 +45,7 @@ Runline Bridge requires a local pairing token by default. In the iOS app, open S For local development only, you can disable pairing: ```bash -RUNLINE_BRIDGE_DISABLE_PAIRING=true CURSOR_API_KEY=your-cursor-key npm run bridge +RUNLINE_BRIDGE_DISABLE_PAIRING=true CURSOR_API_KEY=your-cursor-key runline-bridge up ``` ## MCP Profiles diff --git a/orchestrator/bin/runline-bridge.js b/orchestrator/bin/runline-bridge.js new file mode 100755 index 0000000..df02d71 --- /dev/null +++ b/orchestrator/bin/runline-bridge.js @@ -0,0 +1,85 @@ +#!/usr/bin/env node + +import { networkInterfaces } from "node:os"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const command = process.argv[2] ?? "up"; +const args = process.argv.slice(3); + +if (command === "--help" || command === "-h" || command === "help") { + printHelp(); + process.exit(0); +} + +if (command === "--version" || command === "-v" || command === "version") { + const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"); + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); + console.log(packageJson.version); + process.exit(0); +} + +if (command !== "up") { + console.error(`Unknown command: ${command}`); + printHelp(); + process.exit(2); +} + +const port = valueAfter("--port") ?? process.env.PORT ?? "8787"; +process.env.PORT = port; + +printStartup(port); +await import("../dist/server.js"); + +function valueAfter(name) { + const index = args.indexOf(name); + if (index === -1) { + return undefined; + } + return args[index + 1]; +} + +function printHelp() { + console.log(` +Runline Bridge + +Usage: + runline-bridge up [--port 8787] + +Environment: + CURSOR_API_KEY Cursor API key used when the iOS app does not send one per request. + RUNLINE_SDK_MCP_PROFILES JSON array of MCP/subagent profiles exposed to Runline. + RUNLINE_BRIDGE_DISABLE_PAIRING Set to true for local development only. +`); +} + +function printStartup(port) { + const lanURL = firstLANAddress() + ? `http://${firstLANAddress()}:${port}` + : undefined; + + console.log("Runline Bridge"); + console.log(""); + console.log(`Local URL: http://localhost:${port}`); + if (lanURL) { + console.log(`iPhone URL: ${lanURL}`); + } + console.log(""); + console.log("In Runline on iPhone:"); + console.log(" Settings -> Cursor SDK -> Enable Runline Bridge"); + console.log(" Enter the iPhone URL, tap Start Pairing, then enter the terminal code."); + console.log(""); +} + +function firstLANAddress() { + for (const addresses of Object.values(networkInterfaces())) { + for (const address of addresses ?? []) { + if (address.family === "IPv4" && !address.internal) { + return address.address; + } + } + } + return undefined; +} diff --git a/orchestrator/package-lock.json b/orchestrator/package-lock.json index 9bf711f..5a221bc 100644 --- a/orchestrator/package-lock.json +++ b/orchestrator/package-lock.json @@ -1,22 +1,25 @@ { - "name": "runline-orchestrator", + "name": "runline-bridge", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "runline-orchestrator", + "name": "runline-bridge", "version": "0.1.0", "dependencies": { "@cursor/sdk": "^1.0.11" }, + "bin": { + "runline-bridge": "bin/runline-bridge.js" + }, "devDependencies": { "@types/node": "^24.0.0", "tsx": "^4.19.0", "typescript": "^5.8.0" }, "engines": { - "node": ">=22" + "node": ">=20" } }, "node_modules/@bufbuild/protobuf": { diff --git a/orchestrator/package.json b/orchestrator/package.json index 636168f..5485bad 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -1,13 +1,26 @@ { - "name": "runline-orchestrator", + "name": "runline-bridge", "version": "0.1.0", - "private": true, + "description": "Local Mac bridge for Runline Cursor SDK sessions.", "type": "module", + "bin": { + "runline-bridge": "./bin/runline-bridge.js" + }, + "files": [ + "bin", + "dist", + "README.md" + ], "scripts": { - "bridge": "tsx src/server.ts", + "build": "tsc", + "bridge": "node dist/server.js", "dev": "tsx src/server.ts", + "prepack": "npm run build", "typecheck": "tsc --noEmit" }, + "publishConfig": { + "tag": "beta" + }, "dependencies": { "@cursor/sdk": "^1.0.11" }, @@ -17,6 +30,6 @@ "typescript": "^5.8.0" }, "engines": { - "node": ">=22" + "node": ">=20" } } diff --git a/orchestrator/tsconfig.json b/orchestrator/tsconfig.json index f7a125b..c15501e 100644 --- a/orchestrator/tsconfig.json +++ b/orchestrator/tsconfig.json @@ -7,6 +7,8 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, + "rootDir": "src", + "outDir": "dist", "noUncheckedIndexedAccess": true }, "include": ["src/**/*.ts"] From dba61c76fb540cc1deb74dd7103981d19e85ff48 Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Wed, 6 May 2026 23:08:58 -0400 Subject: [PATCH 10/12] Normalize bridge package bin path --- orchestrator/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchestrator/package.json b/orchestrator/package.json index 5485bad..5ca0639 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -4,7 +4,7 @@ "description": "Local Mac bridge for Runline Cursor SDK sessions.", "type": "module", "bin": { - "runline-bridge": "./bin/runline-bridge.js" + "runline-bridge": "bin/runline-bridge.js" }, "files": [ "bin", From 269942212e33b9e9bd492a35b86dd6d0acfff94d Mon Sep 17 00:00:00 2001 From: Parris Digital Date: Wed, 6 May 2026 23:40:35 -0400 Subject: [PATCH 11/12] Prepare repository for open source beta --- .github/ISSUE_TEMPLATE/bug_report.yml | 54 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.yml | 33 + .github/PULL_REQUEST_TEMPLATE.md | 16 + .github/dependabot.yml | 12 + .github/workflows/ci.yml | 39 + .gitignore | 7 + .gitleaks.toml | 6 + CODE_OF_CONDUCT.md | 23 + CONTRIBUTING.md | 49 + LICENSE | 21 + README.md | 58 +- SECURITY.md | 35 + SUPPORT.md | 22 + orchestrator/README.md | 17 +- ...{package-lock.json => npm-shrinkwrap.json} | 1085 +++-------------- orchestrator/package.json | 29 +- 17 files changed, 570 insertions(+), 941 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitleaks.toml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 SUPPORT.md rename orchestrator/{package-lock.json => npm-shrinkwrap.json} (54%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..5ec75f2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,54 @@ +name: Bug report +description: Report a reproducible Runline issue. +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Do not include Cursor API keys, bridge pairing tokens, Apple credentials, private repository contents, or other secrets. + - type: input + id: version + attributes: + label: App or bridge version + description: Include the TestFlight build number or `runline-bridge --version`. + placeholder: "Runline 1.0 (11), runline-bridge 0.1.0" + validations: + required: true + - type: dropdown + id: mode + attributes: + label: Mode + options: + - Cloud Agent + - Cursor SDK with Runline Bridge + - Settings or onboarding + - Other + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: "1. Open...\n2. Tap...\n3. See..." + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + placeholder: "iPhone/iPad model, iOS version, Mac version, Node version, network setup" + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8fbb09d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security report + url: mailto:parrisdigital@gmail.com + about: Please report security vulnerabilities privately. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..c9a1c6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,33 @@ +name: Feature request +description: Suggest a focused improvement. +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What user workflow should this improve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: What should Runline do? + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - Cloud Agent + - Cursor SDK and Runline Bridge + - iPhone UI + - iPad UI + - Notifications + - Documentation + - Other + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..af2b92e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +## Summary + +- + +## Testing + +- [ ] `npm --prefix orchestrator run typecheck` +- [ ] `npm --prefix orchestrator run build` +- [ ] iOS tests or manual simulator testing, as relevant + +## Checklist + +- [ ] Cloud Agent mode still works without Runline Bridge +- [ ] Cursor SDK mode remains optional and clearly labeled +- [ ] No secrets, signing files, generated archives, IPAs, or local artifacts are committed +- [ ] Documentation was updated if behavior changed diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ce6b0fa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: /orchestrator + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2edbb02 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + bridge: + name: Runline Bridge + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: orchestrator/npm-shrinkwrap.json + - run: npm ci + working-directory: orchestrator + - run: npm run typecheck + working-directory: orchestrator + - run: npm run build + working-directory: orchestrator + - run: npm pack --pack-destination /tmp + working-directory: orchestrator + + project-metadata: + name: Project Metadata + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - run: xcodebuild -list -project CursorMobile.xcodeproj + - run: ruby -c Tools/set_build_number.rb + - run: plutil -lint ExportOptions-AppStore.plist ExportOptions-TestFlightUpload.plist CursorMobile/Resources/Info.plist CursorMobile/Resources/PrivacyInfo.xcprivacy CursorMobile/Resources/CursorMobile.entitlements diff --git a/.gitignore b/.gitignore index e1695d9..a2210a9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,16 @@ orchestrator/dist/ .asc/runs/ .env .env.* +.npmrc *.local.xcconfig Secrets.plist *.p8 +*.p12 +*.pem +*.key +*.cer +*.mobileprovision +*.provisionprofile # macOS .DS_Store diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..4fce96d --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,6 @@ +[allowlist] +description = "Allow documentation placeholders that are intentionally non-secret." +regexes = [ + '''YOUR_CURSOR_API_KEY''', + '''your-cursor-key''' +] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..96a11c0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,23 @@ +# Code of Conduct + +## Our Pledge + +We pledge to make participation in this project respectful and harassment-free for everyone. + +## Expected Behavior + +- Be direct, constructive, and respectful. +- Assume good intent while asking for clarification when needed. +- Keep technical criticism focused on the work. +- Respect privacy and do not post secrets, tokens, private keys, private repository contents, or personal data. + +## Unacceptable Behavior + +- Harassment, insults, threats, or discriminatory language. +- Publishing private information without permission. +- Publicly disclosing security vulnerabilities before maintainers have had a reasonable chance to respond. +- Repeatedly derailing issues or pull requests. + +## Enforcement + +Report concerns to `parrisdigital@gmail.com`. Maintainers may remove comments, close issues, reject pull requests, or block participants when needed to protect the project. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..538b36d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing + +Thanks for helping improve Runline. This project is in public beta, so keep changes focused and easy to review. + +Runline is independent and is not affiliated with, endorsed by, or connected to Cursor or Anysphere. + +## Development Setup + +Requirements: + +- macOS with Xcode capable of building the configured iOS target +- iOS 26+ simulator runtime for full local app testing +- Node.js 20+ +- npm + +Install bridge dependencies: + +```bash +npm --prefix orchestrator install +``` + +Run bridge checks: + +```bash +npm --prefix orchestrator run typecheck +npm --prefix orchestrator run build +``` + +Run iOS tests: + +```bash +xcodebuild test \ + -project CursorMobile.xcodeproj \ + -scheme CursorMobile \ + -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.4.1' +``` + +## Pull Requests + +- Keep UI changes native to SwiftUI and iOS system patterns. +- Keep Cloud Agent mode working without Runline Bridge. +- Keep Cursor SDK mode optional and clearly labeled as requiring Runline Bridge. +- Add or update tests for behavior changes. +- Do not commit generated archives, IPAs, derived data, local ASC artifacts, npm tokens, Apple signing material, API keys, or private repository data. +- Run `git diff --check`, bridge typecheck/build, and relevant iOS tests before opening a PR. + +## Security + +Report vulnerabilities privately using [SECURITY.md](SECURITY.md). Do not disclose credentials or private repository data in public issues or pull requests. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6bc52fc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Matthew Parris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9f14d24..5b272fa 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ # Runline for Cursor -Runline is a native iOS 26+ client for managing Cursor Cloud Agents from iPhone. +Runline is a native iOS 26+ client for managing Cursor Cloud Agents from iPhone and iPad. The app uses a proven Cloud Agents foundation with a chat-first, system-native iOS interface: stock navigation, lists, forms, sheets, toolbars, and settings surfaces. Runline is independent and is not affiliated with, endorsed by, or connected to Cursor or Anysphere. -## Current Foundation +## Status + +Runline is in public beta. Cloud Agent mode is the default path and works directly from iOS. Cursor SDK mode is optional and requires Runline Bridge on the user's Mac. + +## Features - iOS 26+ SwiftUI app target -- Chat-first Cloud Agents navigation +- Chat-first Cloud Agents navigation for iPhone and iPad - Cursor Cloud Agents v1 provider for account, repositories, models, agents, runs, streams, artifacts, archive, unarchive, and delete - Optional `@cursor/sdk` bridge client for Cursor SDK sessions, MCP profiles, subagents, and multi-turn follow-ups - Runline Bridge pairing with one-time terminal codes and Keychain-backed bridge tokens @@ -25,32 +29,60 @@ Cloud Agent mode works directly from iOS. Cursor SDK mode is optional and requir ```bash npm install -g runline-bridge@beta -CURSOR_API_KEY=your-cursor-key runline-bridge up +export CURSOR_API_KEY="replace-with-your-cursor-key" +runline-bridge up ``` Runline Bridge prints the iPhone-reachable URL and pairing instructions. The iOS app stores the bridge token in Keychain after pairing. -## Release Workflow +## Development -ASC is configured through the local `Runline` keychain profile. Release automation lives in `.asc/workflow.json`. +Install bridge dependencies: + +```bash +npm --prefix orchestrator install +``` + +Run bridge checks: + +```bash +npm --prefix orchestrator run typecheck +npm --prefix orchestrator run build +``` -Validate the workflow: +Run iOS tests: ```bash -asc workflow validate -asc workflow list +xcodebuild test \ + -project CursorMobile.xcodeproj \ + -scheme CursorMobile \ + -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.4.1' ``` -Run local release checks: +## Release Workflow + +TestFlight and App Store releases are maintainer-only and are not required for contributors. + +Local ASC automation expects a private `Runline` App Store Connect profile in the maintainer's keychain. Keep Apple API keys, signing files, archives, IPAs, and ASC run artifacts out of git. + +Run local preflight checks: ```bash asc workflow run preflight ``` -Upload the next TestFlight build with an explicit build number: +Upload a TestFlight build with an explicit build number: ```bash -asc workflow run testflight BUILD_NUMBER:11 +asc workflow run testflight BUILD_NUMBER: ``` -Use explicit build numbers so release numbering stays aligned with the active Runline sequence. The current release target is `1.0 (11)`. +Use explicit build numbers so TestFlight stays aligned with the active Runline sequence. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). + +## License + +Runline is released under the [MIT License](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..cece597 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ +# Security Policy + +Runline is a public beta project. Please report security issues privately before opening a public issue. + +## Supported Versions + +Security fixes target: + +- the latest commit on `main` +- the latest TestFlight beta build +- the latest published `runline-bridge` npm package + +Older TestFlight builds and old npm package versions may be unsupported during beta. + +## Reporting a Vulnerability + +Email `parrisdigital@gmail.com` with: + +- a clear description of the issue +- affected app, bridge, or repository version +- reproduction steps +- expected impact +- any logs or screenshots that do not contain secrets + +Do not include Cursor API keys, Apple credentials, npm tokens, GitHub tokens, private repository contents, or bridge pairing tokens in public issues. + +## Credential Handling + +- Cursor API keys are stored on iOS in Keychain. +- Runline Bridge pairing tokens are stored on iOS in Keychain. +- Runline Bridge accepts a Cursor API key per request or from `CURSOR_API_KEY` in the user's local environment. +- Runline Bridge must not store or log user Cursor API keys. +- The repository must not contain `.p8`, `.p12`, `.mobileprovision`, `.env`, `.npmrc`, private keys, or signing certificates. + +If a credential is ever committed, revoke and rotate it before opening the repository publicly. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..e600a90 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,22 @@ +# Support + +Runline is in public beta. + +## General Help + +Open a GitHub issue for: + +- reproducible app bugs +- Runline Bridge installation problems +- documentation issues +- focused feature requests + +Include the app build number, iOS version, device type, bridge version, and clear reproduction steps when relevant. + +## Security Issues + +Do not open public issues for security reports. Use [SECURITY.md](SECURITY.md). + +## Cursor or Apple Account Issues + +Runline is independent and cannot provide support for Cursor accounts, Apple IDs, App Store Connect, or TestFlight account access. diff --git a/orchestrator/README.md b/orchestrator/README.md index 9d4d4c7..7303062 100644 --- a/orchestrator/README.md +++ b/orchestrator/README.md @@ -14,7 +14,8 @@ Use this service only for work that benefits from `@cursor/sdk`: ```bash npm install -g runline-bridge@beta -CURSOR_API_KEY=your-cursor-key runline-bridge up +export CURSOR_API_KEY="replace-with-your-cursor-key" +runline-bridge up ``` Only install Runline Bridge if you want Cursor SDK mode. Cloud Agent mode in the iOS app works without this package. @@ -25,7 +26,8 @@ Only install Runline Bridge if you want Cursor SDK mode. Cloud Agent mode in the cd orchestrator npm install npm run build -CURSOR_API_KEY=your-cursor-key npm run bridge +export CURSOR_API_KEY="replace-with-your-cursor-key" +npm run bridge ``` For Simulator testing, `http://localhost:8787` is usually enough. For a physical iPhone or TestFlight build on the same Wi-Fi network, use your Mac's LAN address instead: @@ -36,7 +38,7 @@ ipconfig getifaddr en0 Then set the app's bridge URL to `http://:8787`. For broader TestFlight use, deploy the bridge behind HTTPS and use that hosted URL. -The iOS app can also send a per-request `Authorization: Bearer ` header. Prefer that for user-owned keys; the bridge does not need to store keys server-side. +The iOS app can also send the user's Cursor API key as a per-request bearer token. Prefer that for user-owned keys; the bridge does not need to store keys server-side. ## Pairing @@ -45,7 +47,8 @@ Runline Bridge requires a local pairing token by default. In the iOS app, open S For local development only, you can disable pairing: ```bash -RUNLINE_BRIDGE_DISABLE_PAIRING=true CURSOR_API_KEY=your-cursor-key runline-bridge up +export CURSOR_API_KEY="replace-with-your-cursor-key" +RUNLINE_BRIDGE_DISABLE_PAIRING=true runline-bridge up ``` ## MCP Profiles @@ -87,14 +90,14 @@ export RUNLINE_SDK_MCP_PROFILES='[ - `GET /agents/:agentId/runs/:runId/state` - `GET /agents/:agentId/runs/:runId/events` -Requests may pass a Cursor API key with `Authorization: Bearer `. If omitted, the service uses `CURSOR_API_KEY`. Do not put user keys in logs or long-lived storage. +Requests may pass a Cursor API key with bearer authentication. If omitted, the service uses `CURSOR_API_KEY`. Do not put user keys in logs or long-lived storage. ## SDK Session Example ```bash curl http://localhost:8787/sdk/sessions \ -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_CURSOR_API_KEY' \ + --oauth2-bearer "$CURSOR_API_KEY" \ -d '{ "prompt": "Create an implementation plan for the failing tests.", "intent": "plan", @@ -113,7 +116,7 @@ The response includes `sessionId`, `agentId`, `runId`, `sessionEventsURL`, and ` ```bash curl http://localhost:8787/sdk/sessions/bc-example/messages \ -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer YOUR_CURSOR_API_KEY' \ + --oauth2-bearer "$CURSOR_API_KEY" \ -d '{ "prompt": "Execute the approved plan.", "intent": "execute", diff --git a/orchestrator/package-lock.json b/orchestrator/npm-shrinkwrap.json similarity index 54% rename from orchestrator/package-lock.json rename to orchestrator/npm-shrinkwrap.json index 5a221bc..a73de4c 100644 --- a/orchestrator/package-lock.json +++ b/orchestrator/npm-shrinkwrap.json @@ -1,14 +1,15 @@ { "name": "runline-bridge", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "runline-bridge", - "version": "0.1.0", + "version": "0.1.1", + "license": "MIT", "dependencies": { - "@cursor/sdk": "^1.0.11" + "@cursor/sdk": "^1.0.12" }, "bin": { "runline-bridge": "bin/runline-bridge.js" @@ -54,9 +55,9 @@ } }, "node_modules/@cursor/sdk": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cursor/sdk/-/sdk-1.0.11.tgz", - "integrity": "sha512-DkTwOAuao9wIeUioaM0aQi6hkWLC8oLAnqlR4HR9hn5xytd9A4cEB2fZpSHd8pJ2YRN0VJVkxnggxLRNT7ghuQ==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk/-/sdk-1.0.12.tgz", + "integrity": "sha512-jGx0wFY1N9uIdIKr303CfM6m/dLXmRCUnU/0yNP/oiOpkBXqgqaThGbgYbcOeVrYonMZc/DZJ9EydXOEPJLcbg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@bufbuild/protobuf": "1.10.0", @@ -70,17 +71,17 @@ "node": ">=18" }, "optionalDependencies": { - "@cursor/sdk-darwin-arm64": "1.0.11", - "@cursor/sdk-darwin-x64": "1.0.11", - "@cursor/sdk-linux-arm64": "1.0.11", - "@cursor/sdk-linux-x64": "1.0.11", - "@cursor/sdk-win32-x64": "1.0.11" + "@cursor/sdk-darwin-arm64": "1.0.12", + "@cursor/sdk-darwin-x64": "1.0.12", + "@cursor/sdk-linux-arm64": "1.0.12", + "@cursor/sdk-linux-x64": "1.0.12", + "@cursor/sdk-win32-x64": "1.0.12" } }, "node_modules/@cursor/sdk-darwin-arm64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-arm64/-/sdk-darwin-arm64-1.0.11.tgz", - "integrity": "sha512-jbbdt4k1Wjjzsye9kfJtn7nPHd1QgBtOA1tbmLVbXIVb5UeAu+q7uT/8aggm8qN8R151m/GNW2ntK29+Q8y/XQ==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-arm64/-/sdk-darwin-arm64-1.0.12.tgz", + "integrity": "sha512-AOFx+aX+4SntAeC66YncHACXk5duxp+HzDrxxF4Tl93N6nLjHaHEKSAXbt87ivL34MCHop4v/3c70QzBhamB2g==", "cpu": [ "arm64" ], @@ -91,9 +92,9 @@ ] }, "node_modules/@cursor/sdk-darwin-x64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-x64/-/sdk-darwin-x64-1.0.11.tgz", - "integrity": "sha512-2352S+tGbaDgj2qb3oNN2FUG5250cn3cD+aKluETFd7jI7Pm3ctwInFN+/NWWnzwftibjKnwcc8ghm9q4xYfWg==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-x64/-/sdk-darwin-x64-1.0.12.tgz", + "integrity": "sha512-/ZDAYFUrnPd8hAGRky9ZGcROqZSZ2b5W+aEjTdINzLhJ8x5ZNXtjaz0ZYSHabOn2BeErjXgTcq+4bX2/To4C1A==", "cpu": [ "x64" ], @@ -104,9 +105,9 @@ ] }, "node_modules/@cursor/sdk-linux-arm64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-arm64/-/sdk-linux-arm64-1.0.11.tgz", - "integrity": "sha512-SGnwU1caprU6L7XCMUH48pyGdrZz1YQhPNUzrUyixHpdfM951KJmAQyuW9Hj2J4J3C1PG4XwIYRHsGN8/EOF2g==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-arm64/-/sdk-linux-arm64-1.0.12.tgz", + "integrity": "sha512-kAxNqiB3dPtlW9fVjjIZEdbIGEGLA9moOM3zYwsXh8J1Qw942nJYMGDGR4o8x0zglwZ24a1JpovvZamrCaC3Yw==", "cpu": [ "arm64" ], @@ -117,9 +118,9 @@ ] }, "node_modules/@cursor/sdk-linux-x64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-x64/-/sdk-linux-x64-1.0.11.tgz", - "integrity": "sha512-zzVwEMc9ykyyFgxaXwfiB0Nuqnp0PkKqiWSt6Iubmi7ADY87dtVS67qwtmVQ+FJVA7iXV+c7LY2sQ2qfQ4aP2w==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-x64/-/sdk-linux-x64-1.0.12.tgz", + "integrity": "sha512-RmBiBCPKMZC5McDerGk2Rk4P47xz2A+uzRoRgH6sMoOjklc33ry11iAZC0D5F5xH85chgY878086A/Q8+XrAuA==", "cpu": [ "x64" ], @@ -130,9 +131,9 @@ ] }, "node_modules/@cursor/sdk-win32-x64": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cursor/sdk-win32-x64/-/sdk-win32-x64-1.0.11.tgz", - "integrity": "sha512-iWvGDFhpW+C6/zah7feY3oURozJxQ78qjld+9ejOaRuuC6p33Q6D/3l6Ihst18lEH9WSjEJClydDFUbm7aPf5A==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-win32-x64/-/sdk-win32-x64-1.0.12.tgz", + "integrity": "sha512-uH4shdHrKOdtNLapy1uuScJ9lL2Pc8zc9I9ZKC6b6bx+0UX6xLAqjPP7dqVPfO6D9u61yLq1Hs86XOLs5ZVkPA==", "cpu": [ "x64" ], @@ -584,46 +585,16 @@ "node": ">=18" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "license": "MIT", - "optional": true - }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "license": "ISC", - "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "license": "MIT", - "optional": true, "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" + "minipass": "^7.0.4" }, "engines": { - "node": ">=10" + "node": ">=18.0.0" } }, "node_modules/@statsig/client-core": { @@ -641,16 +612,6 @@ "@statsig/client-core": "3.31.0" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/@types/node": { "version": "24.12.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", @@ -662,91 +623,15 @@ } }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC", - "optional": true - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "license": "MIT", - "optional": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "license": "ISC", "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "optional": true - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -787,17 +672,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -822,95 +696,13 @@ "ieee754": "^1.1.13" } }, - "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT", - "optional": true - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC", - "optional": true - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.1.3" - }, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18" } }, "node_modules/decompress-response": { @@ -937,13 +729,6 @@ "node": ">=4.0.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT", - "optional": true - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -953,23 +738,6 @@ "node": ">=8" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -989,13 +757,6 @@ "node": ">=6" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "license": "MIT", - "optional": true - }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -1047,6 +808,31 @@ "node": ">=6" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1059,25 +845,6 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC", - "optional": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1093,27 +860,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/get-tsconfig": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", @@ -1133,28 +879,6 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1162,72 +886,6 @@ "license": "ISC", "optional": true }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC", - "optional": true - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1248,45 +906,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "license": "ISC", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1299,79 +918,14 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.1.tgz", - "integrity": "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "license": "MIT", - "optional": true - }, "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "optional": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "license": "ISC", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", "optional": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, "engines": { - "node": ">= 10" + "node": ">=20" } }, "node_modules/mimic-response": { @@ -1386,19 +940,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1409,110 +950,24 @@ } }, "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", - "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "license": "BlueOak-1.0.0", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" + "node": ">= 18" } }, "node_modules/mkdirp-classic": { @@ -1521,29 +976,12 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "optional": true - }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-abi": { "version": "3.89.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", @@ -1557,67 +995,53 @@ } }, "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } }, "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", "license": "MIT", "optional": true, "dependencies": { "env-paths": "^2.2.0", - "glob": "^7.1.4", + "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": ">= 10.12.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "license": "ISC", "optional": true, "dependencies": { - "abbrev": "1" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": ">=6" - } - }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/once": { @@ -1629,30 +1053,17 @@ "wrappy": "1" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "optional": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/prebuild-install": { @@ -1682,25 +1093,14 @@ "node": ">=10" } }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "license": "ISC", - "optional": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "license": "MIT", "optional": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, "engines": { - "node": ">=10" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/pump": { @@ -1752,33 +1152,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1799,13 +1172,6 @@ ], "license": "MIT" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT", - "optional": true - }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -1818,20 +1184,6 @@ "node": ">=10" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC", - "optional": true - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true - }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -1877,64 +1229,26 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", - "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", - "license": "MIT", - "optional": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/sqlite3": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", - "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-6.0.1.tgz", + "integrity": "sha512-X0czUUMG2tmSqJpEQa3tCuZSHKIx8PwM53vLZzKp/o6Rpy25fiVfjdbnZ988M8+O3ZWR1ih0K255VumCb3MAnQ==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "bindings": "^1.5.0", - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "tar": "^6.1.11" + "node-addon-api": "^8.0.0", + "prebuild-install": "^7.1.3", + "tar": "^7.5.10" + }, + "engines": { + "node": ">=20.17.0" }, "optionalDependencies": { - "node-gyp": "8.x" + "node-gyp": "12.x" }, "peerDependencies": { - "node-gyp": "8.x" + "node-gyp": "12.x" }, "peerDependenciesMeta": { "node-gyp": { @@ -1942,19 +1256,6 @@ } } }, - "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1964,34 +1265,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -2002,21 +1275,19 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", + "version": "7.5.14", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.14.tgz", + "integrity": "sha512-/7sHKgQO3JLP9ESlwTYUUftHUadOURUqq23xs1vjcnp8Vss6k0wCfzulyEtk5g91pjvnuriimGlyG7k6msrzRw==", + "license": "BlueOak-1.0.0", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { @@ -2053,13 +1324,21 @@ "node": ">=6" } }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "optional": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=8" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tsx": { @@ -2109,15 +1388,12 @@ } }, "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, "node_modules/undici-types": { @@ -2127,26 +1403,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "license": "ISC", - "optional": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2154,29 +1410,19 @@ "license": "MIT" }, "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "license": "ISC", "optional": true, "dependencies": { - "isexe": "^2.0.0" + "isexe": "^4.0.0" }, "bin": { - "node-which": "bin/node-which" + "node-which": "bin/which.js" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/wrappy": { @@ -2186,10 +1432,13 @@ "license": "ISC" }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, "node_modules/zod": { "version": "3.25.76", diff --git a/orchestrator/package.json b/orchestrator/package.json index 5ca0639..4173947 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -1,15 +1,33 @@ { "name": "runline-bridge", - "version": "0.1.0", + "version": "0.1.1", "description": "Local Mac bridge for Runline Cursor SDK sessions.", + "license": "MIT", "type": "module", + "homepage": "https://github.com/parrisdigital/cursor_mobile#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/parrisdigital/cursor_mobile.git", + "directory": "orchestrator" + }, + "bugs": { + "url": "https://github.com/parrisdigital/cursor_mobile/issues" + }, + "keywords": [ + "cursor", + "cursor-sdk", + "ios", + "runline", + "bridge" + ], "bin": { "runline-bridge": "bin/runline-bridge.js" }, "files": [ "bin", "dist", - "README.md" + "README.md", + "npm-shrinkwrap.json" ], "scripts": { "build": "tsc", @@ -22,7 +40,12 @@ "tag": "beta" }, "dependencies": { - "@cursor/sdk": "^1.0.11" + "@cursor/sdk": "^1.0.12" + }, + "overrides": { + "sqlite3": "^6.0.1", + "tar": "^7.5.14", + "undici": "^6.24.0" }, "devDependencies": { "@types/node": "^24.0.0", From dca666d614e63c55651362af932fea20117e280d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 03:41:27 +0000 Subject: [PATCH 12/12] Bump actions/setup-node from 4 to 6 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2edbb02..c5577bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 20 cache: npm