protoagent 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -3
- package/dist/App.js +2 -3
- package/dist/agentic-loop.js +1 -1
- package/dist/cli.js +59 -2
- package/dist/config.js +199 -71
- package/dist/runtime-config.js +29 -14
- package/dist/tools/bash.js +23 -3
- package/dist/tools/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
```
|
|
2
|
+
█▀█ █▀█ █▀█ ▀█▀ █▀█ ▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀
|
|
3
|
+
█▀▀ █▀▄ █▄█ █ █▄█ █▀█ █▄█ ██▄ █ ▀█ █
|
|
4
|
+
```
|
|
2
5
|
|
|
3
6
|
A minimal, educational AI coding agent CLI written in TypeScript. It stays small enough to read in an afternoon, but it still has the core pieces you expect from a real coding agent: a streaming tool-use loop, approvals, sessions, MCP, skills, sub-agents, and cost tracking.
|
|
4
7
|
|
|
@@ -8,7 +11,7 @@ A minimal, educational AI coding agent CLI written in TypeScript. It stays small
|
|
|
8
11
|
- **Built-in tools** — Read, write, edit, list, search, run shell commands, manage todos, and fetch web pages with `webfetch`
|
|
9
12
|
- **Approval system** — Inline confirmation for file writes, file edits, and non-safe shell commands
|
|
10
13
|
- **Session persistence** — Conversations and TODO state are saved automatically and can be resumed with `--session`
|
|
11
|
-
- **Dynamic extensions** — Load skills on demand and add external tools through MCP servers
|
|
14
|
+
<!-- - **Dynamic extensions** — Load skills on demand and add external tools through MCP servers -->
|
|
12
15
|
- **Sub-agents** — Delegate self-contained tasks to isolated child conversations
|
|
13
16
|
- **Usage tracking** — Live token, context, and estimated cost display in the TUI
|
|
14
17
|
|
|
@@ -19,7 +22,12 @@ npm install -g protoagent
|
|
|
19
22
|
protoagent
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
On first run, ProtoAgent shows an inline setup flow where you pick a provider/model pair and enter an API key.
|
|
25
|
+
On first run, ProtoAgent shows an inline setup flow where you pick a provider/model pair and enter an API key. ProtoAgent stores that selection in `protoagent.jsonc`.
|
|
26
|
+
|
|
27
|
+
Runtime config lookup is simple:
|
|
28
|
+
|
|
29
|
+
- if `<cwd>/.protoagent/protoagent.jsonc` exists, ProtoAgent uses it
|
|
30
|
+
- otherwise it falls back to the shared user config at `~/.config/protoagent/protoagent.jsonc` on macOS/Linux and `~/AppData/Local/protoagent/protoagent.jsonc` on Windows
|
|
23
31
|
|
|
24
32
|
You can also run the standalone wizard directly:
|
|
25
33
|
|
|
@@ -27,6 +35,29 @@ You can also run the standalone wizard directly:
|
|
|
27
35
|
protoagent configure
|
|
28
36
|
```
|
|
29
37
|
|
|
38
|
+
Or configure a specific target non-interactively:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
protoagent configure --project --provider openai --model gpt-5-mini
|
|
42
|
+
protoagent configure --user --provider anthropic --model claude-sonnet-4-6
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To create a runtime config file for the current project or your shared user config, run:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
protoagent init
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`protoagent init` creates `protoagent.jsonc` in either `<cwd>/.protoagent/protoagent.jsonc` or your shared user config location and prints the exact path it used.
|
|
52
|
+
|
|
53
|
+
For scripts or non-interactive setup, use:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
protoagent init --project
|
|
57
|
+
protoagent init --user
|
|
58
|
+
protoagent init --project --force
|
|
59
|
+
```
|
|
60
|
+
|
|
30
61
|
## Interactive Commands
|
|
31
62
|
|
|
32
63
|
- `/help` — Show available slash commands
|
package/dist/App.js
CHANGED
|
@@ -232,7 +232,7 @@ const InlineSetup = ({ onComplete }) => {
|
|
|
232
232
|
model: selectedModelId,
|
|
233
233
|
...(value.trim().length > 0 ? { apiKey: value.trim() } : {}),
|
|
234
234
|
};
|
|
235
|
-
writeConfig(newConfig);
|
|
235
|
+
writeConfig(newConfig, 'project');
|
|
236
236
|
onComplete(newConfig);
|
|
237
237
|
} })] }));
|
|
238
238
|
};
|
|
@@ -365,8 +365,7 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
365
365
|
});
|
|
366
366
|
});
|
|
367
367
|
await loadRuntimeConfig();
|
|
368
|
-
|
|
369
|
-
const loadedConfig = readConfig();
|
|
368
|
+
const loadedConfig = readConfig('active');
|
|
370
369
|
if (!loadedConfig) {
|
|
371
370
|
setNeedsSetup(true);
|
|
372
371
|
return;
|
package/dist/agentic-loop.js
CHANGED
|
@@ -420,7 +420,7 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
420
420
|
result = await runSubAgent(client, model, args.task, args.max_iterations, requestDefaults, subProgress);
|
|
421
421
|
}
|
|
422
422
|
else {
|
|
423
|
-
result = await handleToolCall(name, args, { sessionId });
|
|
423
|
+
result = await handleToolCall(name, args, { sessionId, abortSignal });
|
|
424
424
|
}
|
|
425
425
|
logger.debug('Tool result', {
|
|
426
426
|
tool: name,
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { readFileSync } from 'node:fs';
|
|
|
13
13
|
import { render } from 'ink';
|
|
14
14
|
import { Command } from 'commander';
|
|
15
15
|
import { App } from './App.js';
|
|
16
|
-
import { ConfigureComponent } from './config.js';
|
|
16
|
+
import { ConfigureComponent, InitComponent, readConfig, writeConfig, writeInitConfig } from './config.js';
|
|
17
17
|
// Get package.json version
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
19
|
const __dirname = path.dirname(__filename);
|
|
@@ -33,7 +33,64 @@ program
|
|
|
33
33
|
program
|
|
34
34
|
.command('configure')
|
|
35
35
|
.description('Configure AI model and API key settings')
|
|
36
|
-
.
|
|
36
|
+
.option('--project', 'Write <cwd>/.protoagent/protoagent.jsonc')
|
|
37
|
+
.option('--user', 'Write the shared user protoagent.jsonc')
|
|
38
|
+
.option('--provider <id>', 'Provider id to configure')
|
|
39
|
+
.option('--model <id>', 'Model id to configure')
|
|
40
|
+
.option('--api-key <key>', 'Explicit API key to store in protoagent.jsonc')
|
|
41
|
+
.action((options) => {
|
|
42
|
+
if (options.project || options.user || options.provider || options.model || options.apiKey) {
|
|
43
|
+
if (options.project && options.user) {
|
|
44
|
+
console.error('Choose only one of --project or --user.');
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!options.provider || !options.model) {
|
|
49
|
+
console.error('Non-interactive configure requires --provider and --model.');
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const target = options.project ? 'project' : 'user';
|
|
54
|
+
const resultPath = writeConfig({
|
|
55
|
+
provider: options.provider,
|
|
56
|
+
model: options.model,
|
|
57
|
+
...(typeof options.apiKey === 'string' && options.apiKey.trim() ? { apiKey: options.apiKey.trim() } : {}),
|
|
58
|
+
}, target);
|
|
59
|
+
console.log('Configured ProtoAgent:');
|
|
60
|
+
console.log(resultPath);
|
|
61
|
+
const selected = readConfig(target);
|
|
62
|
+
if (selected) {
|
|
63
|
+
console.log(`${selected.provider} / ${selected.model}`);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
37
67
|
render(_jsx(ConfigureComponent, {}));
|
|
38
68
|
});
|
|
69
|
+
program
|
|
70
|
+
.command('init')
|
|
71
|
+
.description('Create a project-local or shared ProtoAgent runtime config')
|
|
72
|
+
.option('--project', 'Write <cwd>/.protoagent/protoagent.jsonc')
|
|
73
|
+
.option('--user', 'Write the shared user protoagent.jsonc')
|
|
74
|
+
.option('--force', 'Overwrite an existing target file')
|
|
75
|
+
.action((options) => {
|
|
76
|
+
if (options.project || options.user) {
|
|
77
|
+
if (options.project && options.user) {
|
|
78
|
+
console.error('Choose only one of --project or --user.');
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const result = writeInitConfig(options.project ? 'project' : 'user', process.cwd(), {
|
|
83
|
+
overwrite: Boolean(options.force),
|
|
84
|
+
});
|
|
85
|
+
const message = result.status === 'created'
|
|
86
|
+
? 'Created ProtoAgent config:'
|
|
87
|
+
: result.status === 'overwritten'
|
|
88
|
+
? 'Overwrote ProtoAgent config:'
|
|
89
|
+
: 'ProtoAgent config already exists:';
|
|
90
|
+
console.log(message);
|
|
91
|
+
console.log(result.path);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
render(_jsx(InitComponent, {}));
|
|
95
|
+
});
|
|
39
96
|
program.parse(process.argv);
|
package/dist/config.js
CHANGED
|
@@ -2,10 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import { useState
|
|
5
|
+
import { useState } from 'react';
|
|
6
6
|
import { Box, Text } from 'ink';
|
|
7
7
|
import { Select, TextInput, PasswordInput } from '@inkjs/ui';
|
|
8
|
-
import {
|
|
8
|
+
import { parse } from 'jsonc-parser';
|
|
9
|
+
import { getActiveRuntimeConfigPath } from './runtime-config.js';
|
|
9
10
|
import { getAllProviders, getProvider } from './providers.js';
|
|
10
11
|
const CONFIG_DIR_MODE = 0o700;
|
|
11
12
|
const CONFIG_FILE_MODE = 0o600;
|
|
@@ -56,80 +57,150 @@ export const getConfigDirectory = () => {
|
|
|
56
57
|
}
|
|
57
58
|
return path.join(homeDir, '.local', 'share', 'protoagent');
|
|
58
59
|
};
|
|
59
|
-
export const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const dir = getConfigDirectory();
|
|
64
|
-
if (!existsSync(dir)) {
|
|
65
|
-
mkdirSync(dir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
60
|
+
export const getUserRuntimeConfigDirectory = () => {
|
|
61
|
+
const homeDir = os.homedir();
|
|
62
|
+
if (process.platform === 'win32') {
|
|
63
|
+
return path.join(homeDir, 'AppData', 'Local', 'protoagent');
|
|
66
64
|
}
|
|
67
|
-
|
|
65
|
+
return path.join(homeDir, '.config', 'protoagent');
|
|
68
66
|
};
|
|
69
|
-
export const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
67
|
+
export const getUserRuntimeConfigPath = () => {
|
|
68
|
+
return path.join(getUserRuntimeConfigDirectory(), 'protoagent.jsonc');
|
|
69
|
+
};
|
|
70
|
+
export const getProjectRuntimeConfigDirectory = (cwd = process.cwd()) => {
|
|
71
|
+
return path.join(cwd, '.protoagent');
|
|
72
|
+
};
|
|
73
|
+
export const getProjectRuntimeConfigPath = (cwd = process.cwd()) => {
|
|
74
|
+
return path.join(getProjectRuntimeConfigDirectory(cwd), 'protoagent.jsonc');
|
|
75
|
+
};
|
|
76
|
+
export const getInitConfigPath = (target, cwd = process.cwd()) => {
|
|
77
|
+
return target === 'project' ? getProjectRuntimeConfigPath(cwd) : getUserRuntimeConfigPath();
|
|
78
|
+
};
|
|
79
|
+
const RUNTIME_CONFIG_TEMPLATE = `{
|
|
80
|
+
// Add project or user-wide ProtoAgent runtime config here.
|
|
81
|
+
// Example uses:
|
|
82
|
+
// - choose the active provider/model by making it the first provider
|
|
83
|
+
// and the first model under that provider
|
|
84
|
+
// - custom providers/models
|
|
85
|
+
// - MCP server definitions
|
|
86
|
+
// - request default parameters
|
|
87
|
+
"providers": {},
|
|
88
|
+
"mcp": {
|
|
89
|
+
"servers": {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
function ensureDirectory(targetDir) {
|
|
94
|
+
if (!existsSync(targetDir)) {
|
|
95
|
+
mkdirSync(targetDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
96
|
+
}
|
|
97
|
+
hardenPermissions(targetDir, CONFIG_DIR_MODE);
|
|
98
|
+
}
|
|
99
|
+
function isPlainObject(value) {
|
|
100
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
101
|
+
}
|
|
102
|
+
function readRuntimeConfigFileSync(configPath) {
|
|
103
|
+
if (!existsSync(configPath)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const content = readFileSync(configPath, 'utf8');
|
|
108
|
+
const errors = [];
|
|
109
|
+
const parsed = parse(content, errors, { allowTrailingComma: true, disallowComments: false });
|
|
110
|
+
if (errors.length > 0 || !isPlainObject(parsed)) {
|
|
98
111
|
return null;
|
|
99
112
|
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error('Error reading runtime config file:', error);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function getConfiguredProviderAndModel(runtimeConfig) {
|
|
121
|
+
for (const [providerId, providerConfig] of Object.entries(runtimeConfig.providers || {})) {
|
|
122
|
+
const modelId = Object.keys(providerConfig.models || {})[0];
|
|
123
|
+
if (!modelId)
|
|
124
|
+
continue;
|
|
125
|
+
const apiKey = typeof providerConfig.apiKey === 'string' && providerConfig.apiKey.trim().length > 0
|
|
126
|
+
? providerConfig.apiKey.trim()
|
|
127
|
+
: undefined;
|
|
128
|
+
return {
|
|
129
|
+
provider: providerId,
|
|
130
|
+
model: modelId,
|
|
131
|
+
...(apiKey ? { apiKey } : {}),
|
|
132
|
+
};
|
|
100
133
|
}
|
|
101
134
|
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
}
|
|
136
|
+
function writeRuntimeConfigFile(configPath, runtimeConfig) {
|
|
137
|
+
ensureDirectory(path.dirname(configPath));
|
|
138
|
+
writeFileSync(configPath, `${JSON.stringify(runtimeConfig, null, 2)}\n`, { encoding: 'utf8', mode: CONFIG_FILE_MODE });
|
|
139
|
+
hardenPermissions(configPath, CONFIG_FILE_MODE);
|
|
140
|
+
}
|
|
141
|
+
function upsertSelectedConfig(runtimeConfig, config) {
|
|
142
|
+
const existingProviders = runtimeConfig.providers || {};
|
|
143
|
+
const currentProvider = existingProviders[config.provider] || {};
|
|
144
|
+
const currentModels = currentProvider.models || {};
|
|
145
|
+
const selectedModelConfig = currentModels[config.model] || {};
|
|
146
|
+
const nextProvider = {
|
|
147
|
+
...currentProvider,
|
|
109
148
|
...(config.apiKey?.trim() ? { apiKey: config.apiKey.trim() } : {}),
|
|
149
|
+
models: Object.fromEntries([
|
|
150
|
+
[config.model, selectedModelConfig],
|
|
151
|
+
...Object.entries(currentModels).filter(([modelId]) => modelId !== config.model),
|
|
152
|
+
]),
|
|
153
|
+
};
|
|
154
|
+
if (!config.apiKey?.trim()) {
|
|
155
|
+
delete nextProvider.apiKey;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
...runtimeConfig,
|
|
159
|
+
providers: Object.fromEntries([
|
|
160
|
+
[config.provider, nextProvider],
|
|
161
|
+
...Object.entries(existingProviders).filter(([providerId]) => providerId !== config.provider),
|
|
162
|
+
]),
|
|
110
163
|
};
|
|
111
|
-
|
|
164
|
+
}
|
|
165
|
+
export function writeInitConfig(target, cwd = process.cwd(), options = {}) {
|
|
166
|
+
const configPath = getInitConfigPath(target, cwd);
|
|
167
|
+
const alreadyExists = existsSync(configPath);
|
|
168
|
+
if (alreadyExists) {
|
|
169
|
+
if (!options.overwrite) {
|
|
170
|
+
return { path: configPath, status: 'exists' };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
ensureDirectory(path.dirname(configPath));
|
|
175
|
+
}
|
|
176
|
+
writeFileSync(configPath, RUNTIME_CONFIG_TEMPLATE, { encoding: 'utf8', mode: CONFIG_FILE_MODE });
|
|
112
177
|
hardenPermissions(configPath, CONFIG_FILE_MODE);
|
|
178
|
+
return { path: configPath, status: alreadyExists ? 'overwritten' : 'created' };
|
|
179
|
+
}
|
|
180
|
+
export const readConfig = (target = 'active', cwd = process.cwd()) => {
|
|
181
|
+
const configPath = target === 'active' ? getActiveRuntimeConfigPath() : getInitConfigPath(target, cwd);
|
|
182
|
+
if (!configPath) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const runtimeConfig = readRuntimeConfigFileSync(configPath);
|
|
186
|
+
if (!runtimeConfig) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return getConfiguredProviderAndModel(runtimeConfig);
|
|
113
190
|
};
|
|
114
|
-
export
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.catch((error) => {
|
|
128
|
-
console.error('Error loading runtime config:', error);
|
|
129
|
-
setStep(2);
|
|
130
|
-
});
|
|
131
|
-
}, []);
|
|
132
|
-
return _jsx(Text, { children: "Loading configuration..." });
|
|
191
|
+
export function getDefaultConfigTarget(cwd = process.cwd()) {
|
|
192
|
+
const activeConfigPath = getActiveRuntimeConfigPath();
|
|
193
|
+
if (activeConfigPath === getProjectRuntimeConfigPath(cwd)) {
|
|
194
|
+
return 'project';
|
|
195
|
+
}
|
|
196
|
+
return 'user';
|
|
197
|
+
}
|
|
198
|
+
export const writeConfig = (config, target = 'user', cwd = process.cwd()) => {
|
|
199
|
+
const configPath = getInitConfigPath(target, cwd);
|
|
200
|
+
const runtimeConfig = readRuntimeConfigFileSync(configPath) || { providers: {}, mcp: { servers: {} } };
|
|
201
|
+
const nextRuntimeConfig = upsertSelectedConfig(runtimeConfig, config);
|
|
202
|
+
writeRuntimeConfigFile(configPath, nextRuntimeConfig);
|
|
203
|
+
return configPath;
|
|
133
204
|
};
|
|
134
205
|
export const ResetPrompt = ({ existingConfig, setStep, setConfigWritten }) => {
|
|
135
206
|
const [resetInput, setResetInput] = useState('');
|
|
@@ -157,7 +228,7 @@ export const ModelSelection = ({ setSelectedProviderId, setSelectedModelId, setS
|
|
|
157
228
|
};
|
|
158
229
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select an AI Model:" }), _jsx(Select, { options: items, onChange: handleSelect })] }));
|
|
159
230
|
};
|
|
160
|
-
export const ApiKeyInput = ({ selectedProviderId, selectedModelId, setStep, setConfigWritten, }) => {
|
|
231
|
+
export const ApiKeyInput = ({ selectedProviderId, selectedModelId, target, setStep, setConfigWritten, }) => {
|
|
161
232
|
const [errorMessage, setErrorMessage] = useState('');
|
|
162
233
|
const provider = getProvider(selectedProviderId);
|
|
163
234
|
const canUseResolvedAuth = Boolean(resolveApiKey({ provider: selectedProviderId, apiKey: undefined }));
|
|
@@ -171,7 +242,7 @@ export const ApiKeyInput = ({ selectedProviderId, selectedModelId, setStep, setC
|
|
|
171
242
|
model: selectedModelId,
|
|
172
243
|
...(value.trim().length > 0 ? { apiKey: value.trim() } : {}),
|
|
173
244
|
};
|
|
174
|
-
writeConfig(newConfig);
|
|
245
|
+
writeConfig(newConfig, target);
|
|
175
246
|
setConfigWritten(true);
|
|
176
247
|
setStep(4);
|
|
177
248
|
};
|
|
@@ -182,22 +253,79 @@ export const ConfigResult = ({ configWritten }) => {
|
|
|
182
253
|
};
|
|
183
254
|
export const ConfigureComponent = () => {
|
|
184
255
|
const [step, setStep] = useState(0);
|
|
256
|
+
const [target, setTarget] = useState(getDefaultConfigTarget());
|
|
185
257
|
const [existingConfig, setExistingConfig] = useState(null);
|
|
186
258
|
const [selectedProviderId, setSelectedProviderId] = useState('');
|
|
187
259
|
const [selectedModelId, setSelectedModelId] = useState('');
|
|
188
260
|
const [configWritten, setConfigWritten] = useState(false);
|
|
261
|
+
if (step === 0) {
|
|
262
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Choose where to configure ProtoAgent:" }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: [
|
|
263
|
+
{ label: `Project config — ${getProjectRuntimeConfigPath()}`, value: 'project' },
|
|
264
|
+
{ label: `Shared user config — ${getUserRuntimeConfigPath()}`, value: 'user' },
|
|
265
|
+
], onChange: (value) => {
|
|
266
|
+
setTarget(value);
|
|
267
|
+
const existing = readConfig(value);
|
|
268
|
+
setExistingConfig(existing);
|
|
269
|
+
setStep(existing ? 1 : 2);
|
|
270
|
+
} }) })] }));
|
|
271
|
+
}
|
|
189
272
|
switch (step) {
|
|
190
|
-
case 0:
|
|
191
|
-
return _jsx(InitialLoading, { setExistingConfig: setExistingConfig, setStep: setStep });
|
|
192
273
|
case 1:
|
|
193
274
|
return _jsx(ResetPrompt, { existingConfig: existingConfig, setStep: setStep, setConfigWritten: setConfigWritten });
|
|
194
275
|
case 2:
|
|
195
276
|
return (_jsx(ModelSelection, { setSelectedProviderId: setSelectedProviderId, setSelectedModelId: setSelectedModelId, setStep: setStep }));
|
|
196
277
|
case 3:
|
|
197
|
-
return (_jsx(ApiKeyInput, { selectedProviderId: selectedProviderId, selectedModelId: selectedModelId, setStep: setStep, setConfigWritten: setConfigWritten }));
|
|
278
|
+
return (_jsx(ApiKeyInput, { selectedProviderId: selectedProviderId, selectedModelId: selectedModelId, target: target, setStep: setStep, setConfigWritten: setConfigWritten }));
|
|
198
279
|
case 4:
|
|
199
280
|
return _jsx(ConfigResult, { configWritten: configWritten });
|
|
200
281
|
default:
|
|
201
282
|
return _jsx(Text, { children: "Unknown step." });
|
|
202
283
|
}
|
|
203
284
|
};
|
|
285
|
+
export const InitComponent = () => {
|
|
286
|
+
const [selectedTarget, setSelectedTarget] = useState(null);
|
|
287
|
+
const [result, setResult] = useState(null);
|
|
288
|
+
const options = [
|
|
289
|
+
{
|
|
290
|
+
label: 'Project config',
|
|
291
|
+
value: 'project',
|
|
292
|
+
description: getProjectRuntimeConfigPath(),
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
label: 'Shared user config',
|
|
296
|
+
value: 'user',
|
|
297
|
+
description: getUserRuntimeConfigPath(),
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
const activeTarget = selectedTarget ?? 'project';
|
|
301
|
+
const activeOption = options.find((option) => option.value === activeTarget) ?? options[0];
|
|
302
|
+
if (selectedTarget && !result) {
|
|
303
|
+
const selectedPath = getInitConfigPath(selectedTarget);
|
|
304
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Config already exists:" }), _jsx(Text, { children: selectedPath }), _jsx(Text, { children: "Overwrite it? (y/n)" }), _jsx(TextInput, { onSubmit: (answer) => {
|
|
305
|
+
if (answer.trim().toLowerCase() === 'y') {
|
|
306
|
+
setResult(writeInitConfig(selectedTarget, process.cwd(), { overwrite: true }));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
setResult({ path: selectedPath, status: 'exists' });
|
|
310
|
+
}
|
|
311
|
+
} })] }));
|
|
312
|
+
}
|
|
313
|
+
if (result) {
|
|
314
|
+
const color = result.status === 'exists' ? 'yellow' : 'green';
|
|
315
|
+
const message = result.status === 'created'
|
|
316
|
+
? 'Created ProtoAgent config:'
|
|
317
|
+
: result.status === 'overwritten'
|
|
318
|
+
? 'Overwrote ProtoAgent config:'
|
|
319
|
+
: 'ProtoAgent config already exists:';
|
|
320
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: color, children: message }), _jsx(Text, { children: result.path })] }));
|
|
321
|
+
}
|
|
322
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Create a ProtoAgent runtime config:" }), _jsx(Text, { dimColor: true, children: "Select where to write `protoagent.jsonc`." }), _jsx(Text, { dimColor: true, children: activeOption.description }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: options.map((option) => ({ label: option.label, value: option.value })), onChange: (value) => {
|
|
323
|
+
const target = value;
|
|
324
|
+
const configPath = getInitConfigPath(target);
|
|
325
|
+
if (existsSync(configPath)) {
|
|
326
|
+
setSelectedTarget(target);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
setResult(writeInitConfig(target));
|
|
330
|
+
} }) })] }));
|
|
331
|
+
};
|
package/dist/runtime-config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { parse, printParseErrorCode } from 'jsonc-parser';
|
|
@@ -16,13 +17,26 @@ const DEFAULT_RUNTIME_CONFIG = {
|
|
|
16
17
|
mcp: { servers: {} },
|
|
17
18
|
};
|
|
18
19
|
let runtimeConfigCache = null;
|
|
19
|
-
function
|
|
20
|
+
function getProjectRuntimeConfigPath() {
|
|
21
|
+
return path.join(process.cwd(), '.protoagent', 'protoagent.jsonc');
|
|
22
|
+
}
|
|
23
|
+
function getUserRuntimeConfigPath() {
|
|
20
24
|
const homeDir = os.homedir();
|
|
21
|
-
|
|
22
|
-
path.join(homeDir, '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
if (process.platform === 'win32') {
|
|
26
|
+
return path.join(homeDir, 'AppData', 'Local', 'protoagent', 'protoagent.jsonc');
|
|
27
|
+
}
|
|
28
|
+
return path.join(homeDir, '.config', 'protoagent', 'protoagent.jsonc');
|
|
29
|
+
}
|
|
30
|
+
export function getActiveRuntimeConfigPath() {
|
|
31
|
+
const projectPath = getProjectRuntimeConfigPath();
|
|
32
|
+
if (existsSync(projectPath)) {
|
|
33
|
+
return projectPath;
|
|
34
|
+
}
|
|
35
|
+
const userPath = getUserRuntimeConfigPath();
|
|
36
|
+
if (existsSync(userPath)) {
|
|
37
|
+
return userPath;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
26
40
|
}
|
|
27
41
|
function isPlainObject(value) {
|
|
28
42
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -156,16 +170,17 @@ export async function loadRuntimeConfig(forceReload = false) {
|
|
|
156
170
|
if (!forceReload && runtimeConfigCache) {
|
|
157
171
|
return runtimeConfigCache;
|
|
158
172
|
}
|
|
159
|
-
|
|
160
|
-
|
|
173
|
+
const configPath = getActiveRuntimeConfigPath();
|
|
174
|
+
let loaded = DEFAULT_RUNTIME_CONFIG;
|
|
175
|
+
if (configPath) {
|
|
161
176
|
const fileConfig = await readRuntimeConfigFile(configPath);
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
if (fileConfig) {
|
|
178
|
+
logger.debug('Loaded runtime config', { path: configPath });
|
|
179
|
+
loaded = mergeRuntimeConfig(DEFAULT_RUNTIME_CONFIG, fileConfig);
|
|
180
|
+
}
|
|
166
181
|
}
|
|
167
|
-
runtimeConfigCache =
|
|
168
|
-
return
|
|
182
|
+
runtimeConfigCache = loaded;
|
|
183
|
+
return loaded;
|
|
169
184
|
}
|
|
170
185
|
export function getRuntimeConfig() {
|
|
171
186
|
return runtimeConfigCache || DEFAULT_RUNTIME_CONFIG;
|
package/dist/tools/bash.js
CHANGED
|
@@ -114,7 +114,7 @@ async function isSafe(command) {
|
|
|
114
114
|
}
|
|
115
115
|
return validateCommandPaths(tokens);
|
|
116
116
|
}
|
|
117
|
-
export async function runBash(command, timeoutMs = 30_000, sessionId) {
|
|
117
|
+
export async function runBash(command, timeoutMs = 30_000, sessionId, abortSignal) {
|
|
118
118
|
// Layer 1: hard block
|
|
119
119
|
if (isDangerous(command)) {
|
|
120
120
|
return `Error: Command blocked for safety. "${command}" contains a dangerous pattern that cannot be executed.`;
|
|
@@ -139,6 +139,7 @@ export async function runBash(command, timeoutMs = 30_000, sessionId) {
|
|
|
139
139
|
let stdout = '';
|
|
140
140
|
let stderr = '';
|
|
141
141
|
let timedOut = false;
|
|
142
|
+
let aborted = false;
|
|
142
143
|
const child = spawn(command, [], {
|
|
143
144
|
shell: true,
|
|
144
145
|
cwd: process.cwd(),
|
|
@@ -147,13 +148,31 @@ export async function runBash(command, timeoutMs = 30_000, sessionId) {
|
|
|
147
148
|
});
|
|
148
149
|
child.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
149
150
|
child.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
150
|
-
const
|
|
151
|
-
timedOut = true;
|
|
151
|
+
const terminateChild = () => {
|
|
152
152
|
child.kill('SIGTERM');
|
|
153
153
|
setTimeout(() => child.kill('SIGKILL'), 2000);
|
|
154
|
+
};
|
|
155
|
+
const onAbort = () => {
|
|
156
|
+
aborted = true;
|
|
157
|
+
terminateChild();
|
|
158
|
+
};
|
|
159
|
+
if (abortSignal?.aborted) {
|
|
160
|
+
onAbort();
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
abortSignal?.addEventListener('abort', onAbort, { once: true });
|
|
164
|
+
}
|
|
165
|
+
const timer = setTimeout(() => {
|
|
166
|
+
timedOut = true;
|
|
167
|
+
terminateChild();
|
|
154
168
|
}, timeoutMs);
|
|
155
169
|
child.on('close', (code) => {
|
|
156
170
|
clearTimeout(timer);
|
|
171
|
+
abortSignal?.removeEventListener('abort', onAbort);
|
|
172
|
+
if (aborted) {
|
|
173
|
+
resolve(`Command aborted by user.\nPartial stdout:\n${stdout.slice(0, 5000)}\nPartial stderr:\n${stderr.slice(0, 2000)}`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
157
176
|
if (timedOut) {
|
|
158
177
|
resolve(`Command timed out after ${timeoutMs / 1000}s.\nPartial stdout:\n${stdout.slice(0, 5000)}\nPartial stderr:\n${stderr.slice(0, 2000)}`);
|
|
159
178
|
return;
|
|
@@ -172,6 +191,7 @@ export async function runBash(command, timeoutMs = 30_000, sessionId) {
|
|
|
172
191
|
});
|
|
173
192
|
child.on('error', (err) => {
|
|
174
193
|
clearTimeout(timer);
|
|
194
|
+
abortSignal?.removeEventListener('abort', onAbort);
|
|
175
195
|
resolve(`Error executing command: ${err.message}`);
|
|
176
196
|
});
|
|
177
197
|
});
|
package/dist/tools/index.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function handleToolCall(toolName, args, context = {}) {
|
|
|
71
71
|
case 'search_files':
|
|
72
72
|
return await searchFiles(args.search_term, args.directory_path, args.case_sensitive, args.file_extensions);
|
|
73
73
|
case 'bash':
|
|
74
|
-
return await runBash(args.command, args.timeout_ms, context.sessionId);
|
|
74
|
+
return await runBash(args.command, args.timeout_ms, context.sessionId, context.abortSignal);
|
|
75
75
|
case 'todo_read':
|
|
76
76
|
return readTodos(context.sessionId);
|
|
77
77
|
case 'todo_write':
|