erosolar-cli 1.0.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/ARCHITECTURE.json +157 -0
- package/Agents.md +207 -0
- package/LICENSE +21 -0
- package/SOURCE_OF_TRUTH.json +103 -0
- package/dist/adapters/browser/index.js +10 -0
- package/dist/adapters/node/index.js +34 -0
- package/dist/adapters/remote/index.js +19 -0
- package/dist/adapters/types.js +1 -0
- package/dist/bin/erosolar.js +6 -0
- package/dist/capabilities/bashCapability.js +23 -0
- package/dist/capabilities/filesystemCapability.js +23 -0
- package/dist/capabilities/index.js +3 -0
- package/dist/capabilities/searchCapability.js +23 -0
- package/dist/capabilities/tavilyCapability.js +26 -0
- package/dist/capabilities/toolRegistry.js +98 -0
- package/dist/config.js +60 -0
- package/dist/contracts/v1/agent.js +7 -0
- package/dist/contracts/v1/provider.js +6 -0
- package/dist/contracts/v1/tool.js +6 -0
- package/dist/core/agent.js +135 -0
- package/dist/core/agentProfiles.js +34 -0
- package/dist/core/contextWindow.js +29 -0
- package/dist/core/errors/apiKeyErrors.js +114 -0
- package/dist/core/preferences.js +157 -0
- package/dist/core/secretStore.js +143 -0
- package/dist/core/toolRuntime.js +180 -0
- package/dist/core/types.js +1 -0
- package/dist/plugins/providers/anthropic/index.js +24 -0
- package/dist/plugins/providers/deepseek/index.js +24 -0
- package/dist/plugins/providers/google/index.js +25 -0
- package/dist/plugins/providers/index.js +17 -0
- package/dist/plugins/providers/openai/index.js +25 -0
- package/dist/plugins/providers/xai/index.js +24 -0
- package/dist/plugins/tools/bash/localBashPlugin.js +13 -0
- package/dist/plugins/tools/filesystem/localFilesystemPlugin.js +13 -0
- package/dist/plugins/tools/index.js +2 -0
- package/dist/plugins/tools/nodeDefaults.js +16 -0
- package/dist/plugins/tools/registry.js +57 -0
- package/dist/plugins/tools/search/localSearchPlugin.js +13 -0
- package/dist/plugins/tools/tavily/tavilyPlugin.js +16 -0
- package/dist/providers/anthropicProvider.js +218 -0
- package/dist/providers/googleProvider.js +193 -0
- package/dist/providers/openaiChatCompletionsProvider.js +148 -0
- package/dist/providers/openaiResponsesProvider.js +182 -0
- package/dist/providers/providerFactory.js +21 -0
- package/dist/runtime/agentHost.js +152 -0
- package/dist/runtime/agentSession.js +65 -0
- package/dist/runtime/browser.js +9 -0
- package/dist/runtime/cloud.js +9 -0
- package/dist/runtime/node.js +10 -0
- package/dist/runtime/universal.js +28 -0
- package/dist/shell/__tests__/bracketedPasteManager.test.js +35 -0
- package/dist/shell/bracketedPasteManager.js +75 -0
- package/dist/shell/interactiveShell.js +1426 -0
- package/dist/shell/shellApp.js +392 -0
- package/dist/tools/bashTools.js +117 -0
- package/dist/tools/diffUtils.js +137 -0
- package/dist/tools/fileTools.js +232 -0
- package/dist/tools/searchTools.js +175 -0
- package/dist/tools/tavilyTools.js +176 -0
- package/dist/ui/__tests__/richText.test.js +36 -0
- package/dist/ui/codeHighlighter.js +843 -0
- package/dist/ui/designSystem.js +98 -0
- package/dist/ui/display.js +731 -0
- package/dist/ui/layout.js +108 -0
- package/dist/ui/richText.js +318 -0
- package/dist/ui/theme.js +91 -0
- package/dist/workspace.js +44 -0
- package/package.json +62 -0
- package/scripts/preinstall-clean-bins.mjs +66 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createTavilyTools } from '../tools/tavilyTools.js';
|
|
2
|
+
export class TavilyCapabilityModule {
|
|
3
|
+
id = 'capability.tavily';
|
|
4
|
+
options;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
if (!options?.apiKey) {
|
|
7
|
+
throw new Error('TavilyCapabilityModule requires an API key.');
|
|
8
|
+
}
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
async create(_context) {
|
|
12
|
+
const tools = createTavilyTools({
|
|
13
|
+
apiKey: this.options.apiKey,
|
|
14
|
+
baseUrl: this.options.baseUrl,
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
id: 'tavily.tools.web',
|
|
18
|
+
description: 'Live Tavily web search and extraction tools.',
|
|
19
|
+
toolSuite: {
|
|
20
|
+
id: 'tavily.web',
|
|
21
|
+
description: 'Internet search and content extraction via Tavily.',
|
|
22
|
+
tools,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getSecretValue } from '../core/secretStore.js';
|
|
2
|
+
const TOOL_OPTIONS = [
|
|
3
|
+
{
|
|
4
|
+
id: 'filesystem',
|
|
5
|
+
label: 'File operations',
|
|
6
|
+
description: 'Read, write, list, and search files inside the workspace.',
|
|
7
|
+
defaultEnabled: true,
|
|
8
|
+
pluginIds: ['tool.filesystem.local'],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: 'search',
|
|
12
|
+
label: 'Repository search',
|
|
13
|
+
description: 'Structural and glob-aware search helpers (grep, find definition).',
|
|
14
|
+
defaultEnabled: true,
|
|
15
|
+
pluginIds: ['tool.search.local'],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'bash',
|
|
19
|
+
label: 'Shell access',
|
|
20
|
+
description: 'Run commands inside a sandboxed Bash shell.',
|
|
21
|
+
defaultEnabled: true,
|
|
22
|
+
pluginIds: ['tool.bash.local'],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'tavily',
|
|
26
|
+
label: 'Tavily web search',
|
|
27
|
+
description: 'Live web search and extraction powered by Tavily.',
|
|
28
|
+
defaultEnabled: false,
|
|
29
|
+
pluginIds: ['tool.tavily.web'],
|
|
30
|
+
requiresSecret: 'TAVILY_API_KEY',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
const PLUGIN_TO_OPTION = buildPluginLookup();
|
|
34
|
+
export function getToolToggleOptions() {
|
|
35
|
+
return [...TOOL_OPTIONS];
|
|
36
|
+
}
|
|
37
|
+
export function buildEnabledToolSet(saved) {
|
|
38
|
+
const enabled = new Set();
|
|
39
|
+
if (!saved) {
|
|
40
|
+
for (const option of TOOL_OPTIONS) {
|
|
41
|
+
if (option.defaultEnabled) {
|
|
42
|
+
enabled.add(option.id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return enabled;
|
|
46
|
+
}
|
|
47
|
+
const knownIds = new Set(TOOL_OPTIONS.map((option) => option.id));
|
|
48
|
+
for (const id of saved.enabledTools ?? []) {
|
|
49
|
+
if (knownIds.has(id)) {
|
|
50
|
+
enabled.add(id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return enabled;
|
|
54
|
+
}
|
|
55
|
+
export function evaluateToolPermissions(selection) {
|
|
56
|
+
const allowedPluginIds = new Set();
|
|
57
|
+
const warnings = [];
|
|
58
|
+
for (const option of TOOL_OPTIONS) {
|
|
59
|
+
if (!selection.has(option.id)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (option.requiresSecret) {
|
|
63
|
+
const secret = getSecretValue(option.requiresSecret);
|
|
64
|
+
if (!secret) {
|
|
65
|
+
warnings.push({
|
|
66
|
+
id: option.id,
|
|
67
|
+
label: option.label,
|
|
68
|
+
reason: 'missing-secret',
|
|
69
|
+
secretId: option.requiresSecret,
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const pluginId of option.pluginIds) {
|
|
75
|
+
allowedPluginIds.add(pluginId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
allowedPluginIds,
|
|
80
|
+
warnings,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function isPluginEnabled(pluginId, allowedPluginIds) {
|
|
84
|
+
const associated = PLUGIN_TO_OPTION.get(pluginId);
|
|
85
|
+
if (!associated) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return allowedPluginIds.has(pluginId);
|
|
89
|
+
}
|
|
90
|
+
function buildPluginLookup() {
|
|
91
|
+
const map = new Map();
|
|
92
|
+
for (const option of TOOL_OPTIONS) {
|
|
93
|
+
for (const pluginId of option.pluginIds) {
|
|
94
|
+
map.set(pluginId, option);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return map;
|
|
98
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { registerAgentProfile, hasAgentProfile, getAgentProfile, } from './core/agentProfiles.js';
|
|
2
|
+
const DEFAULT_PROFILES = [
|
|
3
|
+
{
|
|
4
|
+
name: 'general',
|
|
5
|
+
label: 'Erosolar',
|
|
6
|
+
description: 'General-purpose operator with balanced reasoning across research, planning, writing, and coding tasks.',
|
|
7
|
+
defaultProvider: 'openai',
|
|
8
|
+
defaultModel: 'gpt-5.1',
|
|
9
|
+
defaultSystemPrompt: 'You are the Erosolar General Agent, a multi-domain operator who can plan work, write documents, reason about data, and modify code with equal rigor. Always ground answers in verifiable evidence: cite files, command output, or captured context. Narrate your intent, decompose complex requests into steps, and reach for tools whenever direct inspection or execution is required.',
|
|
10
|
+
temperature: 0.2,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'erosolar-code',
|
|
14
|
+
label: 'Erosolar Code',
|
|
15
|
+
description: 'OpenAI-tuned coding specialist optimized for rapid edits with deterministic grounding.',
|
|
16
|
+
defaultProvider: 'openai',
|
|
17
|
+
defaultModel: 'gpt-5.1-codex',
|
|
18
|
+
defaultSystemPrompt: 'You are the Erosolar CLI, a powerful AI coding assistant with full capabilities. You can read and write files, execute bash commands, search codebases, and handle complex multi-step tasks. Be proactive and use tools to accomplish tasks effectively.',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
for (const profile of DEFAULT_PROFILES) {
|
|
22
|
+
if (!hasAgentProfile(profile.name)) {
|
|
23
|
+
registerAgentProfile(profile);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function resolveProfileConfig(profile, workspaceContext) {
|
|
27
|
+
const blueprint = getAgentProfile(profile);
|
|
28
|
+
const envPrefix = toEnvPrefix(blueprint.name);
|
|
29
|
+
const modelEnv = process.env[`${envPrefix}_MODEL`];
|
|
30
|
+
const modelLocked = typeof modelEnv === 'string' && modelEnv.trim().length > 0;
|
|
31
|
+
const model = modelLocked ? modelEnv.trim() : blueprint.defaultModel;
|
|
32
|
+
// System prompt can still be customized via environment if needed
|
|
33
|
+
const systemPrompt = process.env[`${envPrefix}_SYSTEM_PROMPT`] ?? blueprint.defaultSystemPrompt;
|
|
34
|
+
const providerEnv = process.env[`${envPrefix}_PROVIDER`];
|
|
35
|
+
const providerLocked = isProviderValue(providerEnv);
|
|
36
|
+
const provider = providerLocked ? providerEnv.trim() : blueprint.defaultProvider;
|
|
37
|
+
const contextBlock = workspaceContext?.trim()
|
|
38
|
+
? `\n\nWorkspace context (auto-detected):\n${workspaceContext.trim()}`
|
|
39
|
+
: '';
|
|
40
|
+
return {
|
|
41
|
+
profile,
|
|
42
|
+
label: blueprint.label,
|
|
43
|
+
provider,
|
|
44
|
+
model,
|
|
45
|
+
temperature: blueprint.temperature,
|
|
46
|
+
maxTokens: blueprint.maxTokens,
|
|
47
|
+
systemPrompt: `${systemPrompt.trim()}${contextBlock}`,
|
|
48
|
+
modelLocked,
|
|
49
|
+
providerLocked,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function toEnvPrefix(profile) {
|
|
53
|
+
return profile
|
|
54
|
+
.trim()
|
|
55
|
+
.toUpperCase()
|
|
56
|
+
.replace(/[^A-Z0-9]/g, '_');
|
|
57
|
+
}
|
|
58
|
+
function isProviderValue(value) {
|
|
59
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
60
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export class AgentRuntime {
|
|
2
|
+
messages = [];
|
|
3
|
+
provider;
|
|
4
|
+
toolRuntime;
|
|
5
|
+
callbacks;
|
|
6
|
+
activeRun = null;
|
|
7
|
+
baseSystemPrompt;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.provider = options.provider;
|
|
10
|
+
this.toolRuntime = options.toolRuntime;
|
|
11
|
+
this.callbacks = options.callbacks ?? {};
|
|
12
|
+
const trimmedPrompt = options.systemPrompt.trim();
|
|
13
|
+
this.baseSystemPrompt = trimmedPrompt || null;
|
|
14
|
+
if (trimmedPrompt) {
|
|
15
|
+
this.messages.push({ role: 'system', content: trimmedPrompt });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async send(text) {
|
|
19
|
+
const prompt = text.trim();
|
|
20
|
+
if (!prompt) {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
this.messages.push({ role: 'user', content: prompt });
|
|
24
|
+
const run = { startedAt: Date.now() };
|
|
25
|
+
this.activeRun = run;
|
|
26
|
+
try {
|
|
27
|
+
return await this.processConversation();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (this.activeRun === run) {
|
|
31
|
+
this.activeRun = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async processConversation() {
|
|
36
|
+
while (true) {
|
|
37
|
+
const response = await this.provider.generate(this.messages, this.providerTools);
|
|
38
|
+
const usage = response.usage ?? null;
|
|
39
|
+
if (response.type === 'tool_calls') {
|
|
40
|
+
const narration = response.content?.trim();
|
|
41
|
+
if (narration) {
|
|
42
|
+
this.emitAssistantMessage(narration, { isFinal: false, usage });
|
|
43
|
+
}
|
|
44
|
+
this.messages.push({
|
|
45
|
+
role: 'assistant',
|
|
46
|
+
content: response.content ?? '',
|
|
47
|
+
toolCalls: response.toolCalls,
|
|
48
|
+
});
|
|
49
|
+
await this.resolveToolCalls(response.toolCalls);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const reply = response.content?.trim() ?? '';
|
|
53
|
+
if (reply) {
|
|
54
|
+
this.emitAssistantMessage(reply, { isFinal: true, usage });
|
|
55
|
+
}
|
|
56
|
+
this.messages.push({ role: 'assistant', content: reply });
|
|
57
|
+
return reply;
|
|
58
|
+
}
|
|
59
|
+
throw new Error('Agent loop exited unexpectedly.');
|
|
60
|
+
}
|
|
61
|
+
async resolveToolCalls(toolCalls) {
|
|
62
|
+
for (const call of toolCalls) {
|
|
63
|
+
const output = await this.toolRuntime.execute(call);
|
|
64
|
+
this.messages.push({
|
|
65
|
+
role: 'tool',
|
|
66
|
+
name: call.name,
|
|
67
|
+
toolCallId: call.id,
|
|
68
|
+
content: output,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
get providerTools() {
|
|
73
|
+
return this.toolRuntime.listProviderTools();
|
|
74
|
+
}
|
|
75
|
+
emitAssistantMessage(content, metadata) {
|
|
76
|
+
if (!content) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const elapsedMs = this.activeRun ? Date.now() - this.activeRun.startedAt : undefined;
|
|
80
|
+
this.callbacks.onAssistantMessage?.(content, {
|
|
81
|
+
...metadata,
|
|
82
|
+
elapsedMs,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
getHistory() {
|
|
86
|
+
return this.messages.map(cloneMessage);
|
|
87
|
+
}
|
|
88
|
+
loadHistory(history) {
|
|
89
|
+
this.messages.length = 0;
|
|
90
|
+
if (history.length === 0) {
|
|
91
|
+
if (this.baseSystemPrompt) {
|
|
92
|
+
this.messages.push({ role: 'system', content: this.baseSystemPrompt });
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const message of history) {
|
|
97
|
+
this.messages.push(cloneMessage(message));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
clearHistory() {
|
|
101
|
+
this.messages.length = 0;
|
|
102
|
+
if (this.baseSystemPrompt) {
|
|
103
|
+
this.messages.push({ role: 'system', content: this.baseSystemPrompt });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function cloneMessage(message) {
|
|
108
|
+
switch (message.role) {
|
|
109
|
+
case 'assistant':
|
|
110
|
+
return {
|
|
111
|
+
role: 'assistant',
|
|
112
|
+
content: message.content,
|
|
113
|
+
toolCalls: message.toolCalls?.map(cloneToolCall),
|
|
114
|
+
};
|
|
115
|
+
case 'tool':
|
|
116
|
+
return {
|
|
117
|
+
role: 'tool',
|
|
118
|
+
name: message.name,
|
|
119
|
+
content: message.content,
|
|
120
|
+
toolCallId: message.toolCallId,
|
|
121
|
+
};
|
|
122
|
+
case 'system':
|
|
123
|
+
return { role: 'system', content: message.content };
|
|
124
|
+
case 'user':
|
|
125
|
+
default:
|
|
126
|
+
return { role: 'user', content: message.content };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function cloneToolCall(call) {
|
|
130
|
+
return {
|
|
131
|
+
id: call.id,
|
|
132
|
+
name: call.name,
|
|
133
|
+
arguments: { ...(call.arguments ?? {}) },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const registry = new Map();
|
|
2
|
+
export function registerAgentProfile(blueprint) {
|
|
3
|
+
if (!blueprint?.name) {
|
|
4
|
+
throw new Error('Agent profile name is required.');
|
|
5
|
+
}
|
|
6
|
+
const trimmedName = blueprint.name.trim();
|
|
7
|
+
if (!trimmedName) {
|
|
8
|
+
throw new Error('Agent profile name cannot be blank.');
|
|
9
|
+
}
|
|
10
|
+
const payload = Object.freeze({
|
|
11
|
+
...blueprint,
|
|
12
|
+
name: trimmedName,
|
|
13
|
+
label: blueprint.label.trim() || trimmedName,
|
|
14
|
+
frozen: true,
|
|
15
|
+
});
|
|
16
|
+
registry.set(trimmedName, payload);
|
|
17
|
+
}
|
|
18
|
+
export function hasAgentProfile(name) {
|
|
19
|
+
return registry.has(name.trim());
|
|
20
|
+
}
|
|
21
|
+
export function getAgentProfile(name) {
|
|
22
|
+
const profile = registry.get(name.trim());
|
|
23
|
+
if (!profile) {
|
|
24
|
+
const known = listAgentProfiles()
|
|
25
|
+
.map((entry) => entry.name)
|
|
26
|
+
.sort()
|
|
27
|
+
.join(', ');
|
|
28
|
+
throw new Error(`Unknown profile "${name}". Registered profiles: ${known || 'none'}.`);
|
|
29
|
+
}
|
|
30
|
+
return profile;
|
|
31
|
+
}
|
|
32
|
+
export function listAgentProfiles() {
|
|
33
|
+
return Array.from(registry.values());
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const MODEL_CONTEXT_WINDOWS = [
|
|
2
|
+
{ pattern: /^gpt-5\.1-?codex$/i, tokens: 200_000 },
|
|
3
|
+
{ pattern: /^gpt-5(?:\.1|-?pro|-?mini|-?nano)/i, tokens: 200_000 },
|
|
4
|
+
{ pattern: /^claude-sonnet-4[-.]?5/i, tokens: 200_000 },
|
|
5
|
+
{ pattern: /^claude-opus-4[-.]?1/i, tokens: 200_000 },
|
|
6
|
+
{ pattern: /^claude-haiku-4[-.]?5/i, tokens: 200_000 },
|
|
7
|
+
{ pattern: /sonnet-4[-.]?5/i, tokens: 200_000 },
|
|
8
|
+
{ pattern: /opus-4[-.]?1/i, tokens: 200_000 },
|
|
9
|
+
{ pattern: /haiku-4[-.]?5/i, tokens: 200_000 },
|
|
10
|
+
];
|
|
11
|
+
/**
|
|
12
|
+
* Returns the approximate context window (in tokens) for the provided model id.
|
|
13
|
+
* Falls back to null when the model is unknown so callers can handle gracefully.
|
|
14
|
+
*/
|
|
15
|
+
export function getContextWindowTokens(model) {
|
|
16
|
+
if (!model) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const normalized = model.trim();
|
|
20
|
+
if (!normalized) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of MODEL_CONTEXT_WINDOWS) {
|
|
24
|
+
if (entry.pattern.test(normalized)) {
|
|
25
|
+
return entry.tokens;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { MissingSecretError, getSecretDefinitionForProvider, } from '../secretStore.js';
|
|
2
|
+
export function detectApiKeyError(error, provider) {
|
|
3
|
+
if (error instanceof MissingSecretError) {
|
|
4
|
+
const primaryProvider = error.secret.providers[0] ?? null;
|
|
5
|
+
return {
|
|
6
|
+
type: 'missing',
|
|
7
|
+
provider: provider ?? primaryProvider,
|
|
8
|
+
secret: error.secret,
|
|
9
|
+
message: error.message,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (isUnauthorizedError(error)) {
|
|
13
|
+
const labelProvider = provider ?? extractProviderFromError(error);
|
|
14
|
+
const secret = labelProvider ? getSecretDefinitionForProvider(labelProvider) : null;
|
|
15
|
+
return {
|
|
16
|
+
type: 'invalid',
|
|
17
|
+
provider: labelProvider,
|
|
18
|
+
secret,
|
|
19
|
+
message: extractErrorMessage(error),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function isUnauthorizedError(error) {
|
|
25
|
+
const status = extractStatus(error);
|
|
26
|
+
if (status === 401 || status === 403) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const payload = extractStructuredError(error);
|
|
30
|
+
if (payload) {
|
|
31
|
+
const normalizedType = normalize(payload.type) || normalize(payload.code);
|
|
32
|
+
if (normalizedType && containsAuthKeyword(normalizedType)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (payload.message && containsAuthKeyword(normalize(payload.message))) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const message = normalize(extractErrorMessage(error));
|
|
40
|
+
if (!message) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return containsAuthKeyword(message);
|
|
44
|
+
}
|
|
45
|
+
function extractStatus(error) {
|
|
46
|
+
if (!error || typeof error !== 'object') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const directStatus = error.status;
|
|
50
|
+
if (typeof directStatus === 'number') {
|
|
51
|
+
return directStatus;
|
|
52
|
+
}
|
|
53
|
+
const response = error.response;
|
|
54
|
+
if (response && typeof response.status === 'number') {
|
|
55
|
+
return response.status;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function extractStructuredError(error) {
|
|
60
|
+
if (!error || typeof error !== 'object') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if ('error' in error) {
|
|
64
|
+
const candidate = error.error;
|
|
65
|
+
if (candidate && typeof candidate === 'object') {
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function extractProviderFromError(error) {
|
|
72
|
+
if (!error || typeof error !== 'object') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const provider = error.provider;
|
|
76
|
+
if (typeof provider === 'string' && provider.trim()) {
|
|
77
|
+
return provider.trim();
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function extractErrorMessage(error) {
|
|
82
|
+
if (typeof error === 'string') {
|
|
83
|
+
return error;
|
|
84
|
+
}
|
|
85
|
+
if (error instanceof Error) {
|
|
86
|
+
return error.message ?? '';
|
|
87
|
+
}
|
|
88
|
+
if (error && typeof error === 'object') {
|
|
89
|
+
const payload = extractStructuredError(error);
|
|
90
|
+
if (payload?.message) {
|
|
91
|
+
return payload.message;
|
|
92
|
+
}
|
|
93
|
+
if ('message' in error && typeof error.message === 'string') {
|
|
94
|
+
return error.message;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
function containsAuthKeyword(value) {
|
|
100
|
+
if (!value) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return (value.includes('api key') ||
|
|
104
|
+
value.includes('apikey') ||
|
|
105
|
+
value.includes('api-key') ||
|
|
106
|
+
value.includes('authentication') ||
|
|
107
|
+
value.includes('unauthorized'));
|
|
108
|
+
}
|
|
109
|
+
function normalize(value) {
|
|
110
|
+
if (!value) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return value.toLowerCase();
|
|
114
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const CONFIG_DIR = join(homedir(), '.erosolar');
|
|
5
|
+
const SETTINGS_PATH = join(CONFIG_DIR, 'settings.json');
|
|
6
|
+
const CURRENT_VERSION = 2;
|
|
7
|
+
export function loadActiveProfilePreference() {
|
|
8
|
+
const payload = readSettingsFile();
|
|
9
|
+
if (!payload?.activeProfile) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return normalizeProfileNameValue(payload.activeProfile);
|
|
13
|
+
}
|
|
14
|
+
export function saveActiveProfilePreference(profile) {
|
|
15
|
+
const normalized = normalizeProfileNameValue(profile);
|
|
16
|
+
if (!normalized) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const payload = readSettingsFile() ?? { version: CURRENT_VERSION, profiles: {} };
|
|
20
|
+
payload.version = CURRENT_VERSION;
|
|
21
|
+
payload.profiles = payload.profiles ?? {};
|
|
22
|
+
payload.activeProfile = normalized;
|
|
23
|
+
writeSettingsFile(payload);
|
|
24
|
+
}
|
|
25
|
+
export function clearActiveProfilePreference() {
|
|
26
|
+
const payload = readSettingsFile();
|
|
27
|
+
if (!payload?.activeProfile) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
payload.version = CURRENT_VERSION;
|
|
31
|
+
payload.profiles = payload.profiles ?? {};
|
|
32
|
+
delete payload.activeProfile;
|
|
33
|
+
writeSettingsFile(payload);
|
|
34
|
+
}
|
|
35
|
+
export function loadModelPreference(profile) {
|
|
36
|
+
const payload = readSettingsFile();
|
|
37
|
+
if (!payload) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const entry = payload.profiles?.[profile];
|
|
41
|
+
if (!entry || typeof entry !== 'object') {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (typeof entry.provider !== 'string' || typeof entry.model !== 'string') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { ...entry };
|
|
48
|
+
}
|
|
49
|
+
export function saveModelPreference(profile, preference) {
|
|
50
|
+
const payload = readSettingsFile() ?? { version: CURRENT_VERSION, profiles: {} };
|
|
51
|
+
payload.version = CURRENT_VERSION;
|
|
52
|
+
payload.profiles = payload.profiles ?? {};
|
|
53
|
+
payload.profiles[profile] = { ...preference };
|
|
54
|
+
writeSettingsFile(payload);
|
|
55
|
+
}
|
|
56
|
+
export function loadToolSettings() {
|
|
57
|
+
const payload = readSettingsFile();
|
|
58
|
+
if (!payload?.tools) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const enabledTools = normalizeToolIds(payload.tools.enabledTools);
|
|
62
|
+
return { enabledTools };
|
|
63
|
+
}
|
|
64
|
+
export function saveToolSettings(settings) {
|
|
65
|
+
const payload = readSettingsFile() ?? { version: CURRENT_VERSION, profiles: {} };
|
|
66
|
+
payload.version = CURRENT_VERSION;
|
|
67
|
+
payload.profiles = payload.profiles ?? {};
|
|
68
|
+
payload.tools = {
|
|
69
|
+
enabledTools: normalizeToolIds(settings.enabledTools),
|
|
70
|
+
};
|
|
71
|
+
writeSettingsFile(payload);
|
|
72
|
+
}
|
|
73
|
+
export function clearToolSettings() {
|
|
74
|
+
const payload = readSettingsFile();
|
|
75
|
+
if (!payload) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
payload.version = CURRENT_VERSION;
|
|
79
|
+
payload.profiles = payload.profiles ?? {};
|
|
80
|
+
if (payload.tools) {
|
|
81
|
+
delete payload.tools;
|
|
82
|
+
}
|
|
83
|
+
writeSettingsFile(payload);
|
|
84
|
+
}
|
|
85
|
+
function readSettingsFile() {
|
|
86
|
+
try {
|
|
87
|
+
if (!existsSync(SETTINGS_PATH)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const raw = readFileSync(SETTINGS_PATH, 'utf8');
|
|
91
|
+
const parsed = JSON.parse(raw);
|
|
92
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const profiles = typeof parsed.profiles === 'object' && parsed.profiles !== null ? parsed.profiles : {};
|
|
96
|
+
const activeProfile = typeof parsed.activeProfile === 'string' && parsed.activeProfile.trim()
|
|
97
|
+
? parsed.activeProfile.trim()
|
|
98
|
+
: undefined;
|
|
99
|
+
return {
|
|
100
|
+
version: typeof parsed.version === 'number' ? parsed.version : CURRENT_VERSION,
|
|
101
|
+
profiles,
|
|
102
|
+
tools: parseToolSettings(parsed.tools),
|
|
103
|
+
activeProfile,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function writeSettingsFile(payload) {
|
|
111
|
+
try {
|
|
112
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
113
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(payload, null, 2));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Persistence failures should not crash the CLI; ignore write errors.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function parseToolSettings(value) {
|
|
120
|
+
if (!value || typeof value !== 'object') {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const record = value;
|
|
124
|
+
if (!Array.isArray(record.enabledTools)) {
|
|
125
|
+
return { enabledTools: [] };
|
|
126
|
+
}
|
|
127
|
+
return { enabledTools: normalizeToolIds(record.enabledTools) };
|
|
128
|
+
}
|
|
129
|
+
function normalizeToolIds(ids) {
|
|
130
|
+
if (!Array.isArray(ids)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
const result = [];
|
|
135
|
+
for (const entry of ids) {
|
|
136
|
+
if (typeof entry !== 'string') {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const id = entry.trim();
|
|
140
|
+
if (!id || seen.has(id)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
seen.add(id);
|
|
144
|
+
result.push(id);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
function normalizeProfileNameValue(value) {
|
|
149
|
+
if (typeof value !== 'string') {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const trimmed = value.trim();
|
|
153
|
+
if (!trimmed) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return trimmed;
|
|
157
|
+
}
|