harnessly 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/build/agent.d.ts +26 -0
- package/build/agent.js +163 -0
- package/build/config.d.ts +8 -0
- package/build/config.js +58 -0
- package/build/harness.d.ts +31 -0
- package/build/harness.js +94 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +172 -0
- package/build/tools.d.ts +1049 -0
- package/build/tools.js +142 -0
- package/build/ui.d.ts +12 -0
- package/build/ui.js +156 -0
- package/package.json +63 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cac } from 'cac';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import { Agent, createModel } from './agent';
|
|
7
|
+
import { appendHistory, clearHistory, deleteConfig, getConfig, getHistory, saveConfig } from './config';
|
|
8
|
+
import { getSessionTokens, resetSessionTokens, getLogPath } from './harness';
|
|
9
|
+
import { SelectProvider, Welcome } from './ui';
|
|
10
|
+
function createPrompt() {
|
|
11
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
12
|
+
return {
|
|
13
|
+
ask(query) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
rl.question(query, (answer) => resolve(answer));
|
|
16
|
+
rl.once('close', () => resolve(null));
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
close() { rl.close(); }
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const cli = cac('harnessly');
|
|
23
|
+
function tryEnvConfig() {
|
|
24
|
+
if (process.env.OPENAI_API_KEY) {
|
|
25
|
+
return { provider: 'OpenAI', apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o' };
|
|
26
|
+
}
|
|
27
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
28
|
+
return { provider: 'Anthropic', apiKey: process.env.ANTHROPIC_API_KEY, model: 'claude-sonnet-4-5' };
|
|
29
|
+
}
|
|
30
|
+
if (process.env.AZURE_API_KEY && process.env.AZURE_RESOURCE_NAME) {
|
|
31
|
+
return { provider: 'Azure', apiKey: process.env.AZURE_API_KEY, model: 'gpt-4o', resourceName: process.env.AZURE_RESOURCE_NAME };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
cli.command('', 'Start the coding agent').action(async () => {
|
|
36
|
+
Welcome();
|
|
37
|
+
let config = getConfig();
|
|
38
|
+
if (!config) {
|
|
39
|
+
config = tryEnvConfig() ?? await SelectProvider();
|
|
40
|
+
saveConfig(config);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
console.log(chalk.dim(` Provider: ${config.provider} | Model: ${config.model || 'default'}`));
|
|
44
|
+
console.log(chalk.dim(` Working dir: ${process.cwd()}\n`));
|
|
45
|
+
}
|
|
46
|
+
let model = createModel(config);
|
|
47
|
+
const prompt = createPrompt();
|
|
48
|
+
let spinner = null;
|
|
49
|
+
const DESTRUCTIVE = new Set(['writeFile', 'runCommand']);
|
|
50
|
+
const approval = async (toolName, args) => {
|
|
51
|
+
if (!DESTRUCTIVE.has(toolName))
|
|
52
|
+
return true;
|
|
53
|
+
if (spinner)
|
|
54
|
+
spinner.stop();
|
|
55
|
+
const detail = formatArgs(args);
|
|
56
|
+
console.log(chalk.yellow(`\n ⚠ ${toolName}(${detail})`));
|
|
57
|
+
const answer = await prompt.ask(chalk.yellow(' Allow? [y/N] '));
|
|
58
|
+
const approved = answer?.trim().toLowerCase() === 'y';
|
|
59
|
+
if (spinner && approved)
|
|
60
|
+
spinner.start('Thinking...');
|
|
61
|
+
return approved;
|
|
62
|
+
};
|
|
63
|
+
let agent = new Agent(model, { approval });
|
|
64
|
+
while (true) {
|
|
65
|
+
const input = await prompt.ask(chalk.cyan('>>> '));
|
|
66
|
+
if (input === null || input === 'exit' || input === '/exit') {
|
|
67
|
+
prompt.close();
|
|
68
|
+
console.log(chalk.dim('\nGoodbye!\n'));
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (!input.trim())
|
|
72
|
+
continue;
|
|
73
|
+
const cmd = input.trim();
|
|
74
|
+
if (cmd === '/config') {
|
|
75
|
+
deleteConfig();
|
|
76
|
+
config = await SelectProvider();
|
|
77
|
+
saveConfig(config);
|
|
78
|
+
model = createModel(config);
|
|
79
|
+
agent = new Agent(model, { approval });
|
|
80
|
+
console.log(chalk.green(' Config updated!\n'));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (cmd === '/clear') {
|
|
84
|
+
agent = new Agent(model, { approval });
|
|
85
|
+
clearHistory();
|
|
86
|
+
resetSessionTokens();
|
|
87
|
+
console.log(chalk.dim(' Conversation cleared.\n'));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (cmd === '/history') {
|
|
91
|
+
const history = getHistory();
|
|
92
|
+
if (history.length === 0) {
|
|
93
|
+
console.log(chalk.dim(' No history yet.\n'));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(chalk.dim(' Recent prompts:'));
|
|
97
|
+
history.slice(-10).forEach((h, i) => {
|
|
98
|
+
console.log(chalk.dim(` ${i + 1}. ${h}`));
|
|
99
|
+
});
|
|
100
|
+
console.log('');
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (cmd === '/tokens') {
|
|
105
|
+
const usage = getSessionTokens();
|
|
106
|
+
console.log(chalk.dim(` Tokens: ${usage.totalTokens.toLocaleString()} (in: ${usage.inputTokens.toLocaleString()}, out: ${usage.outputTokens.toLocaleString()})\n`));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (cmd === '/logs') {
|
|
110
|
+
console.log(chalk.dim(` Log file: ${getLogPath()}\n`));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (cmd === '/help') {
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(chalk.cyan(' Available commands:'));
|
|
116
|
+
console.log(chalk.dim(' /help - Show this help'));
|
|
117
|
+
console.log(chalk.dim(' /config - Change provider/model'));
|
|
118
|
+
console.log(chalk.dim(' /clear - Reset conversation'));
|
|
119
|
+
console.log(chalk.dim(' /history - Show recent prompts'));
|
|
120
|
+
console.log(chalk.dim(' /tokens - Show token usage'));
|
|
121
|
+
console.log(chalk.dim(' /logs - Show log file path'));
|
|
122
|
+
console.log(chalk.dim(' /exit - Quit Devra'));
|
|
123
|
+
console.log('');
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
appendHistory(cmd);
|
|
127
|
+
spinner = ora({ text: 'Thinking...', color: 'cyan' }).start();
|
|
128
|
+
const onToolCall = (toolName, args) => {
|
|
129
|
+
spinner.stop();
|
|
130
|
+
console.log(chalk.dim(` ${toolName}(${formatArgs(args)})`));
|
|
131
|
+
spinner.start('Thinking...');
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
const { textStream, done } = await agent.ask({ userMessage: input, onToolCall });
|
|
135
|
+
let firstChunk = true;
|
|
136
|
+
for await (const chunk of textStream) {
|
|
137
|
+
if (firstChunk) {
|
|
138
|
+
spinner.stop();
|
|
139
|
+
process.stdout.write(`\n${chalk.green('Devra:')} `);
|
|
140
|
+
firstChunk = false;
|
|
141
|
+
}
|
|
142
|
+
process.stdout.write(chunk);
|
|
143
|
+
}
|
|
144
|
+
if (firstChunk)
|
|
145
|
+
spinner.stop();
|
|
146
|
+
process.stdout.write('\n');
|
|
147
|
+
await done();
|
|
148
|
+
const usage = getSessionTokens();
|
|
149
|
+
console.log(chalk.dim(` tokens: ${usage.totalTokens.toLocaleString()} | /help for commands`));
|
|
150
|
+
process.stdout.write('\n');
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
spinner?.stop();
|
|
154
|
+
spinner = null;
|
|
155
|
+
console.log(chalk.red(`\nError: ${err.message}\n`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
function formatArgs(args) {
|
|
160
|
+
if (args.filePath)
|
|
161
|
+
return args.filePath;
|
|
162
|
+
if (args.command)
|
|
163
|
+
return args.command;
|
|
164
|
+
if (args.dirPath)
|
|
165
|
+
return args.dirPath;
|
|
166
|
+
if (args.pattern)
|
|
167
|
+
return args.pattern;
|
|
168
|
+
return JSON.stringify(args).slice(0, 60);
|
|
169
|
+
}
|
|
170
|
+
cli.help();
|
|
171
|
+
cli.version('1.0.0');
|
|
172
|
+
cli.parse();
|