ethagent 2.1.1 → 2.2.0
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/package.json +1 -1
- package/src/auth/openaiOAuth/credentials.ts +47 -0
- package/src/auth/openaiOAuth/crypto.ts +23 -0
- package/src/auth/openaiOAuth/index.ts +238 -0
- package/src/auth/openaiOAuth/landingPage.ts +125 -0
- package/src/auth/openaiOAuth/listener.ts +151 -0
- package/src/auth/openaiOAuth/refresh.ts +70 -0
- package/src/auth/openaiOAuth/shared.ts +115 -0
- package/src/chat/chatSessionState.ts +2 -1
- package/src/chat/commands.ts +2 -1
- package/src/identity/ens/agentRecords.ts +5 -19
- package/src/identity/ens/ensAutomation/setup.ts +0 -1
- package/src/identity/ens/ensAutomation/types.ts +0 -1
- package/src/identity/hub/OperationalRoutes.tsx +2 -11
- package/src/identity/hub/components/IdentitySummary.tsx +8 -3
- package/src/identity/hub/components/MenuScreen.tsx +1 -2
- package/src/identity/hub/components/menuFlagsFromReconciliation.ts +1 -3
- package/src/identity/hub/effects/ens/transactions.ts +15 -15
- package/src/identity/hub/effects/index.ts +0 -1
- package/src/identity/hub/effects/profile/profileState.ts +12 -4
- package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +37 -159
- package/src/identity/hub/effects/rebackup/runRebackup.ts +2 -2
- package/src/identity/hub/effects/restoreAdmin.ts +2 -61
- package/src/identity/hub/effects/shared/sync.ts +3 -44
- package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +1 -39
- package/src/identity/hub/flows/custody/custodyFlowActions.ts +5 -3
- package/src/identity/hub/flows/custody/custodyFlowTypes.ts +1 -1
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +80 -175
- package/src/identity/hub/flows/ens/EnsEditFlow.tsx +20 -75
- package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +16 -56
- package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +0 -18
- package/src/identity/hub/flows/ens/EnsEditRunners.tsx +0 -136
- package/src/identity/hub/flows/ens/EnsEditShared.tsx +5 -4
- package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +56 -205
- package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +7 -0
- package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +0 -31
- package/src/identity/hub/flows/ens/ensEditCopy.ts +1 -1
- package/src/identity/hub/flows/ens/ensEditTypes.ts +6 -20
- package/src/identity/hub/flows/profile/EditProfileFlow.tsx +7 -0
- package/src/identity/hub/flows/restore/RestoreFlow.tsx +5 -5
- package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +0 -1
- package/src/identity/hub/reconciliation/agentReconciliation/run.ts +1 -34
- package/src/identity/hub/reconciliation/agentReconciliation/types.ts +0 -4
- package/src/identity/hub/reconciliation/index.ts +0 -7
- package/src/identity/hub/reconciliation/walletSetup.ts +1 -194
- package/src/identity/wallet/browserWallet/types.ts +0 -5
- package/src/identity/wallet/page/copy.ts +1 -31
- package/src/identity/wallet/walletPurposeCompat.ts +0 -2
- package/src/models/ModelPicker.tsx +246 -8
- package/src/models/catalog.ts +28 -1
- package/src/models/modelPickerOptions.ts +15 -1
- package/src/providers/openai-responses-format.ts +156 -0
- package/src/providers/openai-responses.ts +276 -0
- package/src/providers/registry.ts +85 -8
- package/src/runtime/systemPrompt.ts +1 -1
- package/src/runtime/turn.ts +0 -1
- package/src/storage/secrets.ts +4 -1
- package/src/tools/privateContinuityEditTool.ts +6 -0
- package/src/utils/openExternal.ts +20 -10
- package/src/identity/ens/ensRegistration.ts +0 -199
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { ProviderId } from '../storage/config.js'
|
|
2
|
+
import type { Message, Provider, ProviderCompleteOptions, StreamEvent } from './contracts.js'
|
|
3
|
+
import { ProviderError } from './contracts.js'
|
|
4
|
+
import { providerErrorFromResponse } from './errors.js'
|
|
5
|
+
import { fetchWithRetryStreamEvents } from './retry.js'
|
|
6
|
+
import { iterSseEvents } from './sse.js'
|
|
7
|
+
import { buildResponsesBody } from './openai-responses-format.js'
|
|
8
|
+
import type { OpenAIToolDefinition } from './openai-chat.js'
|
|
9
|
+
|
|
10
|
+
const READ_TIMEOUT_MS = 45_000
|
|
11
|
+
|
|
12
|
+
type DoneStopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'unknown'
|
|
13
|
+
|
|
14
|
+
export type OpenAIResponsesProviderOptions = {
|
|
15
|
+
model: string
|
|
16
|
+
baseUrl: string
|
|
17
|
+
accessToken: string
|
|
18
|
+
accountId?: string
|
|
19
|
+
originator?: string
|
|
20
|
+
tools?: OpenAIToolDefinition[]
|
|
21
|
+
maxRetries?: number
|
|
22
|
+
refresh?: () => Promise<string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type StreamingToolCall = {
|
|
26
|
+
callId: string
|
|
27
|
+
name: string
|
|
28
|
+
inputJson: string
|
|
29
|
+
started: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class OpenAIResponsesProvider implements Provider {
|
|
33
|
+
readonly id: ProviderId = 'openai'
|
|
34
|
+
readonly model: string
|
|
35
|
+
readonly supportsTools: boolean
|
|
36
|
+
private accessToken: string
|
|
37
|
+
private readonly baseUrl: string
|
|
38
|
+
private readonly accountId?: string
|
|
39
|
+
private readonly originator: string
|
|
40
|
+
private readonly tools: OpenAIToolDefinition[]
|
|
41
|
+
private readonly maxRetries?: number
|
|
42
|
+
private readonly refresh?: () => Promise<string>
|
|
43
|
+
|
|
44
|
+
constructor(opts: OpenAIResponsesProviderOptions) {
|
|
45
|
+
this.model = opts.model
|
|
46
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, '')
|
|
47
|
+
this.accessToken = opts.accessToken
|
|
48
|
+
this.accountId = opts.accountId
|
|
49
|
+
this.originator = opts.originator ?? 'codex_cli_rs'
|
|
50
|
+
this.tools = opts.tools ?? []
|
|
51
|
+
this.maxRetries = opts.maxRetries
|
|
52
|
+
this.refresh = opts.refresh
|
|
53
|
+
this.supportsTools = this.tools.length > 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async *complete(
|
|
57
|
+
messages: Message[],
|
|
58
|
+
signal: AbortSignal,
|
|
59
|
+
options: ProviderCompleteOptions = {},
|
|
60
|
+
): AsyncIterable<StreamEvent> {
|
|
61
|
+
if (!this.accessToken) {
|
|
62
|
+
const error = new ProviderError('missing OAuth access token for openai (sign in again via the model picker)')
|
|
63
|
+
yield { type: 'error', message: error.message }
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let attempt = 0
|
|
68
|
+
while (true) {
|
|
69
|
+
attempt += 1
|
|
70
|
+
const body = JSON.stringify(buildResponsesBody({
|
|
71
|
+
model: this.model,
|
|
72
|
+
messages,
|
|
73
|
+
tools: this.tools,
|
|
74
|
+
maxOutputTokens: options.maxTokens,
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
let response: Response
|
|
78
|
+
try {
|
|
79
|
+
response = yield* fetchWithRetryStreamEvents(`${this.baseUrl}/responses`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: this.requestHeaders(),
|
|
82
|
+
body,
|
|
83
|
+
}, { signal, maxRetries: this.maxRetries, rateLimitResetProvider: 'openai-compatible' })
|
|
84
|
+
} catch (err: unknown) {
|
|
85
|
+
if (signal.aborted) return
|
|
86
|
+
yield { type: 'error', message: networkErrorMessage(this.baseUrl, err) }
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (response.status === 401 && this.refresh && attempt === 1) {
|
|
91
|
+
try {
|
|
92
|
+
this.accessToken = await this.refresh()
|
|
93
|
+
continue
|
|
94
|
+
} catch (refreshErr) {
|
|
95
|
+
const message = refreshErr instanceof Error ? refreshErr.message : String(refreshErr)
|
|
96
|
+
yield { type: 'error', message: `OpenAI sign-in expired and refresh failed: ${message}` }
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const error = await providerErrorFromResponse('openai', response)
|
|
103
|
+
yield { type: 'error', message: error.message }
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
if (!response.body) {
|
|
107
|
+
yield { type: 'error', message: 'empty response body' }
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
yield* this.parseStream(response.body, signal)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private requestHeaders(): Record<string, string> {
|
|
117
|
+
const headers: Record<string, string> = {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
Accept: 'text/event-stream',
|
|
120
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
121
|
+
originator: this.originator,
|
|
122
|
+
}
|
|
123
|
+
if (this.accountId) headers['chatgpt-account-id'] = this.accountId
|
|
124
|
+
return headers
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async *parseStream(body: ReadableStream<Uint8Array>, signal: AbortSignal): AsyncIterable<StreamEvent> {
|
|
128
|
+
const toolCalls = new Map<string, StreamingToolCall>()
|
|
129
|
+
let inputTokens: number | undefined
|
|
130
|
+
let outputTokens: number | undefined
|
|
131
|
+
let stopReason: DoneStopReason = 'unknown'
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
for await (const frame of iterSseEvents(body, signal, READ_TIMEOUT_MS)) {
|
|
135
|
+
const eventName = frame.event ?? ''
|
|
136
|
+
if (!frame.data || frame.data === '[DONE]') continue
|
|
137
|
+
let parsed: Record<string, unknown>
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(frame.data) as Record<string, unknown>
|
|
140
|
+
} catch {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
switch (eventName) {
|
|
145
|
+
case 'response.output_text.delta': {
|
|
146
|
+
const delta = typeof parsed.delta === 'string' ? parsed.delta : ''
|
|
147
|
+
if (delta) yield { type: 'text', delta }
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
case 'response.reasoning_summary_text.delta':
|
|
151
|
+
case 'response.reasoning.delta':
|
|
152
|
+
case 'response.reasoning_text.delta': {
|
|
153
|
+
const delta = typeof parsed.delta === 'string' ? parsed.delta : ''
|
|
154
|
+
if (delta) yield { type: 'thinking', delta }
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
case 'response.output_item.added': {
|
|
158
|
+
const item = (parsed.item ?? {}) as Record<string, unknown>
|
|
159
|
+
if (item.type === 'function_call') {
|
|
160
|
+
const callId = pickString(item.call_id) ?? pickString(item.id) ?? `tool-${toolCalls.size}`
|
|
161
|
+
const name = pickString(item.name) ?? ''
|
|
162
|
+
toolCalls.set(callId, { callId, name, inputJson: '', started: false })
|
|
163
|
+
if (name) {
|
|
164
|
+
toolCalls.get(callId)!.started = true
|
|
165
|
+
yield { type: 'tool_use_start', id: callId, name }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
case 'response.function_call_arguments.delta': {
|
|
171
|
+
const callId = resolveCallId(parsed, toolCalls)
|
|
172
|
+
const delta = typeof parsed.delta === 'string' ? parsed.delta : ''
|
|
173
|
+
if (!callId || !delta) break
|
|
174
|
+
const existing = toolCalls.get(callId)
|
|
175
|
+
if (!existing) break
|
|
176
|
+
existing.inputJson += delta
|
|
177
|
+
yield { type: 'tool_use_delta', id: callId, delta }
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
case 'response.output_item.done': {
|
|
181
|
+
const item = (parsed.item ?? {}) as Record<string, unknown>
|
|
182
|
+
if (item.type === 'function_call') {
|
|
183
|
+
const callId = pickString(item.call_id) ?? pickString(item.id)
|
|
184
|
+
if (!callId) break
|
|
185
|
+
const existing = toolCalls.get(callId)
|
|
186
|
+
const name = pickString(item.name) ?? existing?.name ?? ''
|
|
187
|
+
const argsJson = pickString(item.arguments) ?? existing?.inputJson ?? ''
|
|
188
|
+
stopReason = 'tool_use'
|
|
189
|
+
yield {
|
|
190
|
+
type: 'tool_use_stop',
|
|
191
|
+
id: callId,
|
|
192
|
+
name,
|
|
193
|
+
input: parseToolArguments(argsJson),
|
|
194
|
+
}
|
|
195
|
+
toolCalls.delete(callId)
|
|
196
|
+
}
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
case 'response.completed': {
|
|
200
|
+
const usage = (parsed.response as { usage?: Record<string, unknown> } | undefined)?.usage
|
|
201
|
+
const tokens = readUsage(usage)
|
|
202
|
+
if (tokens.input !== undefined) inputTokens = tokens.input
|
|
203
|
+
if (tokens.output !== undefined) outputTokens = tokens.output
|
|
204
|
+
if (stopReason !== 'tool_use') stopReason = 'end_turn'
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
case 'response.failed':
|
|
208
|
+
case 'response.error':
|
|
209
|
+
case 'error': {
|
|
210
|
+
const error = parsed.error as { message?: string } | undefined
|
|
211
|
+
const message = error?.message ?? 'Responses API error'
|
|
212
|
+
yield { type: 'error', message }
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
case 'response.incomplete': {
|
|
216
|
+
const reason = pickString((parsed.response as { incomplete_details?: { reason?: string } } | undefined)?.incomplete_details?.reason)
|
|
217
|
+
if (reason === 'max_output_tokens') stopReason = 'max_tokens'
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
default:
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (err: unknown) {
|
|
225
|
+
if (signal.aborted) return
|
|
226
|
+
yield { type: 'error', message: networkErrorMessage(this.baseUrl, err, 'stream error') }
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (signal.aborted) return
|
|
231
|
+
yield { type: 'done', inputTokens, outputTokens, stopReason }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function pickString(value: unknown): string | undefined {
|
|
236
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveCallId(parsed: Record<string, unknown>, calls: Map<string, StreamingToolCall>): string | undefined {
|
|
240
|
+
return (
|
|
241
|
+
pickString(parsed.call_id)
|
|
242
|
+
?? pickString(parsed.item_id)
|
|
243
|
+
?? pickString((parsed.item as Record<string, unknown> | undefined)?.call_id)
|
|
244
|
+
?? pickString((parsed.item as Record<string, unknown> | undefined)?.id)
|
|
245
|
+
?? (calls.size === 1 ? Array.from(calls.keys())[0] : undefined)
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function readUsage(usage: Record<string, unknown> | undefined): { input?: number; output?: number } {
|
|
250
|
+
if (!usage) return {}
|
|
251
|
+
const input = typeof usage.input_tokens === 'number'
|
|
252
|
+
? usage.input_tokens
|
|
253
|
+
: typeof usage.prompt_tokens === 'number' ? usage.prompt_tokens : undefined
|
|
254
|
+
const output = typeof usage.output_tokens === 'number'
|
|
255
|
+
? usage.output_tokens
|
|
256
|
+
: typeof usage.completion_tokens === 'number' ? usage.completion_tokens : undefined
|
|
257
|
+
return { input, output }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function parseToolArguments(input: string): Record<string, unknown> {
|
|
261
|
+
const trimmed = input.trim()
|
|
262
|
+
if (!trimmed) return {}
|
|
263
|
+
try {
|
|
264
|
+
const parsed = JSON.parse(trimmed) as unknown
|
|
265
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
266
|
+
? (parsed as Record<string, unknown>)
|
|
267
|
+
: {}
|
|
268
|
+
} catch {
|
|
269
|
+
return {}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function networkErrorMessage(baseUrl: string, err: unknown, fallback = 'network error'): string {
|
|
274
|
+
const message = (err as Error).message || fallback
|
|
275
|
+
return `openai request failed at ${baseUrl}: ${message}`
|
|
276
|
+
}
|
|
@@ -6,10 +6,19 @@ import type { SessionMode } from '../runtime/sessionMode.js'
|
|
|
6
6
|
import { AnthropicProvider } from './anthropic.js'
|
|
7
7
|
import { GeminiProvider } from './gemini.js'
|
|
8
8
|
import { OpenAIChatProvider } from './openai-chat.js'
|
|
9
|
+
import { OpenAIResponsesProvider } from './openai-responses.js'
|
|
9
10
|
import { anthropicTools, geminiTools, openAITools } from '../tools/registry.js'
|
|
10
|
-
import { openAIBaseUrlFor } from '../models/catalog.js'
|
|
11
|
+
import { openAIBaseUrlFor, OPENAI_OAUTH_DEFAULT_MODEL, isOpenAIOAuthAllowedModel } from '../models/catalog.js'
|
|
12
|
+
import {
|
|
13
|
+
getOpenAIOAuthCredentials,
|
|
14
|
+
setOpenAIOAuthCredentials,
|
|
15
|
+
type OpenAIOAuthCredentials,
|
|
16
|
+
} from '../auth/openaiOAuth/credentials.js'
|
|
17
|
+
import { refreshOpenAIAccessToken, shouldRefresh } from '../auth/openaiOAuth/refresh.js'
|
|
11
18
|
import type { Tool } from '../tools/contracts.js'
|
|
12
19
|
|
|
20
|
+
export const OPENAI_CHATGPT_BACKEND_URL = 'https://chatgpt.com/backend-api/codex'
|
|
21
|
+
|
|
13
22
|
export function isLocalProvider(provider: string): boolean {
|
|
14
23
|
return provider === 'llamacpp'
|
|
15
24
|
}
|
|
@@ -27,16 +36,84 @@ export function createProvider(config: EthagentConfig, options: { mode?: Session
|
|
|
27
36
|
tools: openAITools(mode, toolContext),
|
|
28
37
|
})
|
|
29
38
|
case 'openai':
|
|
30
|
-
return
|
|
31
|
-
id: 'openai',
|
|
32
|
-
model: config.model,
|
|
33
|
-
baseUrl: openAIBaseUrlFor(config),
|
|
34
|
-
loadApiKey: () => getKey('openai'),
|
|
35
|
-
tools: openAITools(mode, toolContext),
|
|
36
|
-
})
|
|
39
|
+
return createOpenAIProvider(config, openAITools(mode, toolContext))
|
|
37
40
|
case 'anthropic':
|
|
38
41
|
return new AnthropicProvider({ model: config.model, tools: anthropicTools(mode, toolContext) })
|
|
39
42
|
case 'gemini':
|
|
40
43
|
return new GeminiProvider({ model: config.model, tools: geminiTools(mode, toolContext) })
|
|
41
44
|
}
|
|
42
45
|
}
|
|
46
|
+
|
|
47
|
+
function createOpenAIProvider(config: EthagentConfig, tools: ReturnType<typeof openAITools>): Provider {
|
|
48
|
+
return new OpenAIRoutingProvider(config, tools)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class OpenAIRoutingProvider implements Provider {
|
|
52
|
+
readonly id = 'openai' as const
|
|
53
|
+
readonly model: string
|
|
54
|
+
readonly supportsTools: boolean
|
|
55
|
+
private delegate: Provider | null = null
|
|
56
|
+
private readonly config: EthagentConfig
|
|
57
|
+
private readonly tools: ReturnType<typeof openAITools>
|
|
58
|
+
|
|
59
|
+
constructor(config: EthagentConfig, tools: ReturnType<typeof openAITools>) {
|
|
60
|
+
this.config = config
|
|
61
|
+
this.tools = tools
|
|
62
|
+
this.model = config.model
|
|
63
|
+
this.supportsTools = tools.length > 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async *complete(...args: Parameters<Provider['complete']>): ReturnType<Provider['complete']> {
|
|
67
|
+
if (!this.delegate) this.delegate = await this.resolveDelegate()
|
|
68
|
+
yield* this.delegate.complete(...args)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async resolveDelegate(): Promise<Provider> {
|
|
72
|
+
const oauth = await loadFreshOAuthCredentials()
|
|
73
|
+
if (oauth) {
|
|
74
|
+
const oauthModel = isOpenAIOAuthAllowedModel(this.model) ? this.model : OPENAI_OAUTH_DEFAULT_MODEL
|
|
75
|
+
return new OpenAIResponsesProvider({
|
|
76
|
+
model: oauthModel,
|
|
77
|
+
baseUrl: OPENAI_CHATGPT_BACKEND_URL,
|
|
78
|
+
accessToken: oauth.accessToken,
|
|
79
|
+
accountId: oauth.accountId,
|
|
80
|
+
tools: this.tools,
|
|
81
|
+
refresh: async () => {
|
|
82
|
+
const next = await loadFreshOAuthCredentials({ force: true })
|
|
83
|
+
if (!next) throw new Error('No OAuth credentials available')
|
|
84
|
+
return next.accessToken
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
return new OpenAIChatProvider({
|
|
89
|
+
id: 'openai',
|
|
90
|
+
model: this.model,
|
|
91
|
+
baseUrl: openAIBaseUrlFor(this.config),
|
|
92
|
+
loadApiKey: () => getKey('openai'),
|
|
93
|
+
tools: this.tools,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function loadFreshOAuthCredentials(options: { force?: boolean } = {}): Promise<OpenAIOAuthCredentials | null> {
|
|
99
|
+
const current = await getOpenAIOAuthCredentials()
|
|
100
|
+
if (!current) return null
|
|
101
|
+
if (!options.force && !shouldRefresh(current)) return current
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const refreshed = await refreshOpenAIAccessToken(current.refreshToken)
|
|
105
|
+
const now = Date.now()
|
|
106
|
+
const next: OpenAIOAuthCredentials = {
|
|
107
|
+
accessToken: refreshed.accessToken,
|
|
108
|
+
refreshToken: refreshed.refreshToken,
|
|
109
|
+
idToken: refreshed.idToken ?? current.idToken,
|
|
110
|
+
accountId: refreshed.accountId ?? current.accountId,
|
|
111
|
+
expiresAt: now + refreshed.expiresIn * 1000,
|
|
112
|
+
lastRefreshAt: now,
|
|
113
|
+
}
|
|
114
|
+
await setOpenAIOAuthCredentials(next)
|
|
115
|
+
return next
|
|
116
|
+
} catch {
|
|
117
|
+
return current
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -93,6 +93,7 @@ function buildToolEnabledPrompt(ctx: SystemPromptContext): string {
|
|
|
93
93
|
'When exact private continuity text is needed for surgical removal or targeted replacement, call `read_private_continuity_file` with `file: "MEMORY.md"` or `file: "SOUL.md"` first.',
|
|
94
94
|
'When the user wants memory, persona, preferences, or private identity continuity changed, call `propose_private_continuity_edit`; do NOT create, overwrite, or patch SOUL.md/MEMORY.md with `write_file` or `edit_file`.',
|
|
95
95
|
'For private continuity, edit the existing scaffold and build on top of it: prefer `appendToSection`+`appendText` for new notes or use `oldText`+`newText` for targeted replacement. Never omit the edit anchor, never create a new file, and never replace the whole file.',
|
|
96
|
+
'Never call `propose_private_continuity_edit` with `{}` or only `file`. Send exactly one edit mode: either `appendToSection` + non-empty `appendText`, or `oldText` + `newText`. Omit the fields you are not using — do not pass empty strings for `oldText`/`newText` alongside an append, or empty `appendToSection`/`appendText` alongside a targeted edit.',
|
|
96
97
|
'If the user asks to remember preferences or facts, call exactly one private continuity append such as `{"file":"MEMORY.md","appendToSection":"Durable User Preferences","appendText":"- User preference or durable memory."}`.',
|
|
97
98
|
'If the user asks to change persona or standing behavior, call exactly one private continuity append such as `{"file":"SOUL.md","appendToSection":"Persona","appendText":"- Persona or standing behavior."}`.',
|
|
98
99
|
]
|
|
@@ -120,7 +121,6 @@ function buildToolEnabledPrompt(ctx: SystemPromptContext): string {
|
|
|
120
121
|
? [
|
|
121
122
|
'For private SOUL.md or MEMORY.md inspection, do not search project folders. Call `read_private_continuity_file` with `file: "SOUL.md"` or `file: "MEMORY.md"`.',
|
|
122
123
|
'For private SOUL.md or MEMORY.md changes, call `propose_private_continuity_edit` with `file: "SOUL.md"` or `file: "MEMORY.md"` and an in-place append/replacement payload.',
|
|
123
|
-
'Never call `propose_private_continuity_edit` with `{}` or only `file`. For memory/preferences include `appendToSection: "Durable User Preferences"` and a non-empty `appendText`; for persona include `appendToSection: "Persona"` and a non-empty `appendText`.',
|
|
124
124
|
]
|
|
125
125
|
: []),
|
|
126
126
|
'For targeted private continuity edits with `oldText`, copy the text verbatim from the most recent `read_private_continuity_file` output. For workspace targeted edits, copy from the most recent `read_file` output.',
|
package/src/runtime/turn.ts
CHANGED
|
@@ -461,7 +461,6 @@ function nextToolResultRepairNudge(
|
|
|
461
461
|
completedTools: ExecutedToolUse[],
|
|
462
462
|
): string | null {
|
|
463
463
|
if (!provider.supportsTools) return null
|
|
464
|
-
if (provider.id !== 'llamacpp') return null
|
|
465
464
|
const failedPrivateEdit = completedTools.some(completed =>
|
|
466
465
|
completed.name === 'propose_private_continuity_edit'
|
|
467
466
|
&& !completed.result.ok
|
package/src/storage/secrets.ts
CHANGED
|
@@ -129,8 +129,11 @@ export async function getSecret(account: string): Promise<string | null> {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export async function setSecret(account: string, value: string): Promise<KeyBackend> {
|
|
132
|
+
if (typeof value !== 'string') {
|
|
133
|
+
throw new Error(`setSecret(${account}): value is ${typeof value}, expected string`)
|
|
134
|
+
}
|
|
132
135
|
const trimmed = value.trim()
|
|
133
|
-
if (!trimmed) throw new Error(
|
|
136
|
+
if (!trimmed) throw new Error(`setSecret(${account}): value is empty`)
|
|
134
137
|
const keytar = await loadKeytar()
|
|
135
138
|
if (keytar) {
|
|
136
139
|
await keytar.setPassword(KEYTAR_SERVICE, account, trimmed)
|
|
@@ -166,6 +166,12 @@ function normalizePrivateContinuityInput(input: Record<string, unknown>): Record
|
|
|
166
166
|
if (normalized.appendText === undefined) {
|
|
167
167
|
normalized.appendText = normalized.note ?? normalized.text ?? normalized.content
|
|
168
168
|
}
|
|
169
|
+
for (const key of ['oldText', 'newText', 'appendToSection', 'appendText'] as const) {
|
|
170
|
+
const value = normalized[key]
|
|
171
|
+
if (typeof value === 'string' && value.trim() === '') {
|
|
172
|
+
normalized[key] = undefined
|
|
173
|
+
}
|
|
174
|
+
}
|
|
169
175
|
return normalized
|
|
170
176
|
}
|
|
171
177
|
|
|
@@ -3,18 +3,28 @@ import { spawn } from 'node:child_process'
|
|
|
3
3
|
export function openExternalUrl(url: string): void {
|
|
4
4
|
const target = url.trim()
|
|
5
5
|
if (!target) return
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
const safe = target.replace(/"/g, '%22')
|
|
9
|
+
const child = spawn(
|
|
10
|
+
'cmd.exe',
|
|
11
|
+
['/s', '/c', `start "" "${safe}"`],
|
|
12
|
+
{
|
|
13
|
+
detached: true,
|
|
14
|
+
stdio: 'ignore',
|
|
15
|
+
windowsHide: true,
|
|
16
|
+
windowsVerbatimArguments: true,
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
|
+
child.on('error', () => {})
|
|
20
|
+
child.unref()
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const command = process.platform === 'darwin' ? 'open' : 'xdg-open'
|
|
25
|
+
const child = spawn(command, [target], {
|
|
15
26
|
detached: true,
|
|
16
27
|
stdio: 'ignore',
|
|
17
|
-
windowsHide: true,
|
|
18
28
|
})
|
|
19
29
|
child.on('error', () => {})
|
|
20
30
|
child.unref()
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
encodeAbiParameters,
|
|
3
|
-
encodeFunctionData,
|
|
4
|
-
getAddress,
|
|
5
|
-
keccak256,
|
|
6
|
-
namehash,
|
|
7
|
-
parseAbi,
|
|
8
|
-
type Address,
|
|
9
|
-
type Hex,
|
|
10
|
-
type PublicClient,
|
|
11
|
-
} from 'viem'
|
|
12
|
-
|
|
13
|
-
export const ETH_REGISTRAR_CONTROLLER_MAINNET = '0x253553366Da8546fC250F225fe3d25d0C782303b' as Address
|
|
14
|
-
export const PUBLIC_RESOLVER_MAINNET = '0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63' as Address
|
|
15
|
-
export const MIN_COMMIT_AGE_SECONDS = 60
|
|
16
|
-
export const MAX_COMMIT_AGE_SECONDS = 86_400
|
|
17
|
-
export const ONE_YEAR_SECONDS = 31_557_600
|
|
18
|
-
export const REGISTER_VALUE_BUFFER_BIPS = 500n
|
|
19
|
-
|
|
20
|
-
const CONTROLLER_ABI = parseAbi([
|
|
21
|
-
'function available(string name) view returns (bool)',
|
|
22
|
-
'function rentPrice(string name, uint256 duration) view returns (uint256 base, uint256 premium)',
|
|
23
|
-
'function commit(bytes32 commitment)',
|
|
24
|
-
'function register(string name, address owner, uint256 duration, bytes32 secret, address resolver, bytes[] data, bool reverseRecord, uint16 ownerControlledFuses) payable',
|
|
25
|
-
])
|
|
26
|
-
|
|
27
|
-
const RESOLVER_ABI = parseAbi([
|
|
28
|
-
'function setAddr(bytes32 node, address a)',
|
|
29
|
-
])
|
|
30
|
-
|
|
31
|
-
export type RegistrableNameValidation =
|
|
32
|
-
| { ok: true; label: string }
|
|
33
|
-
| { ok: false; reason: 'too-short' | 'too-long' | 'invalid-characters' | 'leading-hyphen' | 'trailing-hyphen' | 'contains-dot'; detail: string }
|
|
34
|
-
|
|
35
|
-
export function validateRegistrableName(value: string): RegistrableNameValidation {
|
|
36
|
-
const label = value.trim().toLowerCase()
|
|
37
|
-
if (label.includes('.')) {
|
|
38
|
-
return { ok: false, reason: 'contains-dot', detail: 'Enter only the label, not a full .eth name.' }
|
|
39
|
-
}
|
|
40
|
-
if (label.length < 3) {
|
|
41
|
-
return { ok: false, reason: 'too-short', detail: 'Names must be at least 3 characters.' }
|
|
42
|
-
}
|
|
43
|
-
if (label.length > 64) {
|
|
44
|
-
return { ok: false, reason: 'too-long', detail: 'Names must be 64 characters or fewer.' }
|
|
45
|
-
}
|
|
46
|
-
if (label.startsWith('-')) {
|
|
47
|
-
return { ok: false, reason: 'leading-hyphen', detail: 'Names cannot start with a hyphen.' }
|
|
48
|
-
}
|
|
49
|
-
if (label.endsWith('-')) {
|
|
50
|
-
return { ok: false, reason: 'trailing-hyphen', detail: 'Names cannot end with a hyphen.' }
|
|
51
|
-
}
|
|
52
|
-
if (!/^[a-z0-9-]+$/.test(label)) {
|
|
53
|
-
return { ok: false, reason: 'invalid-characters', detail: 'Names use lowercase letters, numbers, and hyphens only.' }
|
|
54
|
-
}
|
|
55
|
-
return { ok: true, label }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
type RentPrice = {
|
|
59
|
-
base: bigint
|
|
60
|
-
premium: bigint
|
|
61
|
-
total: bigint
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type EnsRegistrationReadClient = Pick<PublicClient, 'readContract'>
|
|
65
|
-
|
|
66
|
-
export async function readRentPrice(
|
|
67
|
-
client: EnsRegistrationReadClient,
|
|
68
|
-
label: string,
|
|
69
|
-
durationSeconds: bigint,
|
|
70
|
-
): Promise<RentPrice> {
|
|
71
|
-
const result = await client.readContract({
|
|
72
|
-
address: ETH_REGISTRAR_CONTROLLER_MAINNET,
|
|
73
|
-
abi: CONTROLLER_ABI,
|
|
74
|
-
functionName: 'rentPrice',
|
|
75
|
-
args: [label, durationSeconds],
|
|
76
|
-
}) as readonly [bigint, bigint]
|
|
77
|
-
const [base, premium] = result
|
|
78
|
-
return { base, premium, total: base + premium }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function readNameAvailable(
|
|
82
|
-
client: EnsRegistrationReadClient,
|
|
83
|
-
label: string,
|
|
84
|
-
): Promise<boolean> {
|
|
85
|
-
return await client.readContract({
|
|
86
|
-
address: ETH_REGISTRAR_CONTROLLER_MAINNET,
|
|
87
|
-
abi: CONTROLLER_ABI,
|
|
88
|
-
functionName: 'available',
|
|
89
|
-
args: [label],
|
|
90
|
-
}) as boolean
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
type CommitmentArgs = {
|
|
94
|
-
label: string
|
|
95
|
-
owner: Address
|
|
96
|
-
durationSeconds: bigint
|
|
97
|
-
secret: Hex
|
|
98
|
-
resolver?: Address
|
|
99
|
-
setAddrToOwner?: boolean
|
|
100
|
-
reverseRecord?: boolean
|
|
101
|
-
ownerControlledFuses?: number
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
type CommitmentBuild = {
|
|
105
|
-
commitment: Hex
|
|
106
|
-
resolver: Address
|
|
107
|
-
data: Hex[]
|
|
108
|
-
reverseRecord: boolean
|
|
109
|
-
ownerControlledFuses: number
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function buildCommitment(args: CommitmentArgs): CommitmentBuild {
|
|
113
|
-
const owner = getAddress(args.owner)
|
|
114
|
-
const resolver = getAddress(args.resolver ?? PUBLIC_RESOLVER_MAINNET)
|
|
115
|
-
const reverseRecord = args.reverseRecord ?? false
|
|
116
|
-
const ownerControlledFuses = args.ownerControlledFuses ?? 0
|
|
117
|
-
const data: Hex[] = []
|
|
118
|
-
if (args.setAddrToOwner ?? true) {
|
|
119
|
-
data.push(encodeFunctionData({
|
|
120
|
-
abi: RESOLVER_ABI,
|
|
121
|
-
functionName: 'setAddr',
|
|
122
|
-
args: [namehash(`${args.label}.eth`), owner],
|
|
123
|
-
}))
|
|
124
|
-
}
|
|
125
|
-
const commitment = keccak256(encodeAbiParameters(
|
|
126
|
-
[
|
|
127
|
-
{ type: 'string' },
|
|
128
|
-
{ type: 'address' },
|
|
129
|
-
{ type: 'uint256' },
|
|
130
|
-
{ type: 'bytes32' },
|
|
131
|
-
{ type: 'address' },
|
|
132
|
-
{ type: 'bytes[]' },
|
|
133
|
-
{ type: 'bool' },
|
|
134
|
-
{ type: 'uint16' },
|
|
135
|
-
],
|
|
136
|
-
[args.label, owner, args.durationSeconds, args.secret, resolver, data, reverseRecord, ownerControlledFuses],
|
|
137
|
-
))
|
|
138
|
-
return { commitment, resolver, data, reverseRecord, ownerControlledFuses }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function encodeCommitTransaction(commitment: Hex): { to: Address; data: Hex } {
|
|
142
|
-
return {
|
|
143
|
-
to: ETH_REGISTRAR_CONTROLLER_MAINNET,
|
|
144
|
-
data: encodeFunctionData({
|
|
145
|
-
abi: CONTROLLER_ABI,
|
|
146
|
-
functionName: 'commit',
|
|
147
|
-
args: [commitment],
|
|
148
|
-
}),
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
type RegisterTransactionArgs = {
|
|
153
|
-
label: string
|
|
154
|
-
owner: Address
|
|
155
|
-
durationSeconds: bigint
|
|
156
|
-
secret: Hex
|
|
157
|
-
rentPrice: RentPrice
|
|
158
|
-
resolver?: Address
|
|
159
|
-
setAddrToOwner?: boolean
|
|
160
|
-
reverseRecord?: boolean
|
|
161
|
-
ownerControlledFuses?: number
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function encodeRegisterTransaction(args: RegisterTransactionArgs): { to: Address; data: Hex; value: Hex } {
|
|
165
|
-
const owner = getAddress(args.owner)
|
|
166
|
-
const built = buildCommitment({
|
|
167
|
-
label: args.label,
|
|
168
|
-
owner,
|
|
169
|
-
durationSeconds: args.durationSeconds,
|
|
170
|
-
secret: args.secret,
|
|
171
|
-
...(args.resolver !== undefined ? { resolver: args.resolver } : {}),
|
|
172
|
-
...(args.setAddrToOwner !== undefined ? { setAddrToOwner: args.setAddrToOwner } : {}),
|
|
173
|
-
...(args.reverseRecord !== undefined ? { reverseRecord: args.reverseRecord } : {}),
|
|
174
|
-
...(args.ownerControlledFuses !== undefined ? { ownerControlledFuses: args.ownerControlledFuses } : {}),
|
|
175
|
-
})
|
|
176
|
-
const data = encodeFunctionData({
|
|
177
|
-
abi: CONTROLLER_ABI,
|
|
178
|
-
functionName: 'register',
|
|
179
|
-
args: [args.label, owner, args.durationSeconds, args.secret, built.resolver, built.data, built.reverseRecord, built.ownerControlledFuses],
|
|
180
|
-
})
|
|
181
|
-
const buffered = (args.rentPrice.total * (10000n + REGISTER_VALUE_BUFFER_BIPS)) / 10000n
|
|
182
|
-
return {
|
|
183
|
-
to: ETH_REGISTRAR_CONTROLLER_MAINNET,
|
|
184
|
-
data,
|
|
185
|
-
value: ('0x' + buffered.toString(16)) as Hex,
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function generateRegistrationSecret(): Hex {
|
|
190
|
-
const bytes = new Uint8Array(32)
|
|
191
|
-
if (typeof globalThis.crypto?.getRandomValues === 'function') {
|
|
192
|
-
globalThis.crypto.getRandomValues(bytes)
|
|
193
|
-
} else {
|
|
194
|
-
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256)
|
|
195
|
-
}
|
|
196
|
-
let hex = '0x'
|
|
197
|
-
for (const byte of bytes) hex += byte.toString(16).padStart(2, '0')
|
|
198
|
-
return hex as Hex
|
|
199
|
-
}
|