u-foo 2.2.4 → 2.3.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/SKILLS/ufoo/SKILL.md +56 -12
- package/SKILLS/uinit/SKILL.md +3 -2
- package/modules/AGENTS.template.md +2 -1
- package/modules/bus/README.md +1 -1
- package/modules/context/SKILLS/uctx/SKILL.md +6 -4
- package/package.json +1 -1
- package/src/agent/activityStatePublisher.js +6 -2
- package/src/agent/codexThreadProvider.js +2 -2
- package/src/agent/controllerToolExecutor.js +24 -1
- package/src/agent/credentials/claude.js +85 -16
- package/src/agent/credentials/codex.js +251 -23
- package/src/agent/defaultBootstrap.js +3 -1
- package/src/agent/directAuthStatus.js +264 -0
- package/src/agent/internalRunner.js +18 -12
- package/src/agent/loopObservability.js +10 -0
- package/src/agent/loopRuntime.js +19 -0
- package/src/agent/notifier.js +12 -3
- package/src/agent/ufooAgent.js +43 -13
- package/src/agent/upstreamTransport.js +23 -8
- package/src/bus/index.js +6 -1
- package/src/bus/message.js +156 -8
- package/src/chat/commandExecutor.js +187 -7
- package/src/chat/commands.js +23 -4
- package/src/chat/completionController.js +30 -7
- package/src/chat/index.js +3 -5
- package/src/cli/groupCoreCommands.js +5 -0
- package/src/cli.js +309 -0
- package/src/code/UCODE_PROMPT.md +3 -2
- package/src/code/prompts/ufoo.js +3 -2
- package/src/config.js +16 -3
- package/src/context/doctor.js +1 -1
- package/src/daemon/groupOrchestrator.js +13 -9
- package/src/daemon/promptRequest.js +11 -2
- package/src/daemon/soloBootstrap.js +2 -0
- package/src/group/bootstrap.js +1 -1
- package/src/group/promptProfiles.js +106 -22
- package/src/group/templates.js +1 -0
- package/src/init/index.js +4 -0
- package/src/memory/historySearch.js +308 -0
- package/src/memory/index.js +653 -8
- package/src/providerapi/redactor.js +4 -1
- package/src/status/index.js +24 -1
- package/src/tools/handlers/memory.js +168 -0
- package/src/tools/index.js +12 -0
- package/src/tools/registry.js +12 -0
- package/src/tools/schemaFixtures.js +213 -0
- package/src/tools/tier1/editMemory.js +14 -0
- package/src/tools/tier1/forget.js +14 -0
- package/src/tools/tier1/recall.js +14 -0
- package/src/tools/tier1/remember.js +14 -0
- package/src/tools/tier1/searchHistory.js +14 -0
- package/src/tools/tier1/searchMemory.js +14 -0
- package/templates/groups/build-lane.json +44 -6
- package/templates/groups/build-ultra.json +6 -5
- package/templates/groups/design-system.json +84 -0
- package/templates/groups/product-discovery.json +9 -4
- package/templates/groups/ui-plan-review.json +84 -0
- package/templates/groups/ui-polish.json +6 -2
- package/templates/groups/verify-ship.json +9 -4
|
@@ -8,6 +8,17 @@ const {
|
|
|
8
8
|
parseTimestamp,
|
|
9
9
|
} = require("./index");
|
|
10
10
|
|
|
11
|
+
const CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
12
|
+
const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
13
|
+
const DEFAULT_REFRESH_WINDOW_MS = 300 * 1000;
|
|
14
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 3000;
|
|
15
|
+
const DEFAULT_LOCK_RETRY_MS = 25;
|
|
16
|
+
const DEFAULT_STALE_LOCK_MS = 30 * 1000;
|
|
17
|
+
|
|
18
|
+
function sleep(ms) {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
function defaultCodexConfigDir() {
|
|
12
23
|
return path.join(os.homedir(), ".codex");
|
|
13
24
|
}
|
|
@@ -19,6 +30,7 @@ function resolveCodexAuthPaths(options = {}) {
|
|
|
19
30
|
return {
|
|
20
31
|
configDir,
|
|
21
32
|
authPath,
|
|
33
|
+
lockPath: `${authPath}.lock`,
|
|
22
34
|
};
|
|
23
35
|
}
|
|
24
36
|
|
|
@@ -34,6 +46,78 @@ function decodeJwtPayload(token = "") {
|
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
function firstString(...values) {
|
|
50
|
+
for (const value of values) {
|
|
51
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function codexAuthClaims(claims = {}) {
|
|
57
|
+
const info = claims["https://api.openai.com/auth"];
|
|
58
|
+
return info && typeof info === "object" && !Array.isArray(info) ? info : {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function deriveAccountIdFromClaims(claims = {}) {
|
|
62
|
+
const info = codexAuthClaims(claims);
|
|
63
|
+
return firstString(
|
|
64
|
+
info.chatgpt_account_id,
|
|
65
|
+
info.account_id,
|
|
66
|
+
info.user_id,
|
|
67
|
+
claims.account_id,
|
|
68
|
+
claims.sub
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function deriveEmailFromClaims(claims = {}) {
|
|
73
|
+
return firstString(claims.email);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function deriveExpiresAtFromClaims(claims = {}) {
|
|
77
|
+
const exp = Number(claims.exp);
|
|
78
|
+
if (!Number.isFinite(exp) || exp <= 0) return "";
|
|
79
|
+
return new Date(exp * 1000).toISOString();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function withLockFile(lockPath, options = {}, fn) {
|
|
83
|
+
const fsModule = options.fsModule || fs;
|
|
84
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_LOCK_TIMEOUT_MS;
|
|
85
|
+
const retryMs = Number.isFinite(options.retryMs) ? options.retryMs : DEFAULT_LOCK_RETRY_MS;
|
|
86
|
+
const staleMs = Number.isFinite(options.staleMs) ? options.staleMs : DEFAULT_STALE_LOCK_MS;
|
|
87
|
+
const sleepFn = typeof options.sleep === "function" ? options.sleep : sleep;
|
|
88
|
+
const startedAt = Date.now();
|
|
89
|
+
|
|
90
|
+
fsModule.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
91
|
+
|
|
92
|
+
while ((Date.now() - startedAt) <= timeoutMs) {
|
|
93
|
+
let fd = null;
|
|
94
|
+
try {
|
|
95
|
+
fd = fsModule.openSync(lockPath, "wx", 0o600);
|
|
96
|
+
try {
|
|
97
|
+
return await fn();
|
|
98
|
+
} finally {
|
|
99
|
+
try { fsModule.closeSync(fd); } catch {}
|
|
100
|
+
try { fsModule.unlinkSync(lockPath); } catch {}
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (!err || err.code !== "EEXIST") throw err;
|
|
104
|
+
try {
|
|
105
|
+
const stat = fsModule.statSync(lockPath);
|
|
106
|
+
if ((Date.now() - stat.mtimeMs) > staleMs) {
|
|
107
|
+
fsModule.unlinkSync(lockPath);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
// eslint-disable-next-line no-await-in-loop
|
|
112
|
+
await sleepFn(retryMs);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const err = new Error(`Timed out waiting for Codex OAuth lock: ${lockPath}`);
|
|
117
|
+
err.code = "CODEX_AUTH_LOCK_TIMEOUT";
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
|
|
37
121
|
function parseCodexAuthFile(raw = {}) {
|
|
38
122
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
39
123
|
const err = new Error("Codex auth payload must be a JSON object");
|
|
@@ -42,23 +126,19 @@ function parseCodexAuthFile(raw = {}) {
|
|
|
42
126
|
}
|
|
43
127
|
|
|
44
128
|
const tokens = raw.tokens && typeof raw.tokens === "object" ? raw.tokens : null;
|
|
45
|
-
const apiKey =
|
|
46
|
-
const accessToken = tokens &&
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const expiresAt = typeof raw.expired === "string"
|
|
59
|
-
? raw.expired
|
|
60
|
-
: (typeof raw.expire === "string" ? raw.expire : "");
|
|
61
|
-
const email = typeof raw.email === "string" ? raw.email : "";
|
|
129
|
+
const apiKey = firstString(raw.OPENAI_API_KEY, raw.api_key);
|
|
130
|
+
const accessToken = firstString(tokens && tokens.access_token, raw.access_token);
|
|
131
|
+
const refreshToken = firstString(tokens && tokens.refresh_token, raw.refresh_token);
|
|
132
|
+
const idToken = firstString(tokens && tokens.id_token, raw.id_token);
|
|
133
|
+
const accountId = firstString(tokens && tokens.account_id, raw.account_id);
|
|
134
|
+
const expiresAt = firstString(
|
|
135
|
+
raw.expired,
|
|
136
|
+
raw.expire,
|
|
137
|
+
raw.expires_at,
|
|
138
|
+
tokens && tokens.expired,
|
|
139
|
+
tokens && tokens.expires_at
|
|
140
|
+
);
|
|
141
|
+
const email = firstString(raw.email, tokens && tokens.email);
|
|
62
142
|
|
|
63
143
|
if (!apiKey && !accessToken) {
|
|
64
144
|
const err = new Error("Unsupported Codex auth schema");
|
|
@@ -85,9 +165,18 @@ class CodexUpstreamCredentialResolver {
|
|
|
85
165
|
constructor(options = {}) {
|
|
86
166
|
this.fs = options.fsModule || fs;
|
|
87
167
|
this.env = options.env || process.env;
|
|
168
|
+
this.fetchImpl = options.fetchImpl || global.fetch;
|
|
88
169
|
this.now = typeof options.now === "function" ? options.now : () => Date.now();
|
|
89
170
|
this.paths = resolveCodexAuthPaths(options);
|
|
90
|
-
this.refreshWindowMs = Number.isFinite(options.refreshWindowMs) ? options.refreshWindowMs :
|
|
171
|
+
this.refreshWindowMs = Number.isFinite(options.refreshWindowMs) ? options.refreshWindowMs : DEFAULT_REFRESH_WINDOW_MS;
|
|
172
|
+
this.autoRefresh = options.autoRefresh !== false;
|
|
173
|
+
this.refreshRetries = Number.isInteger(options.refreshRetries) && options.refreshRetries > 0
|
|
174
|
+
? options.refreshRetries
|
|
175
|
+
: 2;
|
|
176
|
+
this.sleep = typeof options.sleep === "function" ? options.sleep : sleep;
|
|
177
|
+
this.lockTimeoutMs = Number.isFinite(options.lockTimeoutMs) ? options.lockTimeoutMs : DEFAULT_LOCK_TIMEOUT_MS;
|
|
178
|
+
this.lockRetryMs = Number.isFinite(options.lockRetryMs) ? options.lockRetryMs : DEFAULT_LOCK_RETRY_MS;
|
|
179
|
+
this.lockStaleMs = Number.isFinite(options.lockStaleMs) ? options.lockStaleMs : DEFAULT_STALE_LOCK_MS;
|
|
91
180
|
}
|
|
92
181
|
|
|
93
182
|
resolvePaths() {
|
|
@@ -98,19 +187,146 @@ class CodexUpstreamCredentialResolver {
|
|
|
98
187
|
const raw = JSON.parse(this.fs.readFileSync(this.paths.authPath, "utf8"));
|
|
99
188
|
const parsed = parseCodexAuthFile(raw);
|
|
100
189
|
const claims = decodeJwtPayload(parsed.idToken);
|
|
101
|
-
const derivedEmail =
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
: "";
|
|
190
|
+
const derivedEmail = deriveEmailFromClaims(claims);
|
|
191
|
+
const derivedAccountId = deriveAccountIdFromClaims(claims);
|
|
192
|
+
const derivedExpiresAt = deriveExpiresAtFromClaims(claims);
|
|
105
193
|
const expiresAt = parsed.expiresAt || derivedExpiresAt;
|
|
106
194
|
return {
|
|
107
195
|
...parsed,
|
|
108
196
|
accountEmail: parsed.email || derivedEmail,
|
|
197
|
+
accountId: parsed.accountId || derivedAccountId,
|
|
109
198
|
expiresAt,
|
|
110
199
|
expiresAtMs: parseTimestamp(expiresAt),
|
|
111
200
|
};
|
|
112
201
|
}
|
|
113
202
|
|
|
203
|
+
writeAuthFile(raw) {
|
|
204
|
+
const text = `${JSON.stringify(raw, null, 2)}\n`;
|
|
205
|
+
const tmpPath = `${this.paths.authPath}.tmp-${process.pid}-${Date.now()}`;
|
|
206
|
+
this.fs.mkdirSync(path.dirname(this.paths.authPath), { recursive: true, mode: 0o700 });
|
|
207
|
+
this.fs.writeFileSync(tmpPath, text, "utf8");
|
|
208
|
+
this.fs.renameSync(tmpPath, this.paths.authPath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async refreshTokens(refreshToken) {
|
|
212
|
+
if (typeof this.fetchImpl !== "function") {
|
|
213
|
+
const err = new Error("fetch is unavailable for Codex token refresh");
|
|
214
|
+
err.code = "CODEX_AUTH_REFRESH_UNAVAILABLE";
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let lastErr = null;
|
|
219
|
+
for (let attempt = 0; attempt < this.refreshRetries; attempt += 1) {
|
|
220
|
+
try {
|
|
221
|
+
const body = new URLSearchParams({
|
|
222
|
+
client_id: CODEX_OAUTH_CLIENT_ID,
|
|
223
|
+
grant_type: "refresh_token",
|
|
224
|
+
refresh_token: refreshToken,
|
|
225
|
+
scope: "openid profile email",
|
|
226
|
+
});
|
|
227
|
+
const response = await this.fetchImpl(CODEX_OAUTH_TOKEN_URL, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: {
|
|
230
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
231
|
+
accept: "application/json",
|
|
232
|
+
},
|
|
233
|
+
body: body.toString(),
|
|
234
|
+
});
|
|
235
|
+
const text = await response.text();
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
const err = new Error(`Codex token refresh failed (${response.status}): ${text.slice(0, 500)}`);
|
|
238
|
+
err.code = "CODEX_AUTH_REFRESH_FAILED";
|
|
239
|
+
err.status = response.status;
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
const payload = JSON.parse(text);
|
|
243
|
+
if (!payload || typeof payload !== "object" || !payload.access_token) {
|
|
244
|
+
const err = new Error("Codex token refresh response did not include access_token");
|
|
245
|
+
err.code = "CODEX_AUTH_REFRESH_SCHEMA_UNSUPPORTED";
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
const claims = decodeJwtPayload(payload.id_token);
|
|
249
|
+
return {
|
|
250
|
+
accessToken: firstString(payload.access_token),
|
|
251
|
+
refreshToken: firstString(payload.refresh_token, refreshToken),
|
|
252
|
+
idToken: firstString(payload.id_token),
|
|
253
|
+
tokenType: firstString(payload.token_type, "Bearer"),
|
|
254
|
+
expiresAt: Number.isFinite(Number(payload.expires_in))
|
|
255
|
+
? new Date(this.now() + Number(payload.expires_in) * 1000).toISOString()
|
|
256
|
+
: deriveExpiresAtFromClaims(claims),
|
|
257
|
+
accountId: deriveAccountIdFromClaims(claims),
|
|
258
|
+
email: deriveEmailFromClaims(claims),
|
|
259
|
+
};
|
|
260
|
+
} catch (err) {
|
|
261
|
+
lastErr = err;
|
|
262
|
+
if (err && (
|
|
263
|
+
err.code === "CODEX_AUTH_REFRESH_SCHEMA_UNSUPPORTED"
|
|
264
|
+
|| String(err.message || "").toLowerCase().includes("refresh_token_reused")
|
|
265
|
+
)) {
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const err = new Error(lastErr && lastErr.message ? lastErr.message : "Codex token refresh failed");
|
|
272
|
+
err.code = lastErr && lastErr.code ? lastErr.code : "CODEX_AUTH_REFRESH_FAILED";
|
|
273
|
+
err.cause = lastErr;
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async refreshAuthRecord(record) {
|
|
278
|
+
if (!record || !record.refreshToken) {
|
|
279
|
+
const err = new Error("Codex OAuth credential is expired and has no refresh token");
|
|
280
|
+
err.code = "CODEX_AUTH_REFRESH_UNAVAILABLE";
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return withLockFile(this.paths.lockPath, {
|
|
285
|
+
fsModule: this.fs,
|
|
286
|
+
timeoutMs: this.lockTimeoutMs,
|
|
287
|
+
retryMs: this.lockRetryMs,
|
|
288
|
+
staleMs: this.lockStaleMs,
|
|
289
|
+
sleep: this.sleep,
|
|
290
|
+
}, async () => {
|
|
291
|
+
const currentRecord = this.readAuthFile();
|
|
292
|
+
const currentDescriptor = this.buildResolvedCredential(currentRecord);
|
|
293
|
+
if (currentDescriptor.state === "fresh") {
|
|
294
|
+
return currentRecord;
|
|
295
|
+
}
|
|
296
|
+
if (!currentRecord.refreshToken) {
|
|
297
|
+
const err = new Error("Codex OAuth credential is expired and has no refresh token");
|
|
298
|
+
err.code = "CODEX_AUTH_REFRESH_UNAVAILABLE";
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const refreshed = await this.refreshTokens(currentRecord.refreshToken);
|
|
303
|
+
const nextRaw = currentRecord.raw && typeof currentRecord.raw === "object" && !Array.isArray(currentRecord.raw)
|
|
304
|
+
? { ...currentRecord.raw }
|
|
305
|
+
: {};
|
|
306
|
+
const existingTokens = nextRaw.tokens && typeof nextRaw.tokens === "object" && !Array.isArray(nextRaw.tokens)
|
|
307
|
+
? { ...nextRaw.tokens }
|
|
308
|
+
: {};
|
|
309
|
+
|
|
310
|
+
nextRaw.tokens = {
|
|
311
|
+
...existingTokens,
|
|
312
|
+
id_token: refreshed.idToken || existingTokens.id_token || currentRecord.idToken || "",
|
|
313
|
+
access_token: refreshed.accessToken,
|
|
314
|
+
refresh_token: refreshed.refreshToken || existingTokens.refresh_token || currentRecord.refreshToken || "",
|
|
315
|
+
account_id: refreshed.accountId || currentRecord.accountId || existingTokens.account_id || "",
|
|
316
|
+
};
|
|
317
|
+
nextRaw.last_refresh = new Date(this.now()).toISOString();
|
|
318
|
+
nextRaw.expired = refreshed.expiresAt || currentRecord.expiresAt || "";
|
|
319
|
+
if (refreshed.email || currentRecord.accountEmail || currentRecord.email) {
|
|
320
|
+
nextRaw.email = refreshed.email || currentRecord.accountEmail || currentRecord.email || "";
|
|
321
|
+
}
|
|
322
|
+
if (currentRecord.authMode && !nextRaw.auth_mode) {
|
|
323
|
+
nextRaw.auth_mode = currentRecord.authMode;
|
|
324
|
+
}
|
|
325
|
+
this.writeAuthFile(nextRaw);
|
|
326
|
+
return this.readAuthFile();
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
114
330
|
buildResolvedCredential(record) {
|
|
115
331
|
if (record.apiKey) {
|
|
116
332
|
return buildCredentialDescriptor({
|
|
@@ -185,7 +401,17 @@ class CodexUpstreamCredentialResolver {
|
|
|
185
401
|
}
|
|
186
402
|
throw err;
|
|
187
403
|
}
|
|
188
|
-
|
|
404
|
+
const descriptor = this.buildResolvedCredential(authRecord);
|
|
405
|
+
if (
|
|
406
|
+
this.autoRefresh
|
|
407
|
+
&& descriptor.credentialKind === "oauth"
|
|
408
|
+
&& descriptor.refreshable
|
|
409
|
+
&& (descriptor.state === "expired" || descriptor.state === "near_expiry")
|
|
410
|
+
) {
|
|
411
|
+
const refreshedRecord = await this.refreshAuthRecord(authRecord);
|
|
412
|
+
return this.buildResolvedCredential(refreshedRecord);
|
|
413
|
+
}
|
|
414
|
+
return descriptor;
|
|
189
415
|
}
|
|
190
416
|
}
|
|
191
417
|
|
|
@@ -200,4 +426,6 @@ module.exports = {
|
|
|
200
426
|
resolveCodexAuthPaths,
|
|
201
427
|
parseCodexAuthFile,
|
|
202
428
|
decodeJwtPayload,
|
|
429
|
+
deriveAccountIdFromClaims,
|
|
430
|
+
deriveExpiresAtFromClaims,
|
|
203
431
|
};
|
|
@@ -181,9 +181,11 @@ function resolveDefaultManualBootstrap({
|
|
|
181
181
|
const normalizedAgent = asTrimmedString(agentType).toLowerCase();
|
|
182
182
|
const currentEnv = env && typeof env === "object" ? env : {};
|
|
183
183
|
const currentArgs = Array.isArray(args) ? args.slice() : [];
|
|
184
|
+
const hasCodexStartupBootstrap = normalizedAgent === "codex"
|
|
185
|
+
&& Boolean(currentEnv.UFOO_STARTUP_BOOTSTRAP_TEXT);
|
|
184
186
|
if (
|
|
185
187
|
currentEnv.UFOO_SKIP_DEFAULT_BOOTSTRAP === "1"
|
|
186
|
-
||
|
|
188
|
+
|| hasCodexStartupBootstrap
|
|
187
189
|
|| hasMetaCommandArgs(currentArgs)
|
|
188
190
|
) {
|
|
189
191
|
return { args: currentArgs, env: {}, mode: "skip" };
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { loadConfig } = require("../config");
|
|
4
|
+
const {
|
|
5
|
+
resolveCodexAuthPaths,
|
|
6
|
+
resolveCodexUpstreamCredentials,
|
|
7
|
+
} = require("./credentials/codex");
|
|
8
|
+
const {
|
|
9
|
+
resolveClaudeOauthPaths,
|
|
10
|
+
resolveClaudeUpstreamCredentials,
|
|
11
|
+
} = require("./credentials/claude");
|
|
12
|
+
|
|
13
|
+
function normalizeRefreshWindowMs(value) {
|
|
14
|
+
const num = Number(value);
|
|
15
|
+
if (!Number.isFinite(num) || num < 0) return 300000;
|
|
16
|
+
return Math.floor(num * 1000);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeErrorCode(err, fallback = "DIRECT_AUTH_STATUS_FAILED") {
|
|
20
|
+
return String(err && err.code ? err.code : fallback).trim() || fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeDirectAuthProvider(value = "") {
|
|
24
|
+
const text = String(value || "").trim().toLowerCase();
|
|
25
|
+
if (text === "claude" || text === "claude-cli" || text === "claude-code" || text === "anthropic") return "claude";
|
|
26
|
+
return "codex";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatAccount(credential = {}) {
|
|
30
|
+
const email = String(credential.accountEmail || "").trim();
|
|
31
|
+
const accountId = String(credential.accountId || "").trim();
|
|
32
|
+
if (email && accountId) return `${email} (${accountId})`;
|
|
33
|
+
if (email) return email;
|
|
34
|
+
if (accountId) return accountId;
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatCompactAccount(status = {}) {
|
|
39
|
+
return String(status.accountEmail || status.account || status.accountId || "").trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatCompactExpires(value = "") {
|
|
43
|
+
const text = String(value || "").trim();
|
|
44
|
+
const match = text.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/);
|
|
45
|
+
if (!match) return text;
|
|
46
|
+
return `${match[1]} ${match[2]}${text.endsWith("Z") ? "Z" : ""}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function codexTransportFromCredential(credential = {}) {
|
|
50
|
+
return credential.credentialKind === "oauth" && credential.accessToken
|
|
51
|
+
? "codex-responses"
|
|
52
|
+
: "openai-chat";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function inspectCodexDirectAuth({
|
|
56
|
+
projectRoot,
|
|
57
|
+
env = process.env,
|
|
58
|
+
fetchImpl = global.fetch,
|
|
59
|
+
loadConfigImpl = loadConfig,
|
|
60
|
+
autoRefresh = false,
|
|
61
|
+
} = {}) {
|
|
62
|
+
const config = loadConfigImpl(projectRoot) || {};
|
|
63
|
+
const paths = resolveCodexAuthPaths({ authPath: config.codexAuthPath });
|
|
64
|
+
try {
|
|
65
|
+
const credential = await resolveCodexUpstreamCredentials({
|
|
66
|
+
authPath: config.codexAuthPath,
|
|
67
|
+
refreshWindowMs: normalizeRefreshWindowMs(config.codexOauthRefreshWindowSec),
|
|
68
|
+
fetchImpl,
|
|
69
|
+
env,
|
|
70
|
+
autoRefresh,
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
provider: "codex",
|
|
75
|
+
transport: codexTransportFromCredential(credential),
|
|
76
|
+
credentialKind: String(credential.credentialKind || ""),
|
|
77
|
+
source: String(credential.source || ""),
|
|
78
|
+
state: String(credential.state || ""),
|
|
79
|
+
refreshable: credential.refreshable === true,
|
|
80
|
+
account: formatAccount(credential),
|
|
81
|
+
accountId: String(credential.accountId || ""),
|
|
82
|
+
accountEmail: String(credential.accountEmail || ""),
|
|
83
|
+
expiresAt: String(credential.expiresAt || ""),
|
|
84
|
+
credentialPath: String(credential.credentialPath || paths.authPath || ""),
|
|
85
|
+
};
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
provider: "codex",
|
|
90
|
+
error: err && err.message ? err.message : "Codex direct API credentials are unavailable",
|
|
91
|
+
errorCode: normalizeErrorCode(err, "CODEX_AUTH_STATUS_FAILED"),
|
|
92
|
+
credentialPath: paths.authPath || "",
|
|
93
|
+
hint: "Run Codex login once or set OPENAI_API_KEY; ufoo-agent will not fall back to the CLI.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function inspectClaudeDirectAuth({
|
|
99
|
+
projectRoot,
|
|
100
|
+
env = process.env,
|
|
101
|
+
loadConfigImpl = loadConfig,
|
|
102
|
+
} = {}) {
|
|
103
|
+
const config = loadConfigImpl(projectRoot) || {};
|
|
104
|
+
const paths = resolveClaudeOauthPaths({
|
|
105
|
+
profile: config.claudeOauthProfile,
|
|
106
|
+
tokenPath: config.claudeOauthTokenPath,
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
const credential = await resolveClaudeUpstreamCredentials({
|
|
110
|
+
profile: config.claudeOauthProfile,
|
|
111
|
+
tokenPath: config.claudeOauthTokenPath,
|
|
112
|
+
refreshWindowMs: normalizeRefreshWindowMs(config.claudeOauthRefreshWindowSec),
|
|
113
|
+
env,
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
provider: "claude",
|
|
118
|
+
transport: "anthropic-messages",
|
|
119
|
+
credentialKind: String(credential.credentialKind || ""),
|
|
120
|
+
source: String(credential.source || ""),
|
|
121
|
+
state: String(credential.state || ""),
|
|
122
|
+
refreshable: credential.refreshable === true,
|
|
123
|
+
profile: String(credential.profile || paths.profile || ""),
|
|
124
|
+
expiresAt: String(credential.expiresAt || ""),
|
|
125
|
+
credentialPath: String(credential.credentialPath || paths.tokenPath || ""),
|
|
126
|
+
};
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
provider: "claude",
|
|
131
|
+
error: err && err.message ? err.message : "Claude direct API credentials are unavailable",
|
|
132
|
+
errorCode: normalizeErrorCode(err, "CLAUDE_AUTH_STATUS_FAILED"),
|
|
133
|
+
credentialPath: paths.tokenPath || "",
|
|
134
|
+
hint: "Use ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ~/.claude/settings.json, or Claude OAuth token file; restart chat/daemon if you changed shell env.",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function inspectDirectAuthStatus(options = {}) {
|
|
140
|
+
const { projectRoot, loadConfigImpl = loadConfig, provider = "" } = options;
|
|
141
|
+
const config = loadConfigImpl(projectRoot) || {};
|
|
142
|
+
const selected = normalizeDirectAuthProvider(provider || config.agentProvider || config.routerProvider || "");
|
|
143
|
+
const nextOptions = {
|
|
144
|
+
...options,
|
|
145
|
+
loadConfigImpl: () => config,
|
|
146
|
+
};
|
|
147
|
+
if (selected === "claude") {
|
|
148
|
+
return inspectClaudeDirectAuth(nextOptions);
|
|
149
|
+
}
|
|
150
|
+
return inspectCodexDirectAuth(nextOptions);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatCodexDirectAuthStatus(status = {}, options = {}) {
|
|
154
|
+
if (options.compact === true) {
|
|
155
|
+
if (status.ok) {
|
|
156
|
+
const credential = status.credentialKind || "credential";
|
|
157
|
+
const transport = status.transport || "unknown";
|
|
158
|
+
const state = status.state || "unknown";
|
|
159
|
+
const details = [
|
|
160
|
+
status.source || "",
|
|
161
|
+
formatCompactAccount(status),
|
|
162
|
+
status.expiresAt ? `expires ${formatCompactExpires(status.expiresAt)}` : "",
|
|
163
|
+
status.refreshable ? "refreshable" : "",
|
|
164
|
+
].filter(Boolean);
|
|
165
|
+
const lines = [
|
|
166
|
+
`Codex API: OK · ${credential}/${transport} · ${state}`,
|
|
167
|
+
];
|
|
168
|
+
if (details.length > 0) lines.push(` ${details.join(" · ")}`);
|
|
169
|
+
return lines;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const hint = String(status.hint || "Run Codex login once or set OPENAI_API_KEY.").replace(/;.*$/, ".");
|
|
173
|
+
return [
|
|
174
|
+
`Codex API: FAIL · ${status.errorCode || "CODEX_AUTH_STATUS_FAILED"}`,
|
|
175
|
+
` ${status.error || "Codex direct API credentials are unavailable"} · ${hint}`,
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (status.ok) {
|
|
180
|
+
const state = status.state || "unknown";
|
|
181
|
+
const lines = [
|
|
182
|
+
`Codex direct API: OK (${status.transport || "unknown"}, ${status.credentialKind || "credential"}, ${state})`,
|
|
183
|
+
` - source: ${status.source || "(unknown)"}`,
|
|
184
|
+
];
|
|
185
|
+
if (status.account) lines.push(` - account: ${status.account}`);
|
|
186
|
+
if (status.expiresAt) lines.push(` - expires: ${status.expiresAt}`);
|
|
187
|
+
if (status.credentialPath) lines.push(` - path: ${status.credentialPath}`);
|
|
188
|
+
if (status.refreshable) lines.push(" - refreshable: yes");
|
|
189
|
+
return lines;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const lines = [
|
|
193
|
+
`Codex direct API: FAIL (${status.errorCode || "CODEX_AUTH_STATUS_FAILED"})`,
|
|
194
|
+
` - ${status.error || "Codex direct API credentials are unavailable"}`,
|
|
195
|
+
];
|
|
196
|
+
if (status.credentialPath) lines.push(` - expected path: ${status.credentialPath}`);
|
|
197
|
+
if (status.hint) lines.push(` - ${status.hint}`);
|
|
198
|
+
return lines;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatClaudeDirectAuthStatus(status = {}, options = {}) {
|
|
202
|
+
if (options.compact === true) {
|
|
203
|
+
if (status.ok) {
|
|
204
|
+
const credential = status.credentialKind || "credential";
|
|
205
|
+
const transport = status.transport || "anthropic-messages";
|
|
206
|
+
const state = status.state || "unknown";
|
|
207
|
+
const details = [
|
|
208
|
+
status.source || "",
|
|
209
|
+
status.profile ? `profile ${status.profile}` : "",
|
|
210
|
+
status.expiresAt ? `expires ${formatCompactExpires(status.expiresAt)}` : "",
|
|
211
|
+
status.refreshable ? "refreshable" : "",
|
|
212
|
+
].filter(Boolean);
|
|
213
|
+
const lines = [
|
|
214
|
+
`Claude API: OK · ${credential}/${transport} · ${state}`,
|
|
215
|
+
];
|
|
216
|
+
if (details.length > 0) lines.push(` ${details.join(" · ")}`);
|
|
217
|
+
return lines;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const hint = String(status.hint || "Use ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, Claude settings, or Claude OAuth.").replace(/;.*$/, ".");
|
|
221
|
+
return [
|
|
222
|
+
`Claude API: FAIL · ${status.errorCode || "CLAUDE_AUTH_STATUS_FAILED"}`,
|
|
223
|
+
` ${status.error || "Claude direct API credentials are unavailable"} · ${hint}`,
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (status.ok) {
|
|
228
|
+
const state = status.state || "unknown";
|
|
229
|
+
const lines = [
|
|
230
|
+
`Claude direct API: OK (${status.transport || "anthropic-messages"}, ${status.credentialKind || "credential"}, ${state})`,
|
|
231
|
+
` - source: ${status.source || "(unknown)"}`,
|
|
232
|
+
];
|
|
233
|
+
if (status.profile) lines.push(` - profile: ${status.profile}`);
|
|
234
|
+
if (status.expiresAt) lines.push(` - expires: ${status.expiresAt}`);
|
|
235
|
+
if (status.credentialPath) lines.push(` - path: ${status.credentialPath}`);
|
|
236
|
+
if (status.refreshable) lines.push(" - refreshable: yes");
|
|
237
|
+
return lines;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const lines = [
|
|
241
|
+
`Claude direct API: FAIL (${status.errorCode || "CLAUDE_AUTH_STATUS_FAILED"})`,
|
|
242
|
+
` - ${status.error || "Claude direct API credentials are unavailable"}`,
|
|
243
|
+
];
|
|
244
|
+
if (status.credentialPath) lines.push(` - expected path: ${status.credentialPath}`);
|
|
245
|
+
if (status.hint) lines.push(` - ${status.hint}`);
|
|
246
|
+
return lines;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function formatDirectAuthStatus(status = {}, options = {}) {
|
|
250
|
+
if (status.provider === "claude") {
|
|
251
|
+
return formatClaudeDirectAuthStatus(status, options);
|
|
252
|
+
}
|
|
253
|
+
return formatCodexDirectAuthStatus(status, options);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
inspectDirectAuthStatus,
|
|
258
|
+
inspectCodexDirectAuth,
|
|
259
|
+
inspectClaudeDirectAuth,
|
|
260
|
+
formatDirectAuthStatus,
|
|
261
|
+
formatCodexDirectAuthStatus,
|
|
262
|
+
formatClaudeDirectAuthStatus,
|
|
263
|
+
normalizeDirectAuthProvider,
|
|
264
|
+
};
|
|
@@ -19,6 +19,7 @@ const { resolveClaudeUpstreamCredentials } = require("./credentials/claude");
|
|
|
19
19
|
const { buildUpstreamAuthFromCredential } = require("./credentials");
|
|
20
20
|
const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
|
|
21
21
|
const { redactToolCallPayload, redactSecrets } = require("../providerapi/redactor");
|
|
22
|
+
const { buildCachedMemoryPrefix } = require("../memory");
|
|
22
23
|
|
|
23
24
|
function sleep(ms) {
|
|
24
25
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -60,6 +61,14 @@ function safeSubscriber(subscriber) {
|
|
|
60
61
|
return subscriber.replace(/:/g, "_");
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
function buildMemoryPrefix(projectRoot, limit = 50) {
|
|
65
|
+
try {
|
|
66
|
+
return buildCachedMemoryPrefix(projectRoot, { limit }).prefix.trim();
|
|
67
|
+
} catch {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
function createBusSender(projectRoot, subscriber) {
|
|
64
73
|
const eventBus = new EventBus(projectRoot);
|
|
65
74
|
let sendQueue = Promise.resolve();
|
|
@@ -84,16 +93,8 @@ function createBusSender(projectRoot, subscriber) {
|
|
|
84
93
|
return { enqueue, flush };
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
function shouldFallbackToLegacyThreadProvider(
|
|
88
|
-
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
const code = String(err.code || "").trim().toUpperCase();
|
|
92
|
-
return (
|
|
93
|
-
code === "CLAUDE_AUTH_UNAVAILABLE"
|
|
94
|
-
|| code === "CLAUDE_OAUTH_SCHEMA_UNSUPPORTED"
|
|
95
|
-
|| code === "ANTHROPIC_SDK_UNAVAILABLE"
|
|
96
|
-
);
|
|
96
|
+
function shouldFallbackToLegacyThreadProvider() {
|
|
97
|
+
return false;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
function drainQueue(queueFile) {
|
|
@@ -143,7 +144,10 @@ async function handleEvent(
|
|
|
143
144
|
threadRuntime = null
|
|
144
145
|
) {
|
|
145
146
|
if (!evt || !evt.data || !evt.data.message) return;
|
|
146
|
-
const
|
|
147
|
+
const memoryPrefix = buildMemoryPrefix(projectRoot);
|
|
148
|
+
const prompt = memoryPrefix
|
|
149
|
+
? `${memoryPrefix}\n\n${evt.data.message}`
|
|
150
|
+
: evt.data.message;
|
|
147
151
|
const publisher = evt.publisher || "unknown";
|
|
148
152
|
const sandbox = "workspace-write";
|
|
149
153
|
const streamState = { emitted: false, lastChar: "" };
|
|
@@ -358,6 +362,8 @@ function buildWorkerThreadToolRuntime({ projectRoot, subscriber, observer }) {
|
|
|
358
362
|
projectRoot,
|
|
359
363
|
subscriber,
|
|
360
364
|
eventBus,
|
|
365
|
+
tool_call_id: toolCall.tool_call_id || toolCall.toolCallId || "",
|
|
366
|
+
turn_id: toolCall.turn_id || toolCall.turnId || "",
|
|
361
367
|
}, rawArgs);
|
|
362
368
|
const safeResult = redactSecrets(result);
|
|
363
369
|
emitAudit("post_call", { ...redactedPayload, result: safeResult });
|
|
@@ -407,7 +413,7 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
|
|
|
407
413
|
};
|
|
408
414
|
|
|
409
415
|
if (provider === "codex-cli") {
|
|
410
|
-
if (getCodexThreadMode(projectRoot) !== "
|
|
416
|
+
if (getCodexThreadMode(projectRoot) !== "api") {
|
|
411
417
|
return disabledRuntime;
|
|
412
418
|
}
|
|
413
419
|
|