@lattices/cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -9
- package/app/Info.plist +30 -0
- package/app/Lattices.app/Contents/Info.plist +8 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
- package/app/Lattices.entitlements +15 -0
- package/app/Package.swift +8 -1
- package/app/Resources/tap.wav +0 -0
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +45 -12
- package/app/Sources/AppShellView.swift +81 -8
- package/app/Sources/AudioProvider.swift +386 -0
- package/app/Sources/CheatSheetHUD.swift +261 -19
- package/app/Sources/DaemonProtocol.swift +13 -0
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DesktopModel.swift +189 -6
- package/app/Sources/DesktopModelTypes.swift +2 -0
- package/app/Sources/DiagnosticLog.swift +104 -2
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HUDBottomBar.swift +279 -0
- package/app/Sources/HUDController.swift +1158 -0
- package/app/Sources/HUDLeftBar.swift +849 -0
- package/app/Sources/HUDMinimap.swift +179 -0
- package/app/Sources/HUDRightBar.swift +774 -0
- package/app/Sources/HUDState.swift +367 -0
- package/app/Sources/HUDTopBar.swift +243 -0
- package/app/Sources/HandsOffSession.swift +802 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +49 -9
- package/app/Sources/IntentEngine.swift +962 -0
- package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
- package/app/Sources/Intents/DistributeIntent.swift +56 -0
- package/app/Sources/Intents/FocusIntent.swift +69 -0
- package/app/Sources/Intents/HelpIntent.swift +41 -0
- package/app/Sources/Intents/KillIntent.swift +47 -0
- package/app/Sources/Intents/LatticeIntent.swift +78 -0
- package/app/Sources/Intents/LaunchIntent.swift +67 -0
- package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
- package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
- package/app/Sources/Intents/ScanIntent.swift +52 -0
- package/app/Sources/Intents/SearchIntent.swift +190 -0
- package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
- package/app/Sources/Intents/TileIntent.swift +61 -0
- package/app/Sources/LatticesApi.swift +1275 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- package/app/Sources/MouseFinder.swift +222 -0
- package/app/Sources/OcrModel.swift +34 -1
- package/app/Sources/OmniSearchState.swift +99 -102
- package/app/Sources/OnboardingView.swift +457 -0
- package/app/Sources/PermissionChecker.swift +2 -12
- package/app/Sources/PiChatDock.swift +454 -0
- package/app/Sources/PiChatSession.swift +815 -0
- package/app/Sources/PiWorkspaceView.swift +364 -0
- package/app/Sources/PlacementSpec.swift +195 -0
- package/app/Sources/Preferences.swift +59 -0
- package/app/Sources/ProjectScanner.swift +58 -45
- package/app/Sources/ScreenMapState.swift +701 -55
- package/app/Sources/ScreenMapView.swift +843 -103
- package/app/Sources/ScreenMapWindowController.swift +22 -0
- package/app/Sources/SessionLayerStore.swift +285 -0
- package/app/Sources/SessionManager.swift +4 -1
- package/app/Sources/SettingsView.swift +186 -3
- package/app/Sources/Theme.swift +9 -8
- package/app/Sources/TmuxModel.swift +7 -0
- package/app/Sources/TmuxQuery.swift +27 -3
- package/app/Sources/VoiceChatView.swift +192 -0
- package/app/Sources/VoiceCommandWindow.swift +1594 -0
- package/app/Sources/VoiceIntentResolver.swift +671 -0
- package/app/Sources/VoxClient.swift +454 -0
- package/app/Sources/WindowTiler.swift +348 -87
- package/app/Sources/WorkspaceManager.swift +127 -18
- package/app/Tests/StageDragTests.swift +333 -0
- package/app/Tests/StageJoinTests.swift +313 -0
- package/app/Tests/StageManagerTests.swift +280 -0
- package/app/Tests/StageTileTests.swift +353 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/client.ts +16 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +280 -0
- package/bin/handsoff-worker.ts +740 -0
- package/bin/lattices-app.ts +338 -0
- package/bin/lattices-dev +208 -0
- package/bin/{lattices.js → lattices.ts} +777 -140
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +142 -0
- package/docs/api.md +153 -34
- package/docs/app.md +29 -1
- package/docs/config.md +5 -1
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/layers.md +20 -20
- package/docs/ocr.md +14 -5
- package/docs/overview.md +5 -1
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +374 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/tiling-reference.md +167 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice.md +219 -0
- package/package.json +29 -11
- package/bin/client.js +0 -4
- package/bin/lattices-app.js +0 -221
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
struct PiChatMessage: Identifiable, Equatable {
|
|
5
|
+
enum Role {
|
|
6
|
+
case system
|
|
7
|
+
case user
|
|
8
|
+
case assistant
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let id = UUID()
|
|
12
|
+
let role: Role
|
|
13
|
+
let text: String
|
|
14
|
+
let timestamp: Date
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
struct PiAuthPrompt: Equatable {
|
|
18
|
+
let message: String
|
|
19
|
+
let placeholder: String?
|
|
20
|
+
let allowEmpty: Bool
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
struct PiProvider: Identifiable, Equatable {
|
|
24
|
+
enum AuthMode {
|
|
25
|
+
case apiKey
|
|
26
|
+
case oauth
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let id: String
|
|
30
|
+
let name: String
|
|
31
|
+
let authMode: AuthMode
|
|
32
|
+
let tokenLabel: String
|
|
33
|
+
let tokenPlaceholder: String
|
|
34
|
+
let helpText: String
|
|
35
|
+
|
|
36
|
+
static let supported: [PiProvider] = [
|
|
37
|
+
PiProvider(
|
|
38
|
+
id: "github-copilot",
|
|
39
|
+
name: "GitHub Copilot",
|
|
40
|
+
authMode: .oauth,
|
|
41
|
+
tokenLabel: "OAuth",
|
|
42
|
+
tokenPlaceholder: "",
|
|
43
|
+
helpText: "Uses Pi's device-code login. Personal access tokens are not accepted on this path."
|
|
44
|
+
),
|
|
45
|
+
PiProvider(
|
|
46
|
+
id: "openai-codex",
|
|
47
|
+
name: "OpenAI Codex",
|
|
48
|
+
authMode: .oauth,
|
|
49
|
+
tokenLabel: "OAuth",
|
|
50
|
+
tokenPlaceholder: "",
|
|
51
|
+
helpText: "Uses Pi's browser login for ChatGPT Plus/Pro Codex access."
|
|
52
|
+
),
|
|
53
|
+
PiProvider(
|
|
54
|
+
id: "openai",
|
|
55
|
+
name: "OpenAI",
|
|
56
|
+
authMode: .apiKey,
|
|
57
|
+
tokenLabel: "API key",
|
|
58
|
+
tokenPlaceholder: "sk-...",
|
|
59
|
+
helpText: "Stores an OpenAI API key in Pi's auth.json for this app and Pi CLI to reuse."
|
|
60
|
+
),
|
|
61
|
+
PiProvider(
|
|
62
|
+
id: "anthropic",
|
|
63
|
+
name: "Anthropic",
|
|
64
|
+
authMode: .apiKey,
|
|
65
|
+
tokenLabel: "API key",
|
|
66
|
+
tokenPlaceholder: "sk-ant-...",
|
|
67
|
+
helpText: "Stores an Anthropic API key for Pi. OAuth-capable Anthropic flows can be added later."
|
|
68
|
+
),
|
|
69
|
+
PiProvider(
|
|
70
|
+
id: "google",
|
|
71
|
+
name: "Google Gemini",
|
|
72
|
+
authMode: .apiKey,
|
|
73
|
+
tokenLabel: "API key",
|
|
74
|
+
tokenPlaceholder: "AIza...",
|
|
75
|
+
helpText: "Stores a Gemini API key for Pi's Google provider."
|
|
76
|
+
),
|
|
77
|
+
PiProvider(
|
|
78
|
+
id: "openrouter",
|
|
79
|
+
name: "OpenRouter",
|
|
80
|
+
authMode: .apiKey,
|
|
81
|
+
tokenLabel: "API key",
|
|
82
|
+
tokenPlaceholder: "sk-or-...",
|
|
83
|
+
helpText: "Stores an OpenRouter API key for Pi."
|
|
84
|
+
),
|
|
85
|
+
PiProvider(
|
|
86
|
+
id: "groq",
|
|
87
|
+
name: "Groq",
|
|
88
|
+
authMode: .apiKey,
|
|
89
|
+
tokenLabel: "API key",
|
|
90
|
+
tokenPlaceholder: "gsk_...",
|
|
91
|
+
helpText: "Stores a Groq API key for Pi."
|
|
92
|
+
),
|
|
93
|
+
PiProvider(
|
|
94
|
+
id: "xai",
|
|
95
|
+
name: "xAI",
|
|
96
|
+
authMode: .apiKey,
|
|
97
|
+
tokenLabel: "API key",
|
|
98
|
+
tokenPlaceholder: "xai-...",
|
|
99
|
+
helpText: "Stores an xAI API key for Pi."
|
|
100
|
+
),
|
|
101
|
+
PiProvider(
|
|
102
|
+
id: "mistral",
|
|
103
|
+
name: "Mistral",
|
|
104
|
+
authMode: .apiKey,
|
|
105
|
+
tokenLabel: "API key",
|
|
106
|
+
tokenPlaceholder: "",
|
|
107
|
+
helpText: "Stores a Mistral API key for Pi."
|
|
108
|
+
),
|
|
109
|
+
PiProvider(
|
|
110
|
+
id: "minimax",
|
|
111
|
+
name: "MiniMax",
|
|
112
|
+
authMode: .apiKey,
|
|
113
|
+
tokenLabel: "API key",
|
|
114
|
+
tokenPlaceholder: "",
|
|
115
|
+
helpText: "Stores a MiniMax API key for Pi."
|
|
116
|
+
),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
static func provider(id: String) -> PiProvider {
|
|
120
|
+
supported.first(where: { $0.id == id }) ?? supported[0]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
final class PiChatSession: ObservableObject {
|
|
125
|
+
static let shared = PiChatSession()
|
|
126
|
+
|
|
127
|
+
@Published private(set) var messages: [PiChatMessage] = [
|
|
128
|
+
PiChatMessage(
|
|
129
|
+
role: .system,
|
|
130
|
+
text: "Pi dock ready. This is a lightweight in-app conversation surface, not a full terminal.",
|
|
131
|
+
timestamp: Date()
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
@Published var draft: String = ""
|
|
135
|
+
@Published var isVisible: Bool = false
|
|
136
|
+
@Published private(set) var isSending: Bool = false
|
|
137
|
+
@Published private(set) var statusText: String = "idle"
|
|
138
|
+
@Published var dockHeight: CGFloat = 230 {
|
|
139
|
+
didSet {
|
|
140
|
+
dockHeight = Self.clampDockHeight(dockHeight)
|
|
141
|
+
UserDefaults.standard.set(dockHeight, forKey: Self.dockHeightDefaultsKey)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
@Published var isAuthPanelVisible: Bool = false
|
|
145
|
+
@Published var authProviderID: String = "minimax" {
|
|
146
|
+
didSet {
|
|
147
|
+
guard oldValue != authProviderID else { return }
|
|
148
|
+
UserDefaults.standard.set(authProviderID, forKey: Self.selectedProviderDefaultsKey)
|
|
149
|
+
authToken = ""
|
|
150
|
+
authPromptInput = ""
|
|
151
|
+
pendingAuthPrompt = nil
|
|
152
|
+
authNoticeText = nil
|
|
153
|
+
authErrorText = nil
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
@Published var authToken: String = ""
|
|
157
|
+
@Published var authPromptInput: String = ""
|
|
158
|
+
@Published private(set) var isAuthenticating: Bool = false
|
|
159
|
+
@Published private(set) var pendingAuthPrompt: PiAuthPrompt?
|
|
160
|
+
@Published private(set) var authNoticeText: String?
|
|
161
|
+
@Published private(set) var authErrorText: String?
|
|
162
|
+
@Published private(set) var storedCredentialKinds: [String: String] = [:]
|
|
163
|
+
|
|
164
|
+
private let queue = DispatchQueue(label: "pi-chat-session", qos: .userInitiated)
|
|
165
|
+
private let sessionFileURL: URL
|
|
166
|
+
private let authFileURL: URL
|
|
167
|
+
private var authProcess: Process?
|
|
168
|
+
private var authInputHandle: FileHandle?
|
|
169
|
+
private var authStdoutBuffer: String = ""
|
|
170
|
+
private var authStderrBuffer: String = ""
|
|
171
|
+
|
|
172
|
+
private static let selectedProviderDefaultsKey = "PiChatSelectedProvider"
|
|
173
|
+
private static let dockHeightDefaultsKey = "PiChatDockHeight"
|
|
174
|
+
|
|
175
|
+
private init() {
|
|
176
|
+
let fm = FileManager.default
|
|
177
|
+
let base = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
|
|
178
|
+
?? URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Application Support")
|
|
179
|
+
let dir = base.appendingPathComponent("Lattices/pi-chat", isDirectory: true)
|
|
180
|
+
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
181
|
+
sessionFileURL = dir.appendingPathComponent("session.jsonl")
|
|
182
|
+
authFileURL = Self.piAgentDirURL().appendingPathComponent("auth.json")
|
|
183
|
+
|
|
184
|
+
if let savedProvider = UserDefaults.standard.string(forKey: Self.selectedProviderDefaultsKey),
|
|
185
|
+
PiProvider.supported.contains(where: { $0.id == savedProvider }) {
|
|
186
|
+
authProviderID = savedProvider
|
|
187
|
+
}
|
|
188
|
+
let savedDockHeight = UserDefaults.standard.double(forKey: Self.dockHeightDefaultsKey)
|
|
189
|
+
if savedDockHeight > 0 {
|
|
190
|
+
dockHeight = Self.clampDockHeight(savedDockHeight)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
reloadAuthState()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
var hasPiBinary: Bool {
|
|
197
|
+
resolvePiPath() != nil
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
var providerOptions: [PiProvider] {
|
|
201
|
+
PiProvider.supported
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var currentProvider: PiProvider {
|
|
205
|
+
PiProvider.provider(id: authProviderID)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var selectedCredentialSummary: String {
|
|
209
|
+
guard let kind = storedCredentialKinds[authProviderID] else { return "not authenticated" }
|
|
210
|
+
return kind == "oauth" ? "oauth saved" : "token saved"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
var hasSelectedCredential: Bool {
|
|
214
|
+
storedCredentialKinds[authProviderID] != nil
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
var canSubmitAuthPrompt: Bool {
|
|
218
|
+
guard let prompt = pendingAuthPrompt else { return false }
|
|
219
|
+
let value = authPromptInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
220
|
+
return prompt.allowEmpty || !value.isEmpty
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func toggleVisibility() {
|
|
224
|
+
isVisible.toggle()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func toggleAuthPanel() {
|
|
228
|
+
isAuthPanelVisible.toggle()
|
|
229
|
+
if isAuthPanelVisible {
|
|
230
|
+
dockHeight = max(dockHeight, 300)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func clearConversation() {
|
|
235
|
+
try? FileManager.default.removeItem(at: sessionFileURL)
|
|
236
|
+
messages = [
|
|
237
|
+
PiChatMessage(
|
|
238
|
+
role: .system,
|
|
239
|
+
text: "Started a fresh Pi conversation.",
|
|
240
|
+
timestamp: Date()
|
|
241
|
+
)
|
|
242
|
+
]
|
|
243
|
+
statusText = "idle"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func sendDraft() {
|
|
247
|
+
let text = draft.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
248
|
+
guard !text.isEmpty else { return }
|
|
249
|
+
draft = ""
|
|
250
|
+
send(text)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func send(_ text: String) {
|
|
254
|
+
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
255
|
+
guard !trimmed.isEmpty else { return }
|
|
256
|
+
guard !isSending else { return }
|
|
257
|
+
|
|
258
|
+
messages.append(PiChatMessage(role: .user, text: trimmed, timestamp: Date()))
|
|
259
|
+
|
|
260
|
+
guard let piPath = resolvePiPath() else {
|
|
261
|
+
appendSystemMessage("Pi CLI not found. Install `pi` or add it to PATH.")
|
|
262
|
+
statusText = "missing pi"
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let provider = currentProvider
|
|
267
|
+
isSending = true
|
|
268
|
+
statusText = "thinking..."
|
|
269
|
+
|
|
270
|
+
queue.async { [weak self] in
|
|
271
|
+
guard let self else { return }
|
|
272
|
+
|
|
273
|
+
let proc = Process()
|
|
274
|
+
proc.executableURL = URL(fileURLWithPath: piPath)
|
|
275
|
+
proc.arguments = [
|
|
276
|
+
"--provider", provider.id,
|
|
277
|
+
"-p",
|
|
278
|
+
"--session", self.sessionFileURL.path,
|
|
279
|
+
trimmed,
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
var env = ProcessInfo.processInfo.environment
|
|
283
|
+
env.removeValue(forKey: "CLAUDECODE")
|
|
284
|
+
if provider.id == "github-copilot", self.storedCredentialKinds[provider.id] == nil {
|
|
285
|
+
env.removeValue(forKey: "COPILOT_GITHUB_TOKEN")
|
|
286
|
+
}
|
|
287
|
+
Self.sanitizeEnvironment(&env, for: provider.id, hasStoredCredential: self.storedCredentialKinds[provider.id] != nil)
|
|
288
|
+
proc.environment = env
|
|
289
|
+
|
|
290
|
+
let outPipe = Pipe()
|
|
291
|
+
let errPipe = Pipe()
|
|
292
|
+
proc.standardOutput = outPipe
|
|
293
|
+
proc.standardError = errPipe
|
|
294
|
+
|
|
295
|
+
let stdout: String
|
|
296
|
+
let stderr: String
|
|
297
|
+
let exitCode: Int32
|
|
298
|
+
|
|
299
|
+
do {
|
|
300
|
+
try proc.run()
|
|
301
|
+
proc.waitUntilExit()
|
|
302
|
+
stdout = String(data: outPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
|
|
303
|
+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
304
|
+
stderr = String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
|
|
305
|
+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
306
|
+
exitCode = proc.terminationStatus
|
|
307
|
+
} catch {
|
|
308
|
+
DispatchQueue.main.async {
|
|
309
|
+
self.isSending = false
|
|
310
|
+
self.statusText = "launch failed"
|
|
311
|
+
self.appendSystemMessage("Failed to launch Pi: \(error.localizedDescription)")
|
|
312
|
+
}
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
DispatchQueue.main.async {
|
|
317
|
+
self.isSending = false
|
|
318
|
+
|
|
319
|
+
if exitCode == 0, !stdout.isEmpty {
|
|
320
|
+
self.statusText = "idle"
|
|
321
|
+
self.messages.append(PiChatMessage(
|
|
322
|
+
role: .assistant,
|
|
323
|
+
text: stdout,
|
|
324
|
+
timestamp: Date()
|
|
325
|
+
))
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let message = !stderr.isEmpty ? stderr : (stdout.isEmpty ? "Pi returned no output." : stdout)
|
|
330
|
+
self.statusText = "error"
|
|
331
|
+
self.appendSystemMessage(message)
|
|
332
|
+
if Self.looksLikeAuthError(message) {
|
|
333
|
+
self.isAuthPanelVisible = true
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func saveSelectedToken() {
|
|
340
|
+
let token = authToken.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
341
|
+
guard !token.isEmpty else {
|
|
342
|
+
authErrorText = "Enter a token before saving."
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
do {
|
|
347
|
+
try mutateAuthFile { auth in
|
|
348
|
+
auth[authProviderID] = [
|
|
349
|
+
"type": "api_key",
|
|
350
|
+
"key": token,
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
authToken = ""
|
|
354
|
+
authNoticeText = "Saved \(currentProvider.tokenLabel.lowercased()) for \(currentProvider.name)."
|
|
355
|
+
authErrorText = nil
|
|
356
|
+
reloadAuthState()
|
|
357
|
+
appendSystemMessage("Saved \(currentProvider.name) credentials to Pi auth storage.")
|
|
358
|
+
} catch {
|
|
359
|
+
authErrorText = "Failed to save token: \(error.localizedDescription)"
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
func removeSelectedCredential() {
|
|
364
|
+
do {
|
|
365
|
+
try mutateAuthFile { auth in
|
|
366
|
+
auth.removeValue(forKey: authProviderID)
|
|
367
|
+
}
|
|
368
|
+
authNoticeText = "Removed saved credentials for \(currentProvider.name)."
|
|
369
|
+
authErrorText = nil
|
|
370
|
+
reloadAuthState()
|
|
371
|
+
appendSystemMessage("Removed saved \(currentProvider.name) credentials from Pi auth storage.")
|
|
372
|
+
} catch {
|
|
373
|
+
authErrorText = "Failed to remove credentials: \(error.localizedDescription)"
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
func startSelectedAuthFlow() {
|
|
378
|
+
if currentProvider.authMode == .apiKey {
|
|
379
|
+
saveSelectedToken()
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
startOAuthLogin(for: currentProvider)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
func submitAuthPrompt() {
|
|
387
|
+
guard let prompt = pendingAuthPrompt else { return }
|
|
388
|
+
let value = authPromptInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
389
|
+
guard prompt.allowEmpty || !value.isEmpty else { return }
|
|
390
|
+
|
|
391
|
+
guard let handle = authInputHandle else {
|
|
392
|
+
authErrorText = "Pi auth input pipe is no longer available."
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let line = value + "\n"
|
|
397
|
+
if let data = line.data(using: .utf8) {
|
|
398
|
+
do {
|
|
399
|
+
try handle.write(contentsOf: data)
|
|
400
|
+
authPromptInput = ""
|
|
401
|
+
pendingAuthPrompt = nil
|
|
402
|
+
} catch {
|
|
403
|
+
authErrorText = "Failed to send auth input: \(error.localizedDescription)"
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func cancelAuthFlow() {
|
|
409
|
+
authProcess?.terminate()
|
|
410
|
+
cleanupAuthProcess()
|
|
411
|
+
isAuthenticating = false
|
|
412
|
+
authNoticeText = "Cancelled auth flow."
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private func startOAuthLogin(for provider: PiProvider) {
|
|
416
|
+
guard !isAuthenticating else {
|
|
417
|
+
authErrorText = "An auth flow is already running."
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
guard let nodePath = resolveNodePath() else {
|
|
422
|
+
authErrorText = "Node.js is required for Pi OAuth login."
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
guard let oauthModuleURL = resolveOAuthModuleURL() else {
|
|
427
|
+
authErrorText = "Couldn't locate Pi's OAuth module next to the installed `pi` CLI."
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let proc = Process()
|
|
432
|
+
proc.executableURL = URL(fileURLWithPath: nodePath)
|
|
433
|
+
proc.arguments = [
|
|
434
|
+
"--input-type=module",
|
|
435
|
+
"--eval",
|
|
436
|
+
Self.oauthDriverScript,
|
|
437
|
+
provider.id,
|
|
438
|
+
oauthModuleURL.absoluteString,
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
let stdinPipe = Pipe()
|
|
442
|
+
let stdoutPipe = Pipe()
|
|
443
|
+
let stderrPipe = Pipe()
|
|
444
|
+
proc.standardInput = stdinPipe
|
|
445
|
+
proc.standardOutput = stdoutPipe
|
|
446
|
+
proc.standardError = stderrPipe
|
|
447
|
+
|
|
448
|
+
authStdoutBuffer = ""
|
|
449
|
+
authStderrBuffer = ""
|
|
450
|
+
authPromptInput = ""
|
|
451
|
+
pendingAuthPrompt = nil
|
|
452
|
+
authNoticeText = "Starting \(provider.name) login..."
|
|
453
|
+
authErrorText = nil
|
|
454
|
+
isAuthenticating = true
|
|
455
|
+
|
|
456
|
+
stdoutPipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
|
|
457
|
+
let data = handle.availableData
|
|
458
|
+
guard !data.isEmpty, let text = String(data: data, encoding: .utf8) else { return }
|
|
459
|
+
self?.handleAuthStdout(text)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
stderrPipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
|
|
463
|
+
let data = handle.availableData
|
|
464
|
+
guard !data.isEmpty, let text = String(data: data, encoding: .utf8) else { return }
|
|
465
|
+
self?.handleAuthStderr(text)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
proc.terminationHandler = { [weak self] process in
|
|
469
|
+
DispatchQueue.main.async {
|
|
470
|
+
self?.handleAuthProcessExit(status: process.terminationStatus)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
do {
|
|
475
|
+
try proc.run()
|
|
476
|
+
authProcess = proc
|
|
477
|
+
authInputHandle = stdinPipe.fileHandleForWriting
|
|
478
|
+
appendSystemMessage("Started \(provider.name) auth flow.")
|
|
479
|
+
} catch {
|
|
480
|
+
cleanupAuthProcess()
|
|
481
|
+
isAuthenticating = false
|
|
482
|
+
authErrorText = "Failed to launch auth flow: \(error.localizedDescription)"
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private func handleAuthStdout(_ text: String) {
|
|
487
|
+
DispatchQueue.main.async {
|
|
488
|
+
self.authStdoutBuffer.append(text)
|
|
489
|
+
self.consumeBufferedAuthLines(buffer: &self.authStdoutBuffer, handler: self.handleAuthEventLine(_:))
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private func handleAuthStderr(_ text: String) {
|
|
494
|
+
DispatchQueue.main.async {
|
|
495
|
+
self.authStderrBuffer.append(text)
|
|
496
|
+
self.consumeBufferedAuthLines(buffer: &self.authStderrBuffer) { line in
|
|
497
|
+
guard !line.isEmpty else { return }
|
|
498
|
+
self.authNoticeText = line
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private func consumeBufferedAuthLines(buffer: inout String, handler: (String) -> Void) {
|
|
504
|
+
while let range = buffer.range(of: "\n") {
|
|
505
|
+
let line = String(buffer[..<range.lowerBound])
|
|
506
|
+
buffer.removeSubrange(buffer.startIndex...range.lowerBound)
|
|
507
|
+
handler(line.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private func handleAuthEventLine(_ line: String) {
|
|
512
|
+
guard !line.isEmpty else { return }
|
|
513
|
+
guard let data = line.data(using: .utf8),
|
|
514
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
515
|
+
let type = json["type"] as? String else {
|
|
516
|
+
authNoticeText = line
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
switch type {
|
|
521
|
+
case "prompt":
|
|
522
|
+
pendingAuthPrompt = PiAuthPrompt(
|
|
523
|
+
message: json["message"] as? String ?? "Continue",
|
|
524
|
+
placeholder: json["placeholder"] as? String,
|
|
525
|
+
allowEmpty: json["allowEmpty"] as? Bool ?? false
|
|
526
|
+
)
|
|
527
|
+
authNoticeText = pendingAuthPrompt?.message
|
|
528
|
+
|
|
529
|
+
case "auth":
|
|
530
|
+
let urlString = json["url"] as? String ?? ""
|
|
531
|
+
let instructions = json["instructions"] as? String
|
|
532
|
+
authNoticeText = instructions ?? "Continue in your browser."
|
|
533
|
+
if let url = URL(string: urlString) {
|
|
534
|
+
NSWorkspace.shared.open(url)
|
|
535
|
+
}
|
|
536
|
+
if let instructions, !instructions.isEmpty {
|
|
537
|
+
appendSystemMessage("Pi auth: \(instructions)")
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
case "progress":
|
|
541
|
+
authNoticeText = json["message"] as? String ?? "Working..."
|
|
542
|
+
|
|
543
|
+
case "success":
|
|
544
|
+
guard var credentials = json["credentials"] as? [String: Any] else {
|
|
545
|
+
authErrorText = "Pi auth completed but returned no credentials."
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
credentials["type"] = "oauth"
|
|
549
|
+
do {
|
|
550
|
+
try mutateAuthFile { auth in
|
|
551
|
+
auth[authProviderID] = credentials
|
|
552
|
+
}
|
|
553
|
+
reloadAuthState()
|
|
554
|
+
authNoticeText = "Saved OAuth credentials for \(currentProvider.name)."
|
|
555
|
+
authErrorText = nil
|
|
556
|
+
appendSystemMessage("Saved \(currentProvider.name) OAuth credentials to Pi auth storage.")
|
|
557
|
+
} catch {
|
|
558
|
+
authErrorText = "Failed to save OAuth credentials: \(error.localizedDescription)"
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
case "error":
|
|
562
|
+
let message = json["message"] as? String ?? "Unknown Pi auth error."
|
|
563
|
+
authErrorText = message
|
|
564
|
+
appendSystemMessage("Pi auth failed: \(message)")
|
|
565
|
+
|
|
566
|
+
default:
|
|
567
|
+
authNoticeText = line
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private func handleAuthProcessExit(status: Int32) {
|
|
572
|
+
let hadExplicitError = authErrorText != nil
|
|
573
|
+
cleanupAuthProcess()
|
|
574
|
+
isAuthenticating = false
|
|
575
|
+
pendingAuthPrompt = nil
|
|
576
|
+
|
|
577
|
+
if status == 0 {
|
|
578
|
+
if !hadExplicitError {
|
|
579
|
+
authNoticeText = authNoticeText ?? "Auth flow finished."
|
|
580
|
+
}
|
|
581
|
+
} else if !hadExplicitError {
|
|
582
|
+
authErrorText = "Auth flow exited with status \(status)."
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private func cleanupAuthProcess() {
|
|
587
|
+
authProcess?.standardInput = nil
|
|
588
|
+
if let output = authProcess?.standardOutput as? Pipe {
|
|
589
|
+
output.fileHandleForReading.readabilityHandler = nil
|
|
590
|
+
}
|
|
591
|
+
if let error = authProcess?.standardError as? Pipe {
|
|
592
|
+
error.fileHandleForReading.readabilityHandler = nil
|
|
593
|
+
}
|
|
594
|
+
authInputHandle = nil
|
|
595
|
+
authProcess = nil
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private func appendSystemMessage(_ text: String) {
|
|
599
|
+
messages.append(PiChatMessage(role: .system, text: text, timestamp: Date()))
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private func reloadAuthState() {
|
|
603
|
+
let auth = loadAuthFile()
|
|
604
|
+
var kinds: [String: String] = [:]
|
|
605
|
+
|
|
606
|
+
for (providerID, rawValue) in auth {
|
|
607
|
+
guard let record = rawValue as? [String: Any],
|
|
608
|
+
let type = record["type"] as? String else { continue }
|
|
609
|
+
kinds[providerID] = type
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
storedCredentialKinds = kinds
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private func loadAuthFile() -> [String: Any] {
|
|
616
|
+
guard let data = try? Data(contentsOf: authFileURL), !data.isEmpty else { return [:] }
|
|
617
|
+
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] }
|
|
618
|
+
return json
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private func mutateAuthFile(_ mutate: (inout [String: Any]) -> Void) throws {
|
|
622
|
+
let fm = FileManager.default
|
|
623
|
+
let dir = authFileURL.deletingLastPathComponent()
|
|
624
|
+
try fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
625
|
+
|
|
626
|
+
var auth = loadAuthFile()
|
|
627
|
+
mutate(&auth)
|
|
628
|
+
|
|
629
|
+
let data = try JSONSerialization.data(withJSONObject: auth, options: [.prettyPrinted, .sortedKeys])
|
|
630
|
+
try data.write(to: authFileURL, options: .atomic)
|
|
631
|
+
try fm.setAttributes([.posixPermissions: 0o700], ofItemAtPath: dir.path)
|
|
632
|
+
try fm.setAttributes([.posixPermissions: 0o600], ofItemAtPath: authFileURL.path)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private func resolvePiPath() -> String? {
|
|
636
|
+
resolveCommandPath(
|
|
637
|
+
named: "pi",
|
|
638
|
+
candidates: [
|
|
639
|
+
"/opt/homebrew/bin/pi",
|
|
640
|
+
"/usr/local/bin/pi",
|
|
641
|
+
NSHomeDirectory() + "/.local/bin/pi",
|
|
642
|
+
]
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private func resolveNodePath() -> String? {
|
|
647
|
+
resolveCommandPath(
|
|
648
|
+
named: "node",
|
|
649
|
+
candidates: [
|
|
650
|
+
"/opt/homebrew/bin/node",
|
|
651
|
+
"/usr/local/bin/node",
|
|
652
|
+
"/usr/bin/node",
|
|
653
|
+
]
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private func resolveCommandPath(named command: String, candidates: [String]) -> String? {
|
|
658
|
+
for path in candidates where FileManager.default.isExecutableFile(atPath: path) {
|
|
659
|
+
return path
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let proc = Process()
|
|
663
|
+
proc.executableURL = URL(fileURLWithPath: "/bin/sh")
|
|
664
|
+
proc.arguments = ["-c", "which \(command) 2>/dev/null"]
|
|
665
|
+
let pipe = Pipe()
|
|
666
|
+
proc.standardOutput = pipe
|
|
667
|
+
proc.standardError = FileHandle.nullDevice
|
|
668
|
+
try? proc.run()
|
|
669
|
+
proc.waitUntilExit()
|
|
670
|
+
let output = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
|
|
671
|
+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
672
|
+
return output.isEmpty ? nil : output
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private func resolveOAuthModuleURL() -> URL? {
|
|
676
|
+
guard let packageRoot = resolvePiPackageRoot() else { return nil }
|
|
677
|
+
let moduleURL = packageRoot
|
|
678
|
+
.appendingPathComponent("node_modules")
|
|
679
|
+
.appendingPathComponent("@mariozechner")
|
|
680
|
+
.appendingPathComponent("pi-ai")
|
|
681
|
+
.appendingPathComponent("dist")
|
|
682
|
+
.appendingPathComponent("utils")
|
|
683
|
+
.appendingPathComponent("oauth")
|
|
684
|
+
.appendingPathComponent("index.js")
|
|
685
|
+
return FileManager.default.fileExists(atPath: moduleURL.path) ? moduleURL : nil
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private func resolvePiPackageRoot() -> URL? {
|
|
689
|
+
guard let piPath = resolvePiPath() else { return nil }
|
|
690
|
+
let resolved = URL(fileURLWithPath: piPath).resolvingSymlinksInPath()
|
|
691
|
+
guard resolved.lastPathComponent == "cli.js",
|
|
692
|
+
resolved.deletingLastPathComponent().lastPathComponent == "dist" else { return nil }
|
|
693
|
+
return resolved.deletingLastPathComponent().deletingLastPathComponent()
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private static func piAgentDirURL() -> URL {
|
|
697
|
+
if let override = ProcessInfo.processInfo.environment["PI_CODING_AGENT_DIR"],
|
|
698
|
+
!override.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
699
|
+
return URL(fileURLWithPath: override, isDirectory: true)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return URL(fileURLWithPath: NSHomeDirectory())
|
|
703
|
+
.appendingPathComponent(".pi", isDirectory: true)
|
|
704
|
+
.appendingPathComponent("agent", isDirectory: true)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private static func looksLikeAuthError(_ message: String) -> Bool {
|
|
708
|
+
let lowercased = message.lowercased()
|
|
709
|
+
return lowercased.contains("api key")
|
|
710
|
+
|| lowercased.contains("oauth")
|
|
711
|
+
|| lowercased.contains("token")
|
|
712
|
+
|| lowercased.contains("authentication")
|
|
713
|
+
|| lowercased.contains("unauthorized")
|
|
714
|
+
|| lowercased.contains("bad request")
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private static func clampDockHeight(_ height: CGFloat) -> CGFloat {
|
|
718
|
+
min(max(height, 170), 520)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private static func sanitizeEnvironment(_ env: inout [String: String], for providerID: String, hasStoredCredential: Bool) {
|
|
722
|
+
let providerEnvVars: [String: [String]] = [
|
|
723
|
+
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
|
|
724
|
+
"anthropic": ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
|
|
725
|
+
"openai": ["OPENAI_API_KEY"],
|
|
726
|
+
"google": ["GEMINI_API_KEY"],
|
|
727
|
+
"groq": ["GROQ_API_KEY"],
|
|
728
|
+
"xai": ["XAI_API_KEY"],
|
|
729
|
+
"openrouter": ["OPENROUTER_API_KEY"],
|
|
730
|
+
"mistral": ["MISTRAL_API_KEY"],
|
|
731
|
+
"minimax": ["MINIMAX_API_KEY"],
|
|
732
|
+
"openai-codex": [],
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
for (id, keys) in providerEnvVars where id != providerID {
|
|
736
|
+
for key in keys {
|
|
737
|
+
env.removeValue(forKey: key)
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if providerID == "github-copilot", !hasStoredCredential {
|
|
742
|
+
env.removeValue(forKey: "COPILOT_GITHUB_TOKEN")
|
|
743
|
+
env.removeValue(forKey: "GH_TOKEN")
|
|
744
|
+
env.removeValue(forKey: "GITHUB_TOKEN")
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private static let oauthDriverScript = #"""
|
|
749
|
+
import readline from 'node:readline';
|
|
750
|
+
|
|
751
|
+
const providerId = process.argv[1];
|
|
752
|
+
const oauthModuleUrl = process.argv[2];
|
|
753
|
+
const { getOAuthProvider } = await import(oauthModuleUrl);
|
|
754
|
+
|
|
755
|
+
const provider = getOAuthProvider(providerId);
|
|
756
|
+
if (!provider) {
|
|
757
|
+
process.stdout.write(JSON.stringify({ type: 'error', message: `Unknown OAuth provider: ${providerId}` }) + '\n');
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const rl = readline.createInterface({
|
|
762
|
+
input: process.stdin,
|
|
763
|
+
output: process.stderr,
|
|
764
|
+
terminal: false,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
function emit(event) {
|
|
768
|
+
process.stdout.write(JSON.stringify(event) + '\n');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function readLine() {
|
|
772
|
+
return new Promise((resolve) => {
|
|
773
|
+
rl.once('line', (line) => resolve(line));
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
const credentials = await provider.login({
|
|
779
|
+
onAuth: (info) => emit({
|
|
780
|
+
type: 'auth',
|
|
781
|
+
url: info.url,
|
|
782
|
+
instructions: info.instructions ?? null,
|
|
783
|
+
}),
|
|
784
|
+
onPrompt: async (prompt) => {
|
|
785
|
+
emit({
|
|
786
|
+
type: 'prompt',
|
|
787
|
+
message: prompt.message,
|
|
788
|
+
placeholder: prompt.placeholder ?? null,
|
|
789
|
+
allowEmpty: Boolean(prompt.allowEmpty),
|
|
790
|
+
});
|
|
791
|
+
const input = await readLine();
|
|
792
|
+
return typeof input === 'string' ? input : '';
|
|
793
|
+
},
|
|
794
|
+
onProgress: (message) => emit({
|
|
795
|
+
type: 'progress',
|
|
796
|
+
message,
|
|
797
|
+
}),
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
emit({
|
|
801
|
+
type: 'success',
|
|
802
|
+
credentials,
|
|
803
|
+
});
|
|
804
|
+
rl.close();
|
|
805
|
+
process.exit(0);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
emit({
|
|
808
|
+
type: 'error',
|
|
809
|
+
message: error instanceof Error ? error.message : String(error),
|
|
810
|
+
});
|
|
811
|
+
rl.close();
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
"""#
|
|
815
|
+
}
|