refactor: move OAuth storage and drop legacy sessions
This commit is contained in:
@@ -6,7 +6,7 @@ import SwiftUI
|
|||||||
struct AnthropicAuthControls: View {
|
struct AnthropicAuthControls: View {
|
||||||
let connectionMode: AppState.ConnectionMode
|
let connectionMode: AppState.ConnectionMode
|
||||||
|
|
||||||
@State private var oauthStatus: PiOAuthStore.AnthropicOAuthStatus = PiOAuthStore.anthropicOAuthStatus()
|
@State private var oauthStatus: ClawdisOAuthStore.AnthropicOAuthStatus = ClawdisOAuthStore.anthropicOAuthStatus()
|
||||||
@State private var pkce: AnthropicOAuth.PKCE?
|
@State private var pkce: AnthropicOAuth.PKCE?
|
||||||
@State private var code: String = ""
|
@State private var code: String = ""
|
||||||
@State private var busy = false
|
@State private var busy = false
|
||||||
@@ -20,7 +20,7 @@ struct AnthropicAuthControls: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
if self.connectionMode != .local {
|
if self.connectionMode != .local {
|
||||||
Text("Gateway isn’t running locally; OAuth must be created on the gateway host where Pi runs.")
|
Text("Gateway isn’t running locally; OAuth must be created on the gateway host.")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
@@ -35,10 +35,10 @@ struct AnthropicAuthControls: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Reveal") {
|
Button("Reveal") {
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([PiOAuthStore.oauthURL()])
|
NSWorkspace.shared.activateFileViewerSelecting([ClawdisOAuthStore.oauthURL()])
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.disabled(!FileManager.default.fileExists(atPath: PiOAuthStore.oauthURL().path))
|
.disabled(!FileManager.default.fileExists(atPath: ClawdisOAuthStore.oauthURL().path))
|
||||||
|
|
||||||
Button("Refresh") {
|
Button("Refresh") {
|
||||||
self.refresh()
|
self.refresh()
|
||||||
@@ -46,7 +46,7 @@ struct AnthropicAuthControls: View {
|
|||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(PiOAuthStore.oauthURL().path)
|
Text(ClawdisOAuthStore.oauthURL().path)
|
||||||
.font(.caption.monospaced())
|
.font(.caption.monospaced())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -123,7 +123,11 @@ struct AnthropicAuthControls: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func refresh() {
|
private func refresh() {
|
||||||
self.oauthStatus = PiOAuthStore.anthropicOAuthStatus()
|
let imported = ClawdisOAuthStore.importLegacyAnthropicOAuthIfNeeded()
|
||||||
|
self.oauthStatus = ClawdisOAuthStore.anthropicOAuthStatus()
|
||||||
|
if imported != nil {
|
||||||
|
self.statusText = "Imported existing OAuth credentials."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startOAuth() {
|
private func startOAuth() {
|
||||||
@@ -161,11 +165,11 @@ struct AnthropicAuthControls: View {
|
|||||||
code: parsed.code,
|
code: parsed.code,
|
||||||
state: parsed.state,
|
state: parsed.state,
|
||||||
verifier: pkce.verifier)
|
verifier: pkce.verifier)
|
||||||
try PiOAuthStore.saveAnthropicOAuth(creds)
|
try ClawdisOAuthStore.saveAnthropicOAuth(creds)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
self.pkce = nil
|
self.pkce = nil
|
||||||
self.code = ""
|
self.code = ""
|
||||||
self.statusText = "Connected. Pi can now use Claude via OAuth."
|
self.statusText = "Connected. Clawdis can now use Claude via OAuth."
|
||||||
} catch {
|
} catch {
|
||||||
self.statusText = "OAuth failed: \(error.localizedDescription)"
|
self.statusText = "OAuth failed: \(error.localizedDescription)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ enum AnthropicAuthMode: Equatable {
|
|||||||
|
|
||||||
var shortLabel: String {
|
var shortLabel: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .oauthFile: "OAuth (Pi token file)"
|
case .oauthFile: "OAuth (Clawdis token file)"
|
||||||
case .oauthEnv: "OAuth (env var)"
|
case .oauthEnv: "OAuth (env var)"
|
||||||
case .apiKeyEnv: "API key (env var)"
|
case .apiKeyEnv: "API key (env var)"
|
||||||
case .missing: "Missing credentials"
|
case .missing: "Missing credentials"
|
||||||
@@ -36,7 +36,8 @@ enum AnthropicAuthMode: Equatable {
|
|||||||
enum AnthropicAuthResolver {
|
enum AnthropicAuthResolver {
|
||||||
static func resolve(
|
static func resolve(
|
||||||
environment: [String: String] = ProcessInfo.processInfo.environment,
|
environment: [String: String] = ProcessInfo.processInfo.environment,
|
||||||
oauthStatus: PiOAuthStore.AnthropicOAuthStatus = PiOAuthStore.anthropicOAuthStatus()) -> AnthropicAuthMode
|
oauthStatus: ClawdisOAuthStore.AnthropicOAuthStatus = ClawdisOAuthStore.anthropicOAuthStatus()
|
||||||
|
) -> AnthropicAuthMode
|
||||||
{
|
{
|
||||||
if oauthStatus.isConnected { return .oauthFile }
|
if oauthStatus.isConnected { return .oauthFile }
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ enum AnthropicOAuth {
|
|||||||
URLQueryItem(name: "scope", value: self.scopes),
|
URLQueryItem(name: "scope", value: self.scopes),
|
||||||
URLQueryItem(name: "code_challenge", value: pkce.challenge),
|
URLQueryItem(name: "code_challenge", value: pkce.challenge),
|
||||||
URLQueryItem(name: "code_challenge_method", value: "S256"),
|
URLQueryItem(name: "code_challenge_method", value: "S256"),
|
||||||
// Match Pi: state is the verifier.
|
// Match legacy flow: state is the verifier.
|
||||||
URLQueryItem(name: "state", value: pkce.verifier),
|
URLQueryItem(name: "state", value: pkce.verifier),
|
||||||
]
|
]
|
||||||
return components.url!
|
return components.url!
|
||||||
@@ -140,7 +141,7 @@ enum AnthropicOAuth {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match Pi: expiresAt = now + expires_in - 5 minutes.
|
// Match legacy flow: expiresAt = now + expires_in - 5 minutes.
|
||||||
let expiresAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
let expiresAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
||||||
+ Int64(expiresIn * 1000)
|
+ Int64(expiresIn * 1000)
|
||||||
- Int64(5 * 60 * 1000)
|
- Int64(5 * 60 * 1000)
|
||||||
@@ -150,10 +151,11 @@ enum AnthropicOAuth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PiOAuthStore {
|
enum ClawdisOAuthStore {
|
||||||
static let oauthFilename = "oauth.json"
|
static let oauthFilename = "oauth.json"
|
||||||
private static let providerKey = "anthropic"
|
private static let providerKey = "anthropic"
|
||||||
private static let piAgentDirEnv = "PI_CODING_AGENT_DIR"
|
private static let clawdisOAuthDirEnv = "CLAWDIS_OAUTH_DIR"
|
||||||
|
private static let legacyPiDirEnv = "PI_CODING_AGENT_DIR"
|
||||||
|
|
||||||
enum AnthropicOAuthStatus: Equatable {
|
enum AnthropicOAuthStatus: Equatable {
|
||||||
case missingFile
|
case missingFile
|
||||||
@@ -170,18 +172,18 @@ enum PiOAuthStore {
|
|||||||
|
|
||||||
var shortDescription: String {
|
var shortDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .missingFile: "Pi OAuth token file not found"
|
case .missingFile: "Clawdis OAuth token file not found"
|
||||||
case .unreadableFile: "Pi OAuth token file not readable"
|
case .unreadableFile: "Clawdis OAuth token file not readable"
|
||||||
case .invalidJSON: "Pi OAuth token file invalid"
|
case .invalidJSON: "Clawdis OAuth token file invalid"
|
||||||
case .missingProviderEntry: "No Anthropic entry in Pi OAuth token file"
|
case .missingProviderEntry: "No Anthropic entry in Clawdis OAuth token file"
|
||||||
case .missingTokens: "Anthropic entry missing tokens"
|
case .missingTokens: "Anthropic entry missing tokens"
|
||||||
case .connected: "Pi OAuth credentials found"
|
case .connected: "Clawdis OAuth credentials found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func oauthDir() -> URL {
|
static func oauthDir() -> URL {
|
||||||
if let override = ProcessInfo.processInfo.environment[self.piAgentDirEnv]?
|
if let override = ProcessInfo.processInfo.environment[self.clawdisOAuthDirEnv]?
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines),
|
.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
!override.isEmpty
|
!override.isEmpty
|
||||||
{
|
{
|
||||||
@@ -190,14 +192,58 @@ enum PiOAuthStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return FileManager.default.homeDirectoryForCurrentUser
|
return FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".pi", isDirectory: true)
|
.appendingPathComponent(".clawdis", isDirectory: true)
|
||||||
.appendingPathComponent("agent", isDirectory: true)
|
.appendingPathComponent("credentials", isDirectory: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func oauthURL() -> URL {
|
static func oauthURL() -> URL {
|
||||||
self.oauthDir().appendingPathComponent(self.oauthFilename)
|
self.oauthDir().appendingPathComponent(self.oauthFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func legacyOAuthURLs() -> [URL] {
|
||||||
|
var urls: [URL] = []
|
||||||
|
let env = ProcessInfo.processInfo.environment
|
||||||
|
if let override = env[self.legacyPiDirEnv]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!override.isEmpty
|
||||||
|
{
|
||||||
|
let expanded = NSString(string: override).expandingTildeInPath
|
||||||
|
urls.append(URL(fileURLWithPath: expanded, isDirectory: true).appendingPathComponent(self.oauthFilename))
|
||||||
|
}
|
||||||
|
|
||||||
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
urls.append(home.appendingPathComponent(".pi/agent/\(self.oauthFilename)"))
|
||||||
|
urls.append(home.appendingPathComponent(".claude/\(self.oauthFilename)"))
|
||||||
|
urls.append(home.appendingPathComponent(".config/claude/\(self.oauthFilename)"))
|
||||||
|
urls.append(home.appendingPathComponent(".config/anthropic/\(self.oauthFilename)"))
|
||||||
|
|
||||||
|
var seen = Set<String>()
|
||||||
|
return urls.filter { url in
|
||||||
|
let path = url.standardizedFileURL.path
|
||||||
|
if seen.contains(path) { return false }
|
||||||
|
seen.insert(path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func importLegacyAnthropicOAuthIfNeeded() -> URL? {
|
||||||
|
let dest = self.oauthURL()
|
||||||
|
guard !FileManager.default.fileExists(atPath: dest.path) else { return nil }
|
||||||
|
|
||||||
|
for url in self.legacyOAuthURLs() {
|
||||||
|
guard FileManager.default.fileExists(atPath: url.path) else { continue }
|
||||||
|
guard self.anthropicOAuthStatus(at: url).isConnected else { continue }
|
||||||
|
guard let storage = self.loadStorage(at: url) else { continue }
|
||||||
|
do {
|
||||||
|
try self.saveStorage(storage)
|
||||||
|
return url
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
static func anthropicOAuthStatus() -> AnthropicOAuthStatus {
|
static func anthropicOAuthStatus() -> AnthropicOAuthStatus {
|
||||||
self.anthropicOAuthStatus(at: self.oauthURL())
|
self.anthropicOAuthStatus(at: self.oauthURL())
|
||||||
}
|
}
|
||||||
@@ -240,17 +286,15 @@ enum PiOAuthStore {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func loadStorage(at url: URL) -> [String: Any]? {
|
||||||
|
guard let data = try? Data(contentsOf: url) else { return nil }
|
||||||
|
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return nil }
|
||||||
|
return json as? [String: Any]
|
||||||
|
}
|
||||||
|
|
||||||
static func saveAnthropicOAuth(_ creds: AnthropicOAuthCredentials) throws {
|
static func saveAnthropicOAuth(_ creds: AnthropicOAuthCredentials) throws {
|
||||||
let url = self.oauthURL()
|
let url = self.oauthURL()
|
||||||
let existing: [String: Any] = if FileManager.default.fileExists(atPath: url.path),
|
let existing: [String: Any] = self.loadStorage(at: url) ?? [:]
|
||||||
let data = try? Data(contentsOf: url),
|
|
||||||
let json = try? JSONSerialization.jsonObject(with: data, options: []),
|
|
||||||
let dict = json as? [String: Any]
|
|
||||||
{
|
|
||||||
dict
|
|
||||||
} else {
|
|
||||||
[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
var updated = existing
|
var updated = existing
|
||||||
updated[self.providerKey] = [
|
updated[self.providerKey] = [
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ struct ConfigSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var anthropicAuthHelpText: String {
|
private var anthropicAuthHelpText: String {
|
||||||
"Determined from Pi OAuth token file (~/.pi/agent/oauth.json) " +
|
"Determined from Clawdis OAuth token file (~/.clawdis/credentials/oauth.json) " +
|
||||||
"or environment variables (ANTHROPIC_OAUTH_TOKEN / ANTHROPIC_API_KEY)."
|
"or environment variables (ANTHROPIC_OAUTH_TOKEN / ANTHROPIC_API_KEY)."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ struct OnboardingView: View {
|
|||||||
@State private var anthropicAuthStatus: String?
|
@State private var anthropicAuthStatus: String?
|
||||||
@State private var anthropicAuthBusy = false
|
@State private var anthropicAuthBusy = false
|
||||||
@State private var anthropicAuthConnected = false
|
@State private var anthropicAuthConnected = false
|
||||||
@State private var anthropicAuthDetectedStatus: PiOAuthStore.AnthropicOAuthStatus = .missingFile
|
@State private var anthropicAuthDetectedStatus: ClawdisOAuthStore.AnthropicOAuthStatus = .missingFile
|
||||||
@State private var anthropicAuthAutoDetectClipboard = true
|
@State private var anthropicAuthAutoDetectClipboard = true
|
||||||
@State private var anthropicAuthAutoConnectClipboard = true
|
@State private var anthropicAuthAutoConnectClipboard = true
|
||||||
@State private var anthropicAuthLastPasteboardChangeCount = NSPasteboard.general.changeCount
|
@State private var anthropicAuthLastPasteboardChangeCount = NSPasteboard.general.changeCount
|
||||||
@@ -530,7 +530,7 @@ struct OnboardingView: View {
|
|||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.frame(maxWidth: 540)
|
.frame(maxWidth: 540)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
Text("Pi supports any model — we strongly recommend Opus 4.5 for the best experience.")
|
Text("Clawdis supports any model — we strongly recommend Opus 4.5 for the best experience.")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@@ -555,14 +555,14 @@ struct OnboardingView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"This lets Pi use Claude immediately. Credentials are stored at " +
|
"This lets Clawdis use Claude immediately. Credentials are stored at " +
|
||||||
"`~/.pi/agent/oauth.json` (owner-only). You can redo this anytime.")
|
"`~/.clawdis/credentials/oauth.json` (owner-only). You can redo this anytime.")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Text(PiOAuthStore.oauthURL().path)
|
Text(ClawdisOAuthStore.oauthURL().path)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -571,7 +571,7 @@ struct OnboardingView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button("Reveal") {
|
Button("Reveal") {
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([PiOAuthStore.oauthURL()])
|
NSWorkspace.shared.activateFileViewerSelecting([ClawdisOAuthStore.oauthURL()])
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
@@ -683,9 +683,9 @@ struct OnboardingView: View {
|
|||||||
code: parsed.code,
|
code: parsed.code,
|
||||||
state: parsed.state,
|
state: parsed.state,
|
||||||
verifier: pkce.verifier)
|
verifier: pkce.verifier)
|
||||||
try PiOAuthStore.saveAnthropicOAuth(creds)
|
try ClawdisOAuthStore.saveAnthropicOAuth(creds)
|
||||||
self.refreshAnthropicOAuthStatus()
|
self.refreshAnthropicOAuthStatus()
|
||||||
self.anthropicAuthStatus = "Connected. Pi can now use Claude."
|
self.anthropicAuthStatus = "Connected. Clawdis can now use Claude."
|
||||||
} catch {
|
} catch {
|
||||||
self.anthropicAuthStatus = "OAuth failed: \(error.localizedDescription)"
|
self.anthropicAuthStatus = "OAuth failed: \(error.localizedDescription)"
|
||||||
}
|
}
|
||||||
@@ -717,7 +717,8 @@ struct OnboardingView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func refreshAnthropicOAuthStatus() {
|
private func refreshAnthropicOAuthStatus() {
|
||||||
let status = PiOAuthStore.anthropicOAuthStatus()
|
_ = ClawdisOAuthStore.importLegacyAnthropicOAuthIfNeeded()
|
||||||
|
let status = ClawdisOAuthStore.anthropicOAuthStatus()
|
||||||
self.anthropicAuthDetectedStatus = status
|
self.anthropicAuthDetectedStatus = status
|
||||||
self.anthropicAuthConnected = status.isConnected
|
self.anthropicAuthConnected = status.isConnected
|
||||||
}
|
}
|
||||||
@@ -947,8 +948,8 @@ struct OnboardingView: View {
|
|||||||
self.featureRow(
|
self.featureRow(
|
||||||
title: "Remote gateway checklist",
|
title: "Remote gateway checklist",
|
||||||
subtitle: """
|
subtitle: """
|
||||||
On your gateway host: install/update the `clawdis` package and make sure Pi has credentials
|
On your gateway host: install/update the `clawdis` package and make sure credentials exist
|
||||||
(typically `~/.pi/agent/oauth.json`). Then connect again if needed.
|
(typically `~/.clawdis/credentials/oauth.json`). Then connect again if needed.
|
||||||
""",
|
""",
|
||||||
systemImage: "network")
|
systemImage: "network")
|
||||||
Divider()
|
Divider()
|
||||||
|
|||||||
@@ -83,8 +83,6 @@ enum SessionActions {
|
|||||||
}
|
}
|
||||||
let home = FileManager.default.homeDirectoryForCurrentUser
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||||
urls.append(home.appendingPathComponent(".clawdis/sessions/\(sessionId).jsonl"))
|
urls.append(home.appendingPathComponent(".clawdis/sessions/\(sessionId).jsonl"))
|
||||||
urls.append(home.appendingPathComponent(".pi/agent/sessions/\(sessionId).jsonl"))
|
|
||||||
urls.append(home.appendingPathComponent(".tau/agent/sessions/clawdis/\(sessionId).jsonl"))
|
|
||||||
return urls
|
return urls
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Testing
|
|||||||
struct AnthropicAuthResolverTests {
|
struct AnthropicAuthResolverTests {
|
||||||
@Test
|
@Test
|
||||||
func prefersOAuthFileOverEnv() throws {
|
func prefersOAuthFileOverEnv() throws {
|
||||||
let key = "PI_CODING_AGENT_DIR"
|
let key = "CLAWDIS_OAUTH_DIR"
|
||||||
let previous = ProcessInfo.processInfo.environment[key]
|
let previous = ProcessInfo.processInfo.environment[key]
|
||||||
defer {
|
defer {
|
||||||
if let previous {
|
if let previous {
|
||||||
@@ -61,4 +61,3 @@ struct AnthropicAuthResolverTests {
|
|||||||
#expect(mode == .missing)
|
#expect(mode == .missing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import Testing
|
|||||||
@testable import Clawdis
|
@testable import Clawdis
|
||||||
|
|
||||||
@Suite
|
@Suite
|
||||||
struct PiOAuthStoreTests {
|
struct ClawdisOAuthStoreTests {
|
||||||
@Test
|
@Test
|
||||||
func returnsMissingWhenFileAbsent() {
|
func returnsMissingWhenFileAbsent() {
|
||||||
let url = FileManager.default.temporaryDirectory
|
let url = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdis-oauth-\(UUID().uuidString)")
|
.appendingPathComponent("clawdis-oauth-\(UUID().uuidString)")
|
||||||
.appendingPathComponent("oauth.json")
|
.appendingPathComponent("oauth.json")
|
||||||
#expect(PiOAuthStore.anthropicOAuthStatus(at: url) == .missingFile)
|
#expect(ClawdisOAuthStore.anthropicOAuthStatus(at: url) == .missingFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func usesEnvOverrideForPiAgentDir() throws {
|
func usesEnvOverrideForClawdisOAuthDir() throws {
|
||||||
let key = "PI_CODING_AGENT_DIR"
|
let key = "CLAWDIS_OAUTH_DIR"
|
||||||
let previous = ProcessInfo.processInfo.environment[key]
|
let previous = ProcessInfo.processInfo.environment[key]
|
||||||
defer {
|
defer {
|
||||||
if let previous {
|
if let previous {
|
||||||
@@ -25,10 +25,10 @@ struct PiOAuthStoreTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dir = FileManager.default.temporaryDirectory
|
let dir = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdis-pi-agent-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("clawdis-oauth-\(UUID().uuidString)", isDirectory: true)
|
||||||
setenv(key, dir.path, 1)
|
setenv(key, dir.path, 1)
|
||||||
|
|
||||||
#expect(PiOAuthStore.oauthDir().standardizedFileURL == dir.standardizedFileURL)
|
#expect(ClawdisOAuthStore.oauthDir().standardizedFileURL == dir.standardizedFileURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -42,7 +42,7 @@ struct PiOAuthStoreTests {
|
|||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
#expect(PiOAuthStore.anthropicOAuthStatus(at: url).isConnected)
|
#expect(ClawdisOAuthStore.anthropicOAuthStatus(at: url).isConnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -55,7 +55,7 @@ struct PiOAuthStoreTests {
|
|||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
#expect(PiOAuthStore.anthropicOAuthStatus(at: url).isConnected)
|
#expect(ClawdisOAuthStore.anthropicOAuthStatus(at: url).isConnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -68,7 +68,7 @@ struct PiOAuthStoreTests {
|
|||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
#expect(PiOAuthStore.anthropicOAuthStatus(at: url) == .missingProviderEntry)
|
#expect(ClawdisOAuthStore.anthropicOAuthStatus(at: url) == .missingProviderEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -81,7 +81,7 @@ struct PiOAuthStoreTests {
|
|||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
#expect(PiOAuthStore.anthropicOAuthStatus(at: url) == .missingTokens)
|
#expect(ClawdisOAuthStore.anthropicOAuthStatus(at: url) == .missingTokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func writeOAuthFile(_ json: [String: Any]) throws -> URL {
|
private func writeOAuthFile(_ json: [String: Any]) throws -> URL {
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
summary: "Agent runtime (embedded Pi), workspace contract, and session bootstrap"
|
summary: "Agent runtime (embedded p-mono), workspace contract, and session bootstrap"
|
||||||
read_when:
|
read_when:
|
||||||
- Changing agent runtime, workspace bootstrap, or session behavior
|
- Changing agent runtime, workspace bootstrap, or session behavior
|
||||||
---
|
---
|
||||||
<!-- {% raw %} -->
|
<!-- {% raw %} -->
|
||||||
# Agent Runtime 🤖
|
# Agent Runtime 🤖
|
||||||
|
|
||||||
CLAWDIS runs a single agent runtime: **Pi (embedded, in-process)**.
|
CLAWDIS runs a single embedded agent runtime derived from **p-mono** (internal name: **p**).
|
||||||
|
|
||||||
## Workspace (required)
|
## Workspace (required)
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ If a file is missing, CLAWDIS injects a single “missing file” marker line (a
|
|||||||
|
|
||||||
## Built-in tools (internal)
|
## Built-in tools (internal)
|
||||||
|
|
||||||
Pi’s embedded core tools (read/bash/edit/write and related internals) are defined in code and always available. `TOOLS.md` does **not** control which tools exist; it’s guidance for how *you* want them used.
|
p’s embedded core tools (read/bash/edit/write and related internals) are defined in code and always available. `TOOLS.md` does **not** control which tools exist; it’s guidance for how *you* want them used.
|
||||||
|
|
||||||
## Skills
|
## Skills
|
||||||
|
|
||||||
@@ -41,11 +41,12 @@ Clawdis loads skills from three locations (workspace wins on name conflict):
|
|||||||
|
|
||||||
Skills can be gated by config/env (see `skills.*` in `docs/configuration.md`).
|
Skills can be gated by config/env (see `skills.*` in `docs/configuration.md`).
|
||||||
|
|
||||||
## SDK integration
|
## p-mono integration
|
||||||
|
|
||||||
The embedded agent uses the `@mariozechner/pi-coding-agent` SDK for sessions and discovery.
|
Clawdis reuses pieces of the p-mono codebase (models/tools), but **session management, discovery, and tool wiring are Clawdis-owned**.
|
||||||
- Hooks, custom tools, and slash commands are discovered via the SDK (from `~/.pi/agent` and `<workspace>/.pi` settings).
|
|
||||||
- Bootstrap files are injected as SDK project context (see “Project Context” in the system prompt).
|
- No p-coding agent runtime.
|
||||||
|
- No `~/.pi/agent` or `<workspace>/.pi` settings are consulted.
|
||||||
|
|
||||||
## Peter @ steipete (only)
|
## Peter @ steipete (only)
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ Session transcripts are stored as JSONL at:
|
|||||||
- `~/.clawdis/sessions/<SessionId>.jsonl`
|
- `~/.clawdis/sessions/<SessionId>.jsonl`
|
||||||
|
|
||||||
The session ID is stable and chosen by CLAWDIS.
|
The session ID is stable and chosen by CLAWDIS.
|
||||||
|
Legacy Pi/Tau session folders are **not** read.
|
||||||
|
|
||||||
## Steering while streaming
|
## Steering while streaming
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ App bundle layout:
|
|||||||
- `clawdis …` (CLI)
|
- `clawdis …` (CLI)
|
||||||
- `clawdis gateway-daemon …` (LaunchAgent daemon)
|
- `clawdis gateway-daemon …` (LaunchAgent daemon)
|
||||||
- `Clawdis.app/Contents/Resources/Relay/package.json`
|
- `Clawdis.app/Contents/Resources/Relay/package.json`
|
||||||
- tiny “Pi compatibility” file (see below)
|
- tiny “p runtime compatibility” file (see below)
|
||||||
- `Clawdis.app/Contents/Resources/Relay/theme/`
|
- `Clawdis.app/Contents/Resources/Relay/theme/`
|
||||||
- Pi TUI theme payload (optional, but strongly recommended)
|
- p TUI theme payload (optional, but strongly recommended)
|
||||||
|
|
||||||
Why the sidecar files matter:
|
Why the sidecar files matter:
|
||||||
- `@mariozechner/pi-coding-agent` detects “bun binary mode” and then looks for `package.json` + `theme/` **next to `process.execPath`** (i.e. next to `clawdis`).
|
- The embedded p runtime detects “bun binary mode” and then looks for `package.json` + `theme/` **next to `process.execPath`** (i.e. next to `clawdis`).
|
||||||
- So even if bun can embed assets, Pi currently expects filesystem paths. Keep the sidecar files.
|
- So even if bun can embed assets, the runtime expects filesystem paths. Keep the sidecar files.
|
||||||
|
|
||||||
## Build pipeline
|
## Build pipeline
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
summary: "Planned first-run onboarding flow for Clawdis (local vs remote, Anthropic OAuth, workspace bootstrap ritual)"
|
summary: "Planned first-run onboarding flow for Clawdis (local vs remote, Anthropic OAuth, workspace bootstrap ritual)"
|
||||||
read_when:
|
read_when:
|
||||||
- Designing the macOS onboarding assistant
|
- Designing the macOS onboarding assistant
|
||||||
- Implementing Pi authentication or identity setup
|
- Implementing Anthropic auth or identity setup
|
||||||
---
|
---
|
||||||
<!-- {% raw %} -->
|
<!-- {% raw %} -->
|
||||||
# Onboarding (macOS app)
|
# Onboarding (macOS app)
|
||||||
|
|
||||||
This doc describes the intended **first-run onboarding** for Clawdis. The goal is a good “day 0” experience: pick where the Gateway runs, bind Claude (Anthropic) auth for Pi, and then let the **agent bootstrap itself** via a first-run ritual in the workspace.
|
This doc describes the intended **first-run onboarding** for Clawdis. The goal is a good “day 0” experience: pick where the Gateway runs, bind Claude (Anthropic) auth for the embedded agent runtime, and then let the **agent bootstrap itself** via a first-run ritual in the workspace.
|
||||||
|
|
||||||
## Page order (high level)
|
## Page order (high level)
|
||||||
|
|
||||||
@@ -19,58 +19,42 @@ This doc describes the intended **first-run onboarding** for Clawdis. The goal i
|
|||||||
|
|
||||||
First question: where does the **Gateway** run?
|
First question: where does the **Gateway** run?
|
||||||
|
|
||||||
- **Local (this Mac):** onboarding can run the Anthropic OAuth flow and write Pi’s token store locally.
|
- **Local (this Mac):** onboarding can run the Anthropic OAuth flow and write the Clawdis token store locally.
|
||||||
- **Remote (over SSH/tailnet):** onboarding must not run OAuth locally, because credentials must exist on the **gateway host**.
|
- **Remote (over SSH/tailnet):** onboarding must not run OAuth locally, because credentials must exist on the **gateway host**.
|
||||||
|
|
||||||
Implementation note (2025-12-19): in local mode, the macOS app bundles the Gateway and enables it via a per-user launchd LaunchAgent (no global npm install/Node requirement for the user).
|
Implementation note (2025-12-19): in local mode, the macOS app bundles the Gateway and enables it via a per-user launchd LaunchAgent (no global npm install/Node requirement for the user).
|
||||||
|
|
||||||
## 2) Local-only: Connect Claude (Anthropic OAuth)
|
## 2) Local-only: Connect Claude (Anthropic OAuth)
|
||||||
|
|
||||||
This is the “bind Pi to Clawdis” step. It is explicitly the **Anthropic (Claude Pro/Max) OAuth flow**, not a generic “login”.
|
This is the “bind Clawdis to Anthropic” step. It is explicitly the **Anthropic (Claude Pro/Max) OAuth flow**, not a generic “login”.
|
||||||
|
|
||||||
### Recommended: OAuth
|
### Recommended: OAuth
|
||||||
|
|
||||||
The macOS app should:
|
The macOS app should:
|
||||||
- Start the Anthropic OAuth (PKCE) flow in the user’s browser.
|
- Start the Anthropic OAuth (PKCE) flow in the user’s browser.
|
||||||
- Ask the user to paste the `code#state` value.
|
- Ask the user to paste the `code#state` value.
|
||||||
- Exchange it for tokens and write Pi-compatible credentials to:
|
- Exchange it for tokens and write credentials to:
|
||||||
- `~/.pi/agent/oauth.json` (file mode `0600`, directory mode `0700`)
|
- `~/.clawdis/credentials/oauth.json` (file mode `0600`, directory mode `0700`)
|
||||||
|
|
||||||
Why this location matters: it makes Pi work immediately (Clawdis doesn’t need a terminal and doesn’t need to re-implement Pi’s auth plumbing later).
|
Why this location matters: it’s the Clawdis-owned OAuth store.
|
||||||
|
On first run, Clawdis can import existing OAuth tokens from legacy p/Claude locations if present.
|
||||||
|
|
||||||
### Alternative: API key (instructions only)
|
### Alternative: API key (instructions only)
|
||||||
|
|
||||||
Offer an “API key” option, but for now it is **instructions only**:
|
Offer an “API key” option, but for now it is **instructions only**:
|
||||||
- Get an Anthropic API key.
|
- Get an Anthropic API key.
|
||||||
- Provide it to Pi (or to Clawdis’s Pi invocation) via your preferred mechanism.
|
- Provide it to Clawdis via your preferred mechanism (env/config).
|
||||||
|
|
||||||
Note: environment variables are often confusing when the Gateway is launched by a GUI app (launchd environment != your shell).
|
Note: environment variables are often confusing when the Gateway is launched by a GUI app (launchd environment != your shell).
|
||||||
|
|
||||||
### Provider/model safety rule
|
### Provider/model safety rule
|
||||||
|
|
||||||
Clawdis should **always pass** `--provider` and `--model` when invoking Pi (don’t rely on Pi defaults).
|
Clawdis should **always pass** `--provider` and `--model` when invoking the embedded agent (don’t rely on defaults).
|
||||||
|
|
||||||
Until that is hard-coded, the equivalent configuration is:
|
Example (CLI):
|
||||||
|
|
||||||
```json5
|
```bash
|
||||||
{
|
clawdis agent --mode rpc --provider anthropic --model claude-opus-4-5 "<message>"
|
||||||
inbound: {
|
|
||||||
reply: {
|
|
||||||
mode: "command",
|
|
||||||
command: [
|
|
||||||
"pi",
|
|
||||||
"--mode",
|
|
||||||
"rpc",
|
|
||||||
"--provider",
|
|
||||||
"anthropic",
|
|
||||||
"--model",
|
|
||||||
"claude-opus-4-5",
|
|
||||||
"{{BodyStripped}}"
|
|
||||||
],
|
|
||||||
agent: { kind: "pi", format: "json" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If the user skips auth, onboarding should be clear: the agent likely won’t respond until auth is configured.
|
If the user skips auth, onboarding should be clear: the agent likely won’t respond until auth is configured.
|
||||||
@@ -136,10 +120,10 @@ Daily memory lives under `memory/` in the workspace:
|
|||||||
|
|
||||||
## Remote mode note (why OAuth is hidden)
|
## Remote mode note (why OAuth is hidden)
|
||||||
|
|
||||||
If the Gateway runs on another machine, the Anthropic OAuth credentials must be created/stored on that host (where Pi runs).
|
If the Gateway runs on another machine, the Anthropic OAuth credentials must be created/stored on that host (where the agent runtime runs).
|
||||||
|
|
||||||
For now, remote onboarding should:
|
For now, remote onboarding should:
|
||||||
- explain why OAuth isn’t shown
|
- explain why OAuth isn’t shown
|
||||||
- point the user at the credential location (`~/.pi/agent/oauth.json`) and the workspace location on the gateway host
|
- point the user at the credential location (`~/.clawdis/credentials/oauth.json`) and the workspace location on the gateway host
|
||||||
- mention that the **bootstrap ritual happens on the gateway host** (same BOOTSTRAP/IDENTITY/USER files)
|
- mention that the **bootstrap ritual happens on the gateway host** (same BOOTSTRAP/IDENTITY/USER files)
|
||||||
<!-- {% endraw %} -->
|
<!-- {% endraw %} -->
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ All session state is **owned by the gateway** (the “master” Clawdis). UI cli
|
|||||||
- Store file: `~/.clawdis/sessions/sessions.json` (legacy: `~/.clawdis/sessions.json`).
|
- Store file: `~/.clawdis/sessions/sessions.json` (legacy: `~/.clawdis/sessions.json`).
|
||||||
- Transcripts: `~/.clawdis/sessions/<SessionId>.jsonl` (one file per session id).
|
- Transcripts: `~/.clawdis/sessions/<SessionId>.jsonl` (one file per session id).
|
||||||
- The store is a map `sessionKey -> { sessionId, updatedAt, ... }`. Deleting entries is safe; they are recreated on demand.
|
- The store is a map `sessionKey -> { sessionId, updatedAt, ... }`. Deleting entries is safe; they are recreated on demand.
|
||||||
|
- Clawdis does **not** read legacy Pi/Tau session folders.
|
||||||
|
|
||||||
## Mapping transports → session keys
|
## Mapping transports → session keys
|
||||||
- Direct chats (WhatsApp, Telegram, desktop Web Chat) all collapse to the **primary key** so they share context.
|
- Direct chats (WhatsApp, Telegram, desktop Web Chat) all collapse to the **primary key** so they share context.
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import fsSync from "node:fs";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import type { AppMessage, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
import type { AppMessage, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||||
import type { Api, AssistantMessage, Model } from "@mariozechner/pi-ai";
|
import {
|
||||||
|
setOAuthStorage,
|
||||||
|
type Api,
|
||||||
|
type AssistantMessage,
|
||||||
|
type Model,
|
||||||
|
type OAuthStorage,
|
||||||
|
} from "@mariozechner/pi-ai";
|
||||||
import {
|
import {
|
||||||
buildSystemPrompt,
|
buildSystemPrompt,
|
||||||
createAgentSession,
|
createAgentSession,
|
||||||
@@ -18,7 +25,7 @@ import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
|||||||
import type { ClawdisConfig } from "../config/config.js";
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
import { splitMediaFromOutput } from "../media/parse.js";
|
import { splitMediaFromOutput } from "../media/parse.js";
|
||||||
import { enqueueCommand } from "../process/command-queue.js";
|
import { enqueueCommand } from "../process/command-queue.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||||
import {
|
import {
|
||||||
buildBootstrapContextFiles,
|
buildBootstrapContextFiles,
|
||||||
@@ -75,6 +82,109 @@ type EmbeddedPiQueueHandle = {
|
|||||||
|
|
||||||
const ACTIVE_EMBEDDED_RUNS = new Map<string, EmbeddedPiQueueHandle>();
|
const ACTIVE_EMBEDDED_RUNS = new Map<string, EmbeddedPiQueueHandle>();
|
||||||
|
|
||||||
|
const OAUTH_FILENAME = "oauth.json";
|
||||||
|
const DEFAULT_OAUTH_DIR = path.join(CONFIG_DIR, "credentials");
|
||||||
|
const DEFAULT_AGENT_DIR = path.join(CONFIG_DIR, "agent");
|
||||||
|
let oauthStorageConfigured = false;
|
||||||
|
let cachedDefaultApiKey: ReturnType<typeof defaultGetApiKey> | null = null;
|
||||||
|
|
||||||
|
function resolveClawdisOAuthPath(): string {
|
||||||
|
const overrideDir =
|
||||||
|
process.env.CLAWDIS_OAUTH_DIR?.trim() || DEFAULT_OAUTH_DIR;
|
||||||
|
return path.join(resolveUserPath(overrideDir), OAUTH_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAgentDir(): string {
|
||||||
|
const override =
|
||||||
|
process.env.CLAWDIS_AGENT_DIR?.trim() ||
|
||||||
|
process.env.PI_CODING_AGENT_DIR?.trim() ||
|
||||||
|
DEFAULT_AGENT_DIR;
|
||||||
|
return resolveUserPath(override);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadOAuthStorageAt(pathname: string): OAuthStorage | null {
|
||||||
|
if (!fsSync.existsSync(pathname)) return null;
|
||||||
|
try {
|
||||||
|
const content = fsSync.readFileSync(pathname, "utf8");
|
||||||
|
const json = JSON.parse(content) as OAuthStorage;
|
||||||
|
if (!json || typeof json !== "object") return null;
|
||||||
|
return json;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAnthropicOAuth(storage: OAuthStorage): boolean {
|
||||||
|
const entry = storage.anthropic as
|
||||||
|
| {
|
||||||
|
refresh?: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
access?: string;
|
||||||
|
access_token?: string;
|
||||||
|
accessToken?: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
if (!entry) return false;
|
||||||
|
const refresh =
|
||||||
|
entry.refresh ?? entry.refresh_token ?? entry.refreshToken ?? "";
|
||||||
|
const access = entry.access ?? entry.access_token ?? entry.accessToken ?? "";
|
||||||
|
return Boolean(refresh.trim() && access.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveOAuthStorageAt(pathname: string, storage: OAuthStorage): void {
|
||||||
|
const dir = path.dirname(pathname);
|
||||||
|
fsSync.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||||
|
fsSync.writeFileSync(
|
||||||
|
pathname,
|
||||||
|
`${JSON.stringify(storage, null, 2)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
fsSync.chmodSync(pathname, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyOAuthPaths(): string[] {
|
||||||
|
const paths: string[] = [];
|
||||||
|
const piOverride = process.env.PI_CODING_AGENT_DIR?.trim();
|
||||||
|
if (piOverride) {
|
||||||
|
paths.push(path.join(resolveUserPath(piOverride), OAUTH_FILENAME));
|
||||||
|
}
|
||||||
|
paths.push(path.join(os.homedir(), ".pi", "agent", OAUTH_FILENAME));
|
||||||
|
paths.push(path.join(os.homedir(), ".claude", OAUTH_FILENAME));
|
||||||
|
paths.push(path.join(os.homedir(), ".config", "claude", OAUTH_FILENAME));
|
||||||
|
paths.push(path.join(os.homedir(), ".config", "anthropic", OAUTH_FILENAME));
|
||||||
|
return Array.from(new Set(paths));
|
||||||
|
}
|
||||||
|
|
||||||
|
function importLegacyOAuthIfNeeded(destPath: string): void {
|
||||||
|
if (fsSync.existsSync(destPath)) return;
|
||||||
|
for (const legacyPath of legacyOAuthPaths()) {
|
||||||
|
const storage = loadOAuthStorageAt(legacyPath);
|
||||||
|
if (!storage || !hasAnthropicOAuth(storage)) continue;
|
||||||
|
saveOAuthStorageAt(destPath, storage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureOAuthStorage(): void {
|
||||||
|
if (oauthStorageConfigured) return;
|
||||||
|
oauthStorageConfigured = true;
|
||||||
|
const oauthPath = resolveClawdisOAuthPath();
|
||||||
|
importLegacyOAuthIfNeeded(oauthPath);
|
||||||
|
setOAuthStorage({
|
||||||
|
load: () => loadOAuthStorageAt(oauthPath) ?? {},
|
||||||
|
save: (storage) => saveOAuthStorageAt(oauthPath, storage),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultApiKey() {
|
||||||
|
if (!cachedDefaultApiKey) {
|
||||||
|
ensureOAuthStorage();
|
||||||
|
cachedDefaultApiKey = defaultGetApiKey();
|
||||||
|
}
|
||||||
|
return cachedDefaultApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
export function queueEmbeddedPiMessage(
|
export function queueEmbeddedPiMessage(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
text: string,
|
text: string,
|
||||||
@@ -106,14 +216,13 @@ function resolveModel(
|
|||||||
return { model };
|
return { model };
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultApiKey = defaultGetApiKey();
|
|
||||||
|
|
||||||
async function getApiKeyForModel(model: Model<Api>): Promise<string> {
|
async function getApiKeyForModel(model: Model<Api>): Promise<string> {
|
||||||
|
ensureOAuthStorage();
|
||||||
if (model.provider === "anthropic") {
|
if (model.provider === "anthropic") {
|
||||||
const oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;
|
const oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;
|
||||||
if (oauthEnv?.trim()) return oauthEnv.trim();
|
if (oauthEnv?.trim()) return oauthEnv.trim();
|
||||||
}
|
}
|
||||||
const key = await defaultApiKey(model);
|
const key = await getDefaultApiKey()(model);
|
||||||
if (key) return key;
|
if (key) return key;
|
||||||
throw new Error(`No API key found for provider "${model.provider}"`);
|
throw new Error(`No API key found for provider "${model.provider}"`);
|
||||||
}
|
}
|
||||||
@@ -175,9 +284,10 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
const provider =
|
const provider =
|
||||||
(params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
(params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
||||||
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
||||||
const agentDir =
|
const agentDir = resolveAgentDir();
|
||||||
process.env.PI_CODING_AGENT_DIR ??
|
if (!process.env.PI_CODING_AGENT_DIR) {
|
||||||
path.join(os.homedir(), ".pi", "agent");
|
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||||
|
}
|
||||||
const { model, error } = resolveModel(provider, modelId, agentDir);
|
const { model, error } = resolveModel(provider, modelId, agentDir);
|
||||||
if (!model) {
|
if (!model) {
|
||||||
throw new Error(error ?? `Unknown model: ${provider}/${modelId}`);
|
throw new Error(error ?? `Unknown model: ${provider}/${modelId}`);
|
||||||
|
|||||||
@@ -720,8 +720,6 @@ function readSessionMessages(
|
|||||||
const parsed = JSON.parse(line);
|
const parsed = JSON.parse(line);
|
||||||
if (parsed?.message) {
|
if (parsed?.message) {
|
||||||
messages.push(parsed.message);
|
messages.push(parsed.message);
|
||||||
} else if (parsed?.role && parsed?.content) {
|
|
||||||
messages.push(parsed);
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore bad lines
|
// ignore bad lines
|
||||||
@@ -742,19 +740,6 @@ function resolveSessionTranscriptCandidates(
|
|||||||
candidates.push(
|
candidates.push(
|
||||||
path.join(os.homedir(), ".clawdis", "sessions", `${sessionId}.jsonl`),
|
path.join(os.homedir(), ".clawdis", "sessions", `${sessionId}.jsonl`),
|
||||||
);
|
);
|
||||||
candidates.push(
|
|
||||||
path.join(os.homedir(), ".pi", "agent", "sessions", `${sessionId}.jsonl`),
|
|
||||||
);
|
|
||||||
candidates.push(
|
|
||||||
path.join(
|
|
||||||
os.homedir(),
|
|
||||||
".tau",
|
|
||||||
"agent",
|
|
||||||
"sessions",
|
|
||||||
"clawdis",
|
|
||||||
`${sessionId}.jsonl`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1516,7 +1501,21 @@ export async function startGatewayServer(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logTelegram.info("starting provider");
|
let telegramBotLabel = "";
|
||||||
|
try {
|
||||||
|
const probe = await probeTelegram(
|
||||||
|
telegramToken.trim(),
|
||||||
|
2500,
|
||||||
|
cfg.telegram?.proxy,
|
||||||
|
);
|
||||||
|
const username = probe.ok ? probe.bot?.username?.trim() : null;
|
||||||
|
if (username) telegramBotLabel = ` (@${username})`;
|
||||||
|
} catch (err) {
|
||||||
|
if (isVerbose()) {
|
||||||
|
logTelegram.debug(`bot probe failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logTelegram.info(`starting provider${telegramBotLabel}`);
|
||||||
telegramAbort = new AbortController();
|
telegramAbort = new AbortController();
|
||||||
telegramRuntime = {
|
telegramRuntime = {
|
||||||
...telegramRuntime,
|
...telegramRuntime,
|
||||||
|
|||||||
Reference in New Issue
Block a user