@agentex/agent 0.0.24 → 0.0.26
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/CHANGELOG.md +338 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/derived.d.ts +5 -3
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +11 -7
- package/dist/derived.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/acp/index.d.ts +1 -1
- package/dist/providers/acp/index.d.ts.map +1 -1
- package/dist/providers/acp/index.js +5 -97
- package/dist/providers/acp/index.js.map +1 -1
- package/dist/providers/acp/session.d.ts +8 -1
- package/dist/providers/acp/session.d.ts.map +1 -1
- package/dist/providers/acp/session.js +94 -0
- package/dist/providers/acp/session.js.map +1 -1
- package/dist/providers/claude/attach.d.ts +8 -0
- package/dist/providers/claude/attach.d.ts.map +1 -0
- package/dist/providers/claude/attach.js +113 -0
- package/dist/providers/claude/attach.js.map +1 -0
- package/dist/providers/claude/goal-capability.d.ts +15 -0
- package/dist/providers/claude/goal-capability.d.ts.map +1 -0
- package/dist/providers/claude/goal-capability.js +20 -0
- package/dist/providers/claude/goal-capability.js.map +1 -0
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +8 -4
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +11 -9
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +29 -14
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts +9 -0
- package/dist/providers/codex/attach.d.ts.map +1 -0
- package/dist/providers/codex/attach.js +93 -0
- package/dist/providers/codex/attach.js.map +1 -0
- package/dist/providers/codex/goal-capability.d.ts +13 -0
- package/dist/providers/codex/goal-capability.d.ts.map +1 -0
- package/dist/providers/codex/goal-capability.js +18 -0
- package/dist/providers/codex/goal-capability.js.map +1 -0
- package/dist/providers/codex/index.d.ts +1 -0
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +9 -6
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/session.d.ts +11 -7
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +24 -12
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +28 -0
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
- package/dist/providers/codex/transcript-normalize.js +191 -0
- package/dist/providers/codex/transcript-normalize.js.map +1 -0
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +2 -2
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/openclaw/index.d.ts.map +1 -1
- package/dist/providers/openclaw/index.js +2 -2
- package/dist/providers/openclaw/index.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +3 -5
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/pi/index.d.ts.map +1 -1
- package/dist/providers/pi/index.js +3 -5
- package/dist/providers/pi/index.js.map +1 -1
- package/dist/providers/process/index.d.ts.map +1 -1
- package/dist/providers/process/index.js +2 -2
- package/dist/providers/process/index.js.map +1 -1
- package/dist/registry.d.ts +0 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +0 -4
- package/dist/registry.js.map +1 -1
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/record.d.ts +43 -0
- package/dist/sessions/record.d.ts.map +1 -0
- package/dist/sessions/record.js +85 -0
- package/dist/sessions/record.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/uuid.d.ts +7 -1
- package/dist/utils/uuid.d.ts.map +1 -1
- package/dist/utils/uuid.js +21 -1
- package/dist/utils/uuid.js.map +1 -1
- package/package.json +64 -7
- package/src/derived.ts +311 -0
- package/src/goals/controller.ts +442 -0
- package/src/goals/index.ts +21 -0
- package/src/goals/normalize.ts +173 -0
- package/src/goals/sentinel.ts +90 -0
- package/src/index.ts +270 -0
- package/src/providers/_shared/http-agent.ts +304 -0
- package/src/providers/acp/index.ts +103 -0
- package/src/providers/acp/parse.ts +131 -0
- package/src/providers/acp/session.ts +744 -0
- package/src/providers/claude/attach.ts +147 -0
- package/src/providers/claude/codec.ts +43 -0
- package/src/providers/claude/execute.ts +300 -0
- package/src/providers/claude/goal-capability.ts +21 -0
- package/src/providers/claude/index.ts +72 -0
- package/src/providers/claude/mcp.ts +82 -0
- package/src/providers/claude/parse.ts +824 -0
- package/src/providers/claude/session.ts +1192 -0
- package/src/providers/claude/transcript.ts +555 -0
- package/src/providers/codex/attach.ts +123 -0
- package/src/providers/codex/codec.ts +50 -0
- package/src/providers/codex/execute.ts +337 -0
- package/src/providers/codex/goal-capability.ts +19 -0
- package/src/providers/codex/index.ts +57 -0
- package/src/providers/codex/modes.ts +159 -0
- package/src/providers/codex/parse.ts +691 -0
- package/src/providers/codex/plan-mode.ts +49 -0
- package/src/providers/codex/session.ts +1287 -0
- package/src/providers/codex/transcript-normalize.ts +197 -0
- package/src/providers/codex/transcript.ts +487 -0
- package/src/providers/codex/usage-scanner.ts +178 -0
- package/src/providers/copilot/index.ts +19 -0
- package/src/providers/cursor/codec.ts +44 -0
- package/src/providers/cursor/execute.ts +271 -0
- package/src/providers/cursor/index.ts +25 -0
- package/src/providers/cursor/parse.ts +288 -0
- package/src/providers/gemini/index.ts +21 -0
- package/src/providers/openclaw/codec.ts +40 -0
- package/src/providers/openclaw/execute.ts +19 -0
- package/src/providers/openclaw/index.ts +29 -0
- package/src/providers/opencode/codec.ts +50 -0
- package/src/providers/opencode/event-parse.ts +141 -0
- package/src/providers/opencode/execute.ts +251 -0
- package/src/providers/opencode/http-session.ts +427 -0
- package/src/providers/opencode/index.ts +30 -0
- package/src/providers/opencode/parse.ts +203 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/pi/codec.ts +44 -0
- package/src/providers/pi/execute.ts +297 -0
- package/src/providers/pi/index.ts +30 -0
- package/src/providers/pi/parse.ts +231 -0
- package/src/providers/pi/session.ts +381 -0
- package/src/providers/process/execute.ts +148 -0
- package/src/providers/process/index.ts +52 -0
- package/src/registry.ts +40 -0
- package/src/sessions/index.ts +8 -0
- package/src/sessions/record.ts +108 -0
- package/src/types.ts +1638 -0
- package/src/utils/ask-user-question.ts +57 -0
- package/src/utils/auth.ts +661 -0
- package/src/utils/binary.ts +179 -0
- package/src/utils/endpoint.ts +172 -0
- package/src/utils/env.ts +63 -0
- package/src/utils/execute-all.ts +68 -0
- package/src/utils/exit-plan-mode.ts +40 -0
- package/src/utils/instructions.ts +427 -0
- package/src/utils/process.ts +223 -0
- package/src/utils/runtime-config.ts +100 -0
- package/src/utils/runtime-homes.ts +49 -0
- package/src/utils/skill-commands.ts +493 -0
- package/src/utils/skills.ts +500 -0
- package/src/utils/template.ts +16 -0
- package/src/utils/tool-names.ts +51 -0
- package/src/utils/uuid.ts +21 -0
- package/src/utils/workspace.ts +156 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import type {
|
|
5
|
+
AuthIdentity,
|
|
6
|
+
AuthOption,
|
|
7
|
+
AuthReport,
|
|
8
|
+
AuthResolveContext,
|
|
9
|
+
BinaryStatus,
|
|
10
|
+
ProviderModule,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { buildEnv, ensurePathInEnv } from "./env.js";
|
|
13
|
+
import { findBinary } from "./binary.js";
|
|
14
|
+
import { runChildProcess } from "./process.js";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Legacy env-only billing classifier — kept for ExecutionResult.billingType
|
|
18
|
+
// prediction. Do not use for new code; prefer resolveAuthForProvider.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface ResolvedAuth {
|
|
22
|
+
method: "api_key" | "bedrock" | "oauth" | "subscription";
|
|
23
|
+
billingType: "api" | "metered_api" | "subscription";
|
|
24
|
+
resolveModelId?(requestedModel: string): string;
|
|
25
|
+
region?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hasEnv(env: Record<string, string>, key: string): boolean {
|
|
29
|
+
const v = env[key];
|
|
30
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function fileExists(p: string): Promise<boolean> {
|
|
34
|
+
try {
|
|
35
|
+
const stat = await fs.stat(p);
|
|
36
|
+
return stat.isFile();
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function providerHome(envVar: string, defaultDir: string): string {
|
|
43
|
+
const override = process.env[envVar];
|
|
44
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
45
|
+
return override;
|
|
46
|
+
}
|
|
47
|
+
return path.join(os.homedir(), defaultDir);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function bedrockModelId(model: string, region?: string): string {
|
|
51
|
+
const prefix = region ?? "us";
|
|
52
|
+
const mapping: Record<string, string> = {
|
|
53
|
+
"claude-sonnet-4-6": `${prefix}.anthropic.claude-sonnet-4-6-v1`,
|
|
54
|
+
"claude-opus-4-6": `${prefix}.anthropic.claude-opus-4-6-v1`,
|
|
55
|
+
"claude-haiku-4-5": `${prefix}.anthropic.claude-haiku-4-5-v1`,
|
|
56
|
+
"claude-3.5-sonnet": `${prefix}.anthropic.claude-3-5-sonnet-20241022-v2:0`,
|
|
57
|
+
"claude-3.5-haiku": `${prefix}.anthropic.claude-3-5-haiku-20241022-v1:0`,
|
|
58
|
+
};
|
|
59
|
+
return mapping[model] ?? model;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function detectAuth(providerType: string, env: Record<string, string>): ResolvedAuth {
|
|
63
|
+
switch (providerType) {
|
|
64
|
+
case "claude": {
|
|
65
|
+
if (hasEnv(env, "ANTHROPIC_BEDROCK_BASE_URL") || (hasEnv(env, "AWS_ACCESS_KEY_ID") && hasEnv(env, "AWS_REGION"))) {
|
|
66
|
+
const region = env["AWS_REGION"]?.trim();
|
|
67
|
+
return {
|
|
68
|
+
method: "bedrock",
|
|
69
|
+
billingType: "metered_api",
|
|
70
|
+
region,
|
|
71
|
+
resolveModelId: (model: string) => bedrockModelId(model, region),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (hasEnv(env, "ANTHROPIC_API_KEY")) {
|
|
75
|
+
return { method: "api_key", billingType: "api" };
|
|
76
|
+
}
|
|
77
|
+
return { method: "subscription", billingType: "subscription" };
|
|
78
|
+
}
|
|
79
|
+
case "codex": {
|
|
80
|
+
if (hasEnv(env, "OPENAI_API_KEY")) {
|
|
81
|
+
return { method: "api_key", billingType: "api" };
|
|
82
|
+
}
|
|
83
|
+
return { method: "subscription", billingType: "subscription" };
|
|
84
|
+
}
|
|
85
|
+
case "gemini": {
|
|
86
|
+
if (hasEnv(env, "GEMINI_API_KEY") || hasEnv(env, "GOOGLE_API_KEY")) {
|
|
87
|
+
return { method: "api_key", billingType: "api" };
|
|
88
|
+
}
|
|
89
|
+
return { method: "subscription", billingType: "subscription" };
|
|
90
|
+
}
|
|
91
|
+
case "cursor": {
|
|
92
|
+
if (hasEnv(env, "CURSOR_API_KEY") || hasEnv(env, "OPENAI_API_KEY")) {
|
|
93
|
+
return { method: "api_key", billingType: "api" };
|
|
94
|
+
}
|
|
95
|
+
return { method: "subscription", billingType: "subscription" };
|
|
96
|
+
}
|
|
97
|
+
case "opencode":
|
|
98
|
+
case "pi":
|
|
99
|
+
return { method: "api_key", billingType: "api" };
|
|
100
|
+
default:
|
|
101
|
+
return { method: "subscription", billingType: "subscription" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Binary status probe — spawns `<cli> --version` to populate BinaryStatus.
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
async function checkBinary(
|
|
110
|
+
name: string,
|
|
111
|
+
configOverride: string | undefined,
|
|
112
|
+
env: Record<string, string>,
|
|
113
|
+
): Promise<BinaryStatus & { prefixArgs: string[] }> {
|
|
114
|
+
let resolved;
|
|
115
|
+
try {
|
|
116
|
+
resolved = await findBinary(name, configOverride);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
installed: false,
|
|
120
|
+
prefixArgs: [],
|
|
121
|
+
error: err instanceof Error ? err.message : String(err),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let version: string | undefined;
|
|
126
|
+
try {
|
|
127
|
+
ensurePathInEnv(env);
|
|
128
|
+
const proc = await runChildProcess({
|
|
129
|
+
runId: "check-binary",
|
|
130
|
+
command: resolved.bin,
|
|
131
|
+
args: [...resolved.prefixArgs, "--version"],
|
|
132
|
+
cwd: process.cwd(),
|
|
133
|
+
env,
|
|
134
|
+
timeoutSec: 5,
|
|
135
|
+
});
|
|
136
|
+
if (proc.exitCode === 0) {
|
|
137
|
+
const out = (proc.stdout || proc.stderr).trim();
|
|
138
|
+
const match = out.match(/\b\d+\.\d+(?:\.\d+)?(?:[-+][\w.-]+)?\b/);
|
|
139
|
+
version = match ? match[0] : (out.split(/\s+/)[0] || undefined);
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Version detection is best-effort; binary is still "installed" if findBinary succeeded.
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
installed: true,
|
|
147
|
+
resolvedPath: resolved.bin,
|
|
148
|
+
prefixArgs: resolved.prefixArgs,
|
|
149
|
+
version,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Claude — `claude auth status --json`
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
interface ClaudeAuthStatusJson {
|
|
158
|
+
loggedIn?: boolean;
|
|
159
|
+
authMethod?: string; // e.g. "claude.ai", "api_key", "bedrock"
|
|
160
|
+
apiProvider?: string; // e.g. "firstParty", "bedrock", "vertex", "foundry"
|
|
161
|
+
email?: string;
|
|
162
|
+
orgId?: string;
|
|
163
|
+
orgName?: string;
|
|
164
|
+
subscriptionType?: string; // e.g. "max", "pro", "team", "enterprise"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function classifyClaudeAuthMethod(raw: string | undefined): "subscription" | "api_key" | "bedrock" | null {
|
|
168
|
+
if (!raw) return null;
|
|
169
|
+
const m = raw.toLowerCase();
|
|
170
|
+
if (m.includes("bedrock") || m.includes("vertex") || m.includes("foundry")) return "bedrock";
|
|
171
|
+
if (m.includes("claude.ai") || m.includes("oauth") || m.includes("subscription")) return "subscription";
|
|
172
|
+
if (m.includes("api") || m.includes("console")) return "api_key";
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function resolveClaudeAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
|
|
177
|
+
const env = buildEnv(ctx?.env);
|
|
178
|
+
const binary = await checkBinary("claude", ctx?.command, env);
|
|
179
|
+
|
|
180
|
+
const apiKeyOption: AuthOption = {
|
|
181
|
+
method: "api_key",
|
|
182
|
+
source: { kind: "env", var: "ANTHROPIC_API_KEY" },
|
|
183
|
+
present: hasEnv(env, "ANTHROPIC_API_KEY"),
|
|
184
|
+
};
|
|
185
|
+
const bedrockPresent =
|
|
186
|
+
hasEnv(env, "ANTHROPIC_BEDROCK_BASE_URL") ||
|
|
187
|
+
(hasEnv(env, "AWS_ACCESS_KEY_ID") && hasEnv(env, "AWS_REGION"));
|
|
188
|
+
const bedrockOption: AuthOption = {
|
|
189
|
+
method: "bedrock",
|
|
190
|
+
source: {
|
|
191
|
+
kind: "env_combo",
|
|
192
|
+
vars: ["ANTHROPIC_BEDROCK_BASE_URL", "AWS_ACCESS_KEY_ID", "AWS_REGION"],
|
|
193
|
+
},
|
|
194
|
+
present: bedrockPresent,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Binary missing → fall back to filesystem/keychain heuristic.
|
|
198
|
+
if (!binary.installed) {
|
|
199
|
+
const subOption: AuthOption = process.platform === "darwin"
|
|
200
|
+
? {
|
|
201
|
+
method: "subscription",
|
|
202
|
+
source: { kind: "keychain", service: "Claude Code" },
|
|
203
|
+
present: false, // can't verify without binary
|
|
204
|
+
}
|
|
205
|
+
: await (async () => {
|
|
206
|
+
const credPath = path.join(
|
|
207
|
+
providerHome("CLAUDE_CONFIG_DIR", ".claude"),
|
|
208
|
+
".credentials.json",
|
|
209
|
+
);
|
|
210
|
+
return {
|
|
211
|
+
method: "subscription" as const,
|
|
212
|
+
source: { kind: "file" as const, path: credPath },
|
|
213
|
+
present: await fileExists(credPath),
|
|
214
|
+
};
|
|
215
|
+
})();
|
|
216
|
+
return {
|
|
217
|
+
providerType: "claude",
|
|
218
|
+
binary: stripPrefixArgs(binary),
|
|
219
|
+
options: [apiKeyOption, bedrockOption, subOption],
|
|
220
|
+
source: "filesystem",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Binary present — try `claude auth status --json`.
|
|
225
|
+
try {
|
|
226
|
+
ensurePathInEnv(env);
|
|
227
|
+
const proc = await runChildProcess({
|
|
228
|
+
runId: "claude-auth-status",
|
|
229
|
+
command: binary.resolvedPath!,
|
|
230
|
+
args: [...binary.prefixArgs, "auth", "status", "--json"],
|
|
231
|
+
cwd: process.cwd(),
|
|
232
|
+
env,
|
|
233
|
+
timeoutSec: 10,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (proc.exitCode === 0 && proc.stdout.trim()) {
|
|
237
|
+
const data: ClaudeAuthStatusJson = JSON.parse(proc.stdout);
|
|
238
|
+
const active = classifyClaudeAuthMethod(data.authMethod);
|
|
239
|
+
const loggedIn = data.loggedIn === true;
|
|
240
|
+
|
|
241
|
+
const subPresent = loggedIn && (active === "subscription" || active === null);
|
|
242
|
+
const subOption: AuthOption = {
|
|
243
|
+
method: "subscription",
|
|
244
|
+
source: { kind: "cli", command: "claude auth status --json" },
|
|
245
|
+
present: subPresent,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Identity
|
|
249
|
+
const identity: AuthIdentity = {};
|
|
250
|
+
if (data.email) identity.email = data.email;
|
|
251
|
+
if (data.orgName) identity.orgName = data.orgName;
|
|
252
|
+
if (data.subscriptionType) identity.subscriptionType = data.subscriptionType;
|
|
253
|
+
if (data.authMethod) identity.authMethod = data.authMethod;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
providerType: "claude",
|
|
257
|
+
binary: stripPrefixArgs(binary),
|
|
258
|
+
options: [apiKeyOption, bedrockOption, subOption],
|
|
259
|
+
identity: Object.keys(identity).length > 0 ? identity : undefined,
|
|
260
|
+
source: "cli",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// fall through to filesystem
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// CLI call failed (old version, parse error, etc). Fall back to filesystem.
|
|
268
|
+
const subOption: AuthOption = process.platform === "darwin"
|
|
269
|
+
? {
|
|
270
|
+
method: "subscription",
|
|
271
|
+
source: { kind: "keychain", service: "Claude Code" },
|
|
272
|
+
present: false,
|
|
273
|
+
}
|
|
274
|
+
: await (async () => {
|
|
275
|
+
const credPath = path.join(
|
|
276
|
+
providerHome("CLAUDE_CONFIG_DIR", ".claude"),
|
|
277
|
+
".credentials.json",
|
|
278
|
+
);
|
|
279
|
+
return {
|
|
280
|
+
method: "subscription" as const,
|
|
281
|
+
source: { kind: "file" as const, path: credPath },
|
|
282
|
+
present: await fileExists(credPath),
|
|
283
|
+
};
|
|
284
|
+
})();
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
providerType: "claude",
|
|
288
|
+
binary: stripPrefixArgs(binary),
|
|
289
|
+
options: [apiKeyOption, bedrockOption, subOption],
|
|
290
|
+
source: "filesystem",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Codex — `codex login status`. Text-only output; regex with multiple
|
|
296
|
+
// tolerant patterns + filesystem fallback for forward-compat.
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
const CODEX_SUBSCRIPTION_PATTERNS: RegExp[] = [
|
|
300
|
+
/logged\s+in\s+using\s+chatgpt/i,
|
|
301
|
+
/logged\s+in\s+with\s+chatgpt/i,
|
|
302
|
+
/authenticated\s+via\s+chatgpt/i,
|
|
303
|
+
/signed\s+in.*chatgpt/i,
|
|
304
|
+
/using\s+subscription/i,
|
|
305
|
+
];
|
|
306
|
+
const CODEX_API_KEY_PATTERNS: RegExp[] = [
|
|
307
|
+
/logged\s+in\s+using\s+(an?\s+)?api[-_ ]?key/i,
|
|
308
|
+
/logged\s+in\s+with\s+(an?\s+)?api[-_ ]?key/i,
|
|
309
|
+
/authenticated\s+via\s+api[-_ ]?key/i,
|
|
310
|
+
/signed\s+in.*api[-_ ]?key/i,
|
|
311
|
+
];
|
|
312
|
+
const CODEX_NOT_LOGGED_IN_PATTERNS: RegExp[] = [
|
|
313
|
+
/not\s+logged\s+in/i,
|
|
314
|
+
/not\s+authenticated/i,
|
|
315
|
+
/please\s+(run\s+)?`?codex\s+login`?/i,
|
|
316
|
+
/no\s+authentication/i,
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
async function resolveCodexAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
|
|
320
|
+
const env = buildEnv(ctx?.env);
|
|
321
|
+
const binary = await checkBinary("codex", ctx?.command, env);
|
|
322
|
+
|
|
323
|
+
const apiKeyOption: AuthOption = {
|
|
324
|
+
method: "api_key",
|
|
325
|
+
source: { kind: "env", var: "OPENAI_API_KEY" },
|
|
326
|
+
present: hasEnv(env, "OPENAI_API_KEY"),
|
|
327
|
+
};
|
|
328
|
+
const authPath = path.join(providerHome("CODEX_HOME", ".codex"), "auth.json");
|
|
329
|
+
|
|
330
|
+
if (!binary.installed) {
|
|
331
|
+
const subPresent = await fileExists(authPath);
|
|
332
|
+
return {
|
|
333
|
+
providerType: "codex",
|
|
334
|
+
binary: stripPrefixArgs(binary),
|
|
335
|
+
options: [
|
|
336
|
+
apiKeyOption,
|
|
337
|
+
{
|
|
338
|
+
method: "subscription",
|
|
339
|
+
source: { kind: "file", path: authPath },
|
|
340
|
+
present: subPresent,
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
source: "filesystem",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let subscriptionPresent = false;
|
|
348
|
+
let activeAuthMethod: string | undefined;
|
|
349
|
+
let usedCli = false;
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
ensurePathInEnv(env);
|
|
353
|
+
const proc = await runChildProcess({
|
|
354
|
+
runId: "codex-login-status",
|
|
355
|
+
command: binary.resolvedPath!,
|
|
356
|
+
args: [...binary.prefixArgs, "login", "status"],
|
|
357
|
+
cwd: process.cwd(),
|
|
358
|
+
env,
|
|
359
|
+
timeoutSec: 10,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const out = `${proc.stdout}\n${proc.stderr}`.trim();
|
|
363
|
+
|
|
364
|
+
if (CODEX_SUBSCRIPTION_PATTERNS.some((r) => r.test(out))) {
|
|
365
|
+
subscriptionPresent = true;
|
|
366
|
+
activeAuthMethod = "chatgpt";
|
|
367
|
+
usedCli = true;
|
|
368
|
+
} else if (CODEX_API_KEY_PATTERNS.some((r) => r.test(out))) {
|
|
369
|
+
// API key is the active auth; subscription status unknown from this output alone.
|
|
370
|
+
// Trust the filesystem to know if a subscription is ALSO stored.
|
|
371
|
+
subscriptionPresent = await fileExists(authPath);
|
|
372
|
+
activeAuthMethod = "api_key";
|
|
373
|
+
usedCli = true;
|
|
374
|
+
} else if (CODEX_NOT_LOGGED_IN_PATTERNS.some((r) => r.test(out))) {
|
|
375
|
+
subscriptionPresent = false;
|
|
376
|
+
usedCli = true;
|
|
377
|
+
} else {
|
|
378
|
+
// Unknown wording (forward-compat) — fall back to filesystem.
|
|
379
|
+
subscriptionPresent = await fileExists(authPath);
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
subscriptionPresent = await fileExists(authPath);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
providerType: "codex",
|
|
387
|
+
binary: stripPrefixArgs(binary),
|
|
388
|
+
options: [
|
|
389
|
+
apiKeyOption,
|
|
390
|
+
{
|
|
391
|
+
method: "subscription",
|
|
392
|
+
source: usedCli
|
|
393
|
+
? { kind: "cli", command: "codex login status" }
|
|
394
|
+
: { kind: "file", path: authPath },
|
|
395
|
+
present: subscriptionPresent,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
identity: activeAuthMethod ? { authMethod: activeAuthMethod } : undefined,
|
|
399
|
+
source: usedCli ? "cli" : "filesystem",
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Gemini — filesystem-only (no `gemini auth status` subcommand exists).
|
|
405
|
+
// Still runs `gemini --version` to populate BinaryStatus.
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
async function resolveGeminiAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
|
|
409
|
+
const env = buildEnv(ctx?.env);
|
|
410
|
+
const binary = await checkBinary("gemini", ctx?.command, env);
|
|
411
|
+
|
|
412
|
+
const credPath = path.join(providerHome("GEMINI_CONFIG_DIR", ".gemini"), "oauth_creds.json");
|
|
413
|
+
return {
|
|
414
|
+
providerType: "gemini",
|
|
415
|
+
binary: stripPrefixArgs(binary),
|
|
416
|
+
options: [
|
|
417
|
+
{
|
|
418
|
+
method: "api_key",
|
|
419
|
+
source: { kind: "env", var: "GEMINI_API_KEY" },
|
|
420
|
+
present: hasEnv(env, "GEMINI_API_KEY"),
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
method: "api_key",
|
|
424
|
+
source: { kind: "env", var: "GOOGLE_API_KEY" },
|
|
425
|
+
present: hasEnv(env, "GOOGLE_API_KEY"),
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
method: "subscription",
|
|
429
|
+
source: { kind: "file", path: credPath },
|
|
430
|
+
present: await fileExists(credPath),
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
source: "filesystem",
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// Cursor / OpenCode / Pi / OpenClaw / Process — no CLI auth commands;
|
|
439
|
+
// straightforward env-only reports with binary status when applicable.
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
async function resolveCursorAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
|
|
443
|
+
const env = buildEnv(ctx?.env);
|
|
444
|
+
const binary = await checkBinary("agent", ctx?.command, env);
|
|
445
|
+
return {
|
|
446
|
+
providerType: "cursor",
|
|
447
|
+
binary: stripPrefixArgs(binary),
|
|
448
|
+
options: [
|
|
449
|
+
{ method: "api_key", source: { kind: "env", var: "CURSOR_API_KEY" }, present: hasEnv(env, "CURSOR_API_KEY") },
|
|
450
|
+
{ method: "api_key", source: { kind: "env", var: "OPENAI_API_KEY" }, present: hasEnv(env, "OPENAI_API_KEY") },
|
|
451
|
+
],
|
|
452
|
+
source: "filesystem",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function resolveOpencodeAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
|
|
457
|
+
const env = buildEnv(ctx?.env);
|
|
458
|
+
const binary = await checkBinary("opencode", ctx?.command, env);
|
|
459
|
+
return {
|
|
460
|
+
providerType: "opencode",
|
|
461
|
+
binary: stripPrefixArgs(binary),
|
|
462
|
+
options: [
|
|
463
|
+
{ method: "api_key", source: { kind: "env", var: "OPENAI_API_KEY" }, present: hasEnv(env, "OPENAI_API_KEY") },
|
|
464
|
+
{ method: "api_key", source: { kind: "env", var: "ANTHROPIC_API_KEY" }, present: hasEnv(env, "ANTHROPIC_API_KEY") },
|
|
465
|
+
],
|
|
466
|
+
source: "filesystem",
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function resolvePiAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
|
|
471
|
+
const env = buildEnv(ctx?.env);
|
|
472
|
+
const binary = await checkBinary("pi", ctx?.command, env);
|
|
473
|
+
return {
|
|
474
|
+
providerType: "pi",
|
|
475
|
+
binary: stripPrefixArgs(binary),
|
|
476
|
+
options: [
|
|
477
|
+
{ method: "api_key", source: { kind: "env", var: "OPENAI_API_KEY" }, present: hasEnv(env, "OPENAI_API_KEY") },
|
|
478
|
+
{ method: "api_key", source: { kind: "env", var: "ANTHROPIC_API_KEY" }, present: hasEnv(env, "ANTHROPIC_API_KEY") },
|
|
479
|
+
],
|
|
480
|
+
source: "filesystem",
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function stripPrefixArgs(b: BinaryStatus & { prefixArgs: string[] }): BinaryStatus {
|
|
485
|
+
const { prefixArgs: _prefixArgs, ...rest } = b;
|
|
486
|
+
return rest;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// Dispatcher + 60s cache
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
const CACHE_TTL_MS = 60_000;
|
|
494
|
+
const cache = new Map<string, { report: AuthReport; ts: number }>();
|
|
495
|
+
|
|
496
|
+
function cacheKey(providerType: string, ctx?: AuthResolveContext): string {
|
|
497
|
+
const envKey = ctx?.env ? JSON.stringify(ctx.env) : "";
|
|
498
|
+
const cmdKey = ctx?.command ?? "";
|
|
499
|
+
return `${providerType}|${envKey}|${cmdKey}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function resolveInternal(providerType: string, ctx?: AuthResolveContext): Promise<AuthReport> {
|
|
503
|
+
switch (providerType) {
|
|
504
|
+
case "codex":
|
|
505
|
+
return resolveCodexAuth(ctx);
|
|
506
|
+
case "claude":
|
|
507
|
+
return resolveClaudeAuth(ctx);
|
|
508
|
+
case "gemini":
|
|
509
|
+
return resolveGeminiAuth(ctx);
|
|
510
|
+
case "cursor":
|
|
511
|
+
return resolveCursorAuth(ctx);
|
|
512
|
+
case "opencode":
|
|
513
|
+
return resolveOpencodeAuth(ctx);
|
|
514
|
+
case "pi":
|
|
515
|
+
return resolvePiAuth(ctx);
|
|
516
|
+
default:
|
|
517
|
+
return {
|
|
518
|
+
providerType,
|
|
519
|
+
binary: { installed: false, error: `No auth resolver for provider "${providerType}"` },
|
|
520
|
+
options: [],
|
|
521
|
+
source: "filesystem",
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Resolve the auth report for a provider by type. Cached for 60s per
|
|
528
|
+
* (providerType, env, command). Pass `{ fresh: true }` to bypass the cache.
|
|
529
|
+
*/
|
|
530
|
+
export async function resolveAuthForProvider(
|
|
531
|
+
providerType: string,
|
|
532
|
+
ctx?: AuthResolveContext,
|
|
533
|
+
): Promise<AuthReport> {
|
|
534
|
+
if (!ctx?.fresh) {
|
|
535
|
+
const key = cacheKey(providerType, ctx);
|
|
536
|
+
const hit = cache.get(key);
|
|
537
|
+
if (hit && Date.now() - hit.ts < CACHE_TTL_MS) {
|
|
538
|
+
return hit.report;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const report = await resolveInternal(providerType, ctx);
|
|
543
|
+
|
|
544
|
+
const key = cacheKey(providerType, ctx);
|
|
545
|
+
cache.set(key, { report, ts: Date.now() });
|
|
546
|
+
return report;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Clear the resolveAuth cache. Primarily for tests and explicit refresh. */
|
|
550
|
+
export function clearAuthCache(): void {
|
|
551
|
+
cache.clear();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
// Sugar: method-specific presence checks. Each commits the caller to a
|
|
556
|
+
// billing mode, making the choice visible at the call site.
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
|
|
559
|
+
function anyPresent(report: AuthReport, method: AuthOption["method"]): boolean {
|
|
560
|
+
return report.options.some((o) => o.method === method && o.present === true);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** True only if a subscription credential is confirmed present. */
|
|
564
|
+
export async function hasSubscription(
|
|
565
|
+
provider: ProviderModule,
|
|
566
|
+
ctx?: AuthResolveContext,
|
|
567
|
+
): Promise<boolean> {
|
|
568
|
+
const report = await provider.resolveAuth(ctx);
|
|
569
|
+
return anyPresent(report, "subscription");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** True only if an API key is confirmed set in the env. */
|
|
573
|
+
export async function hasApiKey(
|
|
574
|
+
provider: ProviderModule,
|
|
575
|
+
ctx?: AuthResolveContext,
|
|
576
|
+
): Promise<boolean> {
|
|
577
|
+
const report = await provider.resolveAuth(ctx);
|
|
578
|
+
return anyPresent(report, "api_key");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** True only if Bedrock credentials are confirmed present in the env. */
|
|
582
|
+
export async function hasBedrock(
|
|
583
|
+
provider: ProviderModule,
|
|
584
|
+
ctx?: AuthResolveContext,
|
|
585
|
+
): Promise<boolean> {
|
|
586
|
+
const report = await provider.resolveAuth(ctx);
|
|
587
|
+
return anyPresent(report, "bedrock");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// Top-level convenience: "is the user logged in?" + "what command should I
|
|
592
|
+
// surface in a re-auth banner?". Pair these with the StreamEvent
|
|
593
|
+
// `auth_required` to drive a complete login UX from a host application.
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Shell command a host should surface when an `auth_required` event fires
|
|
598
|
+
* for `providerType`, or when `isLoggedIn` returns false. Values are the
|
|
599
|
+
* provider's canonical CLI subcommand for interactive login — they spawn
|
|
600
|
+
* a browser flow (Claude, Codex, Gemini, Cursor) or print API-key setup
|
|
601
|
+
* instructions (OpenCode, Pi).
|
|
602
|
+
*
|
|
603
|
+
* Unknown provider types fall back to `${providerType} login`, which is
|
|
604
|
+
* intentionally guess-y: if a new provider is added without updating this
|
|
605
|
+
* function, the host gets a plausible string instead of a thrown error.
|
|
606
|
+
*/
|
|
607
|
+
export function loginCommandFor(providerType: string): string {
|
|
608
|
+
switch (providerType) {
|
|
609
|
+
case "claude":
|
|
610
|
+
return "claude auth login";
|
|
611
|
+
case "codex":
|
|
612
|
+
return "codex login";
|
|
613
|
+
case "gemini":
|
|
614
|
+
// Gemini CLI uses `gemini` interactive mode for OAuth + `/auth` slash
|
|
615
|
+
// command; there's no top-level `gemini auth login` subcommand. The
|
|
616
|
+
// closest non-interactive path is launching `gemini` itself, which
|
|
617
|
+
// triggers an OAuth flow on first run.
|
|
618
|
+
return "gemini";
|
|
619
|
+
case "cursor":
|
|
620
|
+
return "cursor-agent login";
|
|
621
|
+
case "opencode":
|
|
622
|
+
return "opencode auth login";
|
|
623
|
+
case "pi":
|
|
624
|
+
// pi-coding-agent uses env-var auth (OPENAI_API_KEY / ANTHROPIC_API_KEY);
|
|
625
|
+
// there's no login subcommand. Tell the user to set the env var.
|
|
626
|
+
return "export OPENAI_API_KEY=... or ANTHROPIC_API_KEY=...";
|
|
627
|
+
default:
|
|
628
|
+
return `${providerType} login`;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Returns true when the provider has at least one credential confirmed
|
|
634
|
+
* present (any of api_key, bedrock, subscription). Wraps
|
|
635
|
+
* `resolveAuthForProvider` for callers that just want a yes/no.
|
|
636
|
+
*
|
|
637
|
+
* For Claude, this honors `claude auth status --json` (the authoritative
|
|
638
|
+
* source) via the existing `resolveAuth` path — the binary is consulted
|
|
639
|
+
* first and the keychain/file fallback is used only when the binary is
|
|
640
|
+
* missing or its status subcommand fails.
|
|
641
|
+
*
|
|
642
|
+
* Note: this is a snapshot of credential *presence*, not validity. A
|
|
643
|
+
* present but expired OAuth token still returns true here, because the
|
|
644
|
+
* status CLI itself reports `loggedIn: true` until the next refresh
|
|
645
|
+
* attempt fails. The definitive "is this credential actually accepted by
|
|
646
|
+
* the API?" check happens at request time and surfaces as an
|
|
647
|
+
* `auth_required` StreamEvent — pair the two:
|
|
648
|
+
*
|
|
649
|
+
* 1. Call `isLoggedIn` before starting a session to short-circuit
|
|
650
|
+
* obvious "never logged in" cases.
|
|
651
|
+
* 2. Listen for `auth_required` events during the session to catch
|
|
652
|
+
* tokens that were valid at presence-check time but expired/revoked
|
|
653
|
+
* in flight.
|
|
654
|
+
*/
|
|
655
|
+
export async function isLoggedIn(
|
|
656
|
+
providerType: string,
|
|
657
|
+
ctx?: AuthResolveContext,
|
|
658
|
+
): Promise<boolean> {
|
|
659
|
+
const report = await resolveAuthForProvider(providerType, ctx);
|
|
660
|
+
return report.options.some((o) => o.present);
|
|
661
|
+
}
|