draco-cli 0.0.0-stage → 3.0.2
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 +76 -3
- package/bin/draco.js +6 -0
- package/package.json +32 -6
- package/src/cli/args.js +28 -0
- package/src/cli/command-registry.js +130 -0
- package/src/cli/line-queue.js +35 -0
- package/src/cli/oneshot.js +17 -0
- package/src/cli/repl.js +46 -0
- package/src/cli/slash-commands.js +271 -0
- package/src/config.js +216 -0
- package/src/core/approvals.js +20 -0
- package/src/core/compaction.js +53 -0
- package/src/core/config-manager.js +174 -0
- package/src/core/fallback.js +11 -0
- package/src/core/images.js +40 -0
- package/src/core/logger.js +207 -0
- package/src/core/middleware.js +102 -0
- package/src/core/plugin-manager.js +149 -0
- package/src/core/prompt.js +45 -0
- package/src/core/session-history.js +161 -0
- package/src/core/session.js +307 -0
- package/src/core/sessions-store.js +56 -0
- package/src/index.js +255 -0
- package/src/integrations/mcp-manager.js +248 -0
- package/src/providers/openai-compatible.js +288 -0
- package/src/tools/fs-tools.js +79 -0
- package/src/tools/index.js +152 -0
- package/src/tools/sandbox.js +21 -0
- package/src/tools/shell-tool.js +43 -0
- package/src/tools/web-tools.js +322 -0
- package/src/ui/diff.js +53 -0
- package/src/ui/markdown.js +106 -0
- package/src/ui/terminal.js +114 -0
- package/src/ui/theme.js +167 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const theme = require('../ui/theme.js');
|
|
7
|
+
|
|
8
|
+
class Logger {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.level = options.level || 'info';
|
|
11
|
+
this.logDir = options.logDir || path.join(os.homedir(), '.draco', 'logs');
|
|
12
|
+
this.maxFileSize = options.maxFileSize || 5 * 1024 * 1024;
|
|
13
|
+
this.maxFiles = options.maxFiles || 5;
|
|
14
|
+
this.console = options.console !== false;
|
|
15
|
+
this.file = options.file !== false;
|
|
16
|
+
|
|
17
|
+
this.levels = {
|
|
18
|
+
error: 0,
|
|
19
|
+
warn: 1,
|
|
20
|
+
info: 2,
|
|
21
|
+
debug: 3,
|
|
22
|
+
trace: 4,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
this.colors = {
|
|
26
|
+
error: '\x1b[31m',
|
|
27
|
+
warn: '\x1b[33m',
|
|
28
|
+
info: '\x1b[36m',
|
|
29
|
+
debug: '\x1b[35m',
|
|
30
|
+
trace: '\x1b[90m',
|
|
31
|
+
reset: '\x1b[0m',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (this.file) {
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(this.logDir)) {
|
|
37
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
theme.printError(`Failed to create log directory: ${err.message}`);
|
|
41
|
+
this.file = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
shouldLog(level) {
|
|
47
|
+
return this.levels[level] <= this.levels[this.level];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
formatMessage(level, message, data) {
|
|
51
|
+
const timestamp = new Date().toISOString();
|
|
52
|
+
let formatted = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
|
|
53
|
+
|
|
54
|
+
if (data !== undefined) {
|
|
55
|
+
if (data instanceof Error) {
|
|
56
|
+
formatted += `\n${data.stack || data.message}`;
|
|
57
|
+
} else if (typeof data === 'object') {
|
|
58
|
+
formatted += `\n${JSON.stringify(data, null, 2)}`;
|
|
59
|
+
} else {
|
|
60
|
+
formatted += ` ${data}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return formatted;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
writeToFile(level, message) {
|
|
68
|
+
if (!this.file) return;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const logFile = path.join(this.logDir, `${level}.log`);
|
|
72
|
+
const formatted = this.formatMessage(level, message);
|
|
73
|
+
|
|
74
|
+
fs.appendFileSync(logFile, formatted + '\n');
|
|
75
|
+
|
|
76
|
+
const stats = fs.statSync(logFile);
|
|
77
|
+
if (stats.size > this.maxFileSize) {
|
|
78
|
+
this.rotateFile(logFile);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
theme.printError(`Failed to write to log file: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
rotateFile(filePath) {
|
|
86
|
+
try {
|
|
87
|
+
for (let i = this.maxFiles - 1; i > 0; i--) {
|
|
88
|
+
const oldFile = `${filePath}.${i}`;
|
|
89
|
+
const newFile = `${filePath}.${i + 1}`;
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(oldFile)) {
|
|
92
|
+
if (i === this.maxFiles - 1) {
|
|
93
|
+
fs.unlinkSync(oldFile);
|
|
94
|
+
} else {
|
|
95
|
+
fs.renameSync(oldFile, newFile);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (fs.existsSync(filePath)) {
|
|
101
|
+
fs.renameSync(filePath, `${filePath}.1`);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
theme.printError(`Failed to rotate log file: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
log(level, message, data) {
|
|
109
|
+
if (!this.shouldLog(level)) return;
|
|
110
|
+
|
|
111
|
+
const formatted = this.formatMessage(level, message, data);
|
|
112
|
+
|
|
113
|
+
if (this.console) {
|
|
114
|
+
if (level === 'error') {
|
|
115
|
+
console.error(theme.errorTag(formatted));
|
|
116
|
+
} else {
|
|
117
|
+
const color = this.colors[level] || '';
|
|
118
|
+
console.log(`${color}${formatted}${this.colors.reset}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.writeToFile(level, formatted);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
error(message, data) {
|
|
126
|
+
this.log('error', message, data);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
warn(message, data) {
|
|
130
|
+
this.log('warn', message, data);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
info(message, data) {
|
|
134
|
+
this.log('info', message, data);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
debug(message, data) {
|
|
138
|
+
this.log('debug', message, data);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
trace(message, data) {
|
|
142
|
+
this.log('trace', message, data);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setLevel(level) {
|
|
146
|
+
if (this.levels[level] === undefined) {
|
|
147
|
+
throw new Error(`Invalid log level: ${level}. Must be one of: ${Object.keys(this.levels).join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
this.level = level;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getLevel() {
|
|
153
|
+
return this.level;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getRecentLogs(level = 'info', lines = 50) {
|
|
157
|
+
if (!this.file) return [];
|
|
158
|
+
try {
|
|
159
|
+
const logFile = path.join(this.logDir, `${level}.log`);
|
|
160
|
+
if (!fs.existsSync(logFile)) return [];
|
|
161
|
+
const content = fs.readFileSync(logFile, 'utf8');
|
|
162
|
+
const allLines = content.split('\n').filter(Boolean);
|
|
163
|
+
return allLines.slice(-lines);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
child(options = {}) {
|
|
170
|
+
return new Logger({
|
|
171
|
+
...this,
|
|
172
|
+
...options,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
clearLogs() {
|
|
177
|
+
if (!this.file) return false;
|
|
178
|
+
try {
|
|
179
|
+
const files = fs.readdirSync(this.logDir);
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
if (file.endsWith('.log')) {
|
|
182
|
+
fs.unlinkSync(path.join(this.logDir, file));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
theme.printError(`Failed to clear logs: ${error.message}`);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getLogSize(level = 'info') {
|
|
193
|
+
if (!this.file) return 0;
|
|
194
|
+
try {
|
|
195
|
+
const logFile = path.join(this.logDir, `${level}.log`);
|
|
196
|
+
if (!fs.existsSync(logFile)) return 0;
|
|
197
|
+
const stats = fs.statSync(logFile);
|
|
198
|
+
return stats.size;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const logger = new Logger();
|
|
206
|
+
|
|
207
|
+
module.exports = { Logger, logger };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const theme = require('../ui/theme.js');
|
|
4
|
+
|
|
5
|
+
class MiddlewarePipeline {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.middlewares = [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
use(middleware) {
|
|
11
|
+
if (typeof middleware !== 'function') {
|
|
12
|
+
throw new Error('Middleware must be a function');
|
|
13
|
+
}
|
|
14
|
+
this.middlewares.push(middleware);
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
remove(middleware) {
|
|
19
|
+
const index = this.middlewares.indexOf(middleware);
|
|
20
|
+
if (index !== -1) {
|
|
21
|
+
this.middlewares.splice(index, 1);
|
|
22
|
+
}
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async execute(context) {
|
|
27
|
+
let index = 0;
|
|
28
|
+
const next = async () => {
|
|
29
|
+
if (index < this.middlewares.length) {
|
|
30
|
+
const middleware = this.middlewares[index++];
|
|
31
|
+
await middleware(context, next);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
await next();
|
|
36
|
+
return context;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
clear() {
|
|
40
|
+
this.middlewares = [];
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
list() {
|
|
45
|
+
return [...this.middlewares];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const loggingMiddleware = async (context, next) => {
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
console.log(`[${new Date().toISOString()}] Request started`);
|
|
52
|
+
|
|
53
|
+
await next();
|
|
54
|
+
|
|
55
|
+
const duration = Date.now() - start;
|
|
56
|
+
console.log(`[${new Date().toISOString()}] Request completed in ${duration}ms`);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const errorMiddleware = async (context, next) => {
|
|
60
|
+
try {
|
|
61
|
+
await next();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
theme.printError(error.message);
|
|
64
|
+
context.error = error;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const validationMiddleware = async (context, next) => {
|
|
69
|
+
if (!context.input || context.input.trim() === '') {
|
|
70
|
+
throw new Error('Input cannot be empty');
|
|
71
|
+
}
|
|
72
|
+
await next();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const rateLimitMiddleware = (maxRequests = 100, windowMs = 60000) => {
|
|
76
|
+
const requests = new Map();
|
|
77
|
+
|
|
78
|
+
return async (context, next) => {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const windowStart = now - windowMs;
|
|
81
|
+
const clientRequests = (requests.get(context.clientId) || []).filter(
|
|
82
|
+
(t) => t > windowStart
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (clientRequests.length >= maxRequests) {
|
|
86
|
+
throw new Error('Rate limit exceeded');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
clientRequests.push(now);
|
|
90
|
+
requests.set(context.clientId, clientRequests);
|
|
91
|
+
|
|
92
|
+
await next();
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
MiddlewarePipeline,
|
|
98
|
+
loggingMiddleware,
|
|
99
|
+
errorMiddleware,
|
|
100
|
+
validationMiddleware,
|
|
101
|
+
rateLimitMiddleware,
|
|
102
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const theme = require('../ui/theme.js');
|
|
7
|
+
|
|
8
|
+
class PluginManager {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.plugins = new Map();
|
|
12
|
+
this.hooks = new Map();
|
|
13
|
+
this.pluginDir = path.join(os.homedir(), '.draco', 'plugins');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async loadPlugins() {
|
|
17
|
+
if (!fs.existsSync(this.pluginDir)) {
|
|
18
|
+
fs.mkdirSync(this.pluginDir, { recursive: true });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pluginFiles = fs.readdirSync(this.pluginDir).filter((f) => f.endsWith('.js'));
|
|
23
|
+
|
|
24
|
+
for (const file of pluginFiles) {
|
|
25
|
+
try {
|
|
26
|
+
await this.loadPlugin(path.join(this.pluginDir, file));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
theme.printError(`Failed to load plugin ${file}: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async loadPlugin(pluginPath) {
|
|
34
|
+
const pluginModule = require(pluginPath);
|
|
35
|
+
const plugin = typeof pluginModule === 'function' ? pluginModule() : pluginModule;
|
|
36
|
+
|
|
37
|
+
if (!plugin || !plugin.name) {
|
|
38
|
+
throw new Error('Plugin must export a name');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pluginInfo = {
|
|
42
|
+
name: plugin.name,
|
|
43
|
+
version: plugin.version || '1.0.0',
|
|
44
|
+
description: plugin.description || '',
|
|
45
|
+
author: plugin.author || '',
|
|
46
|
+
hooks: plugin.hooks || {},
|
|
47
|
+
commands: plugin.commands || [],
|
|
48
|
+
initialized: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (plugin.init) {
|
|
52
|
+
await plugin.init(this.config);
|
|
53
|
+
pluginInfo.initialized = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.plugins.set(plugin.name, pluginInfo);
|
|
57
|
+
|
|
58
|
+
for (const [hookName, hookFn] of Object.entries(pluginInfo.hooks)) {
|
|
59
|
+
if (!this.hooks.has(hookName)) {
|
|
60
|
+
this.hooks.set(hookName, []);
|
|
61
|
+
}
|
|
62
|
+
this.hooks.get(hookName).push({
|
|
63
|
+
plugin: plugin.name,
|
|
64
|
+
handler: hookFn,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return pluginInfo;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async unloadPlugin(name) {
|
|
72
|
+
const plugin = this.plugins.get(name);
|
|
73
|
+
if (!plugin) return false;
|
|
74
|
+
|
|
75
|
+
for (const [hookName, hookHandlers] of this.hooks) {
|
|
76
|
+
const filtered = hookHandlers.filter((h) => h.plugin !== name);
|
|
77
|
+
if (filtered.length === 0) {
|
|
78
|
+
this.hooks.delete(hookName);
|
|
79
|
+
} else {
|
|
80
|
+
this.hooks.set(hookName, filtered);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.plugins.delete(name);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async executeHook(hookName, data) {
|
|
89
|
+
const handlers = this.hooks.get(hookName) || [];
|
|
90
|
+
let result = data;
|
|
91
|
+
|
|
92
|
+
for (const { plugin, handler } of handlers) {
|
|
93
|
+
try {
|
|
94
|
+
result = await handler(result);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
theme.printError(`Hook ${hookName} failed in plugin ${plugin}: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getPlugin(name) {
|
|
104
|
+
return this.plugins.get(name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
listPlugins() {
|
|
108
|
+
return Array.from(this.plugins.values());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
hasPlugin(name) {
|
|
112
|
+
return this.plugins.has(name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async installPlugin(source) {
|
|
116
|
+
const pluginName = path.basename(source, '.js');
|
|
117
|
+
const destPath = path.join(this.pluginDir, `${pluginName}.js`);
|
|
118
|
+
|
|
119
|
+
if (fs.existsSync(destPath)) {
|
|
120
|
+
throw new Error(`Plugin ${pluginName} already exists`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (source.startsWith('http')) {
|
|
124
|
+
const response = await fetch(source);
|
|
125
|
+
const code = await response.text();
|
|
126
|
+
fs.writeFileSync(destPath, code);
|
|
127
|
+
} else {
|
|
128
|
+
fs.copyFileSync(source, destPath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return this.loadPlugin(destPath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async uninstallPlugin(name) {
|
|
135
|
+
const plugin = this.plugins.get(name);
|
|
136
|
+
if (!plugin) return false;
|
|
137
|
+
|
|
138
|
+
await this.unloadPlugin(name);
|
|
139
|
+
|
|
140
|
+
const pluginPath = path.join(this.pluginDir, `${name}.js`);
|
|
141
|
+
if (fs.existsSync(pluginPath)) {
|
|
142
|
+
fs.unlinkSync(pluginPath);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { PluginManager };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function loadAgentsMd(workspaceRoot) {
|
|
7
|
+
const p = path.join(workspaceRoot, 'AGENTS.md');
|
|
8
|
+
try {
|
|
9
|
+
const stat = fs.statSync(p);
|
|
10
|
+
if (!stat.isFile()) return '';
|
|
11
|
+
return fs.readFileSync(p, 'utf8').slice(0, 32000);
|
|
12
|
+
} catch {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildSystemPrompt(cfg, agentsMd) {
|
|
18
|
+
const toolsLine =
|
|
19
|
+
'Your tools: readFile, writeFile, makeDirectory, executeShell, webFetch, webSearch.';
|
|
20
|
+
const webLine =
|
|
21
|
+
'webSearch is for open-ended discovery ("find out about X"); webFetch is for retrieving a specific URL you already have. Prefer local project context and only use the web when the task clearly needs current or external information.';
|
|
22
|
+
const lines = [
|
|
23
|
+
`You are Draco, an autonomous terminal-based coding agent working inside the user's workspace at ${cfg.workspaceRoot}.`,
|
|
24
|
+
'You are confined to that directory: every file operation is sandboxed to it and paths outside are rejected.',
|
|
25
|
+
toolsLine,
|
|
26
|
+
'- Use the tools proactively to actually perform work instead of describing hypothetical edits.',
|
|
27
|
+
'- Read a file with readFile before editing it unless its contents are already in context.',
|
|
28
|
+
'- Shell commands and file writes may require user approval depending on the current approval mode; batch related commands sensibly.',
|
|
29
|
+
'- If a tool call fails, read the error message and adapt; never repeat the identical failing call.',
|
|
30
|
+
webLine,
|
|
31
|
+
'Be concise, technical, and act on the request directly. Format answers in Markdown.',
|
|
32
|
+
];
|
|
33
|
+
if (agentsMd && agentsMd.trim()) {
|
|
34
|
+
lines.push(
|
|
35
|
+
'',
|
|
36
|
+
'Project instructions from AGENTS.md (follow these carefully):',
|
|
37
|
+
'---',
|
|
38
|
+
agentsMd.trim(),
|
|
39
|
+
'---'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return lines.join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { buildSystemPrompt, loadAgentsMd };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const theme = require('../ui/theme.js');
|
|
7
|
+
|
|
8
|
+
class SessionHistory {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.historyFile = path.join(
|
|
12
|
+
os.homedir(),
|
|
13
|
+
'.draco',
|
|
14
|
+
'history',
|
|
15
|
+
`${config.workspaceHash || 'default'}.json`
|
|
16
|
+
);
|
|
17
|
+
this.sessions = [];
|
|
18
|
+
this.currentIndex = -1;
|
|
19
|
+
this.maxSessions = 100;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
load() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(this.historyFile)) {
|
|
25
|
+
const data = fs.readFileSync(this.historyFile, 'utf8');
|
|
26
|
+
this.sessions = JSON.parse(data);
|
|
27
|
+
this.currentIndex = this.sessions.length - 1;
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
theme.printError(`Failed to load history: ${error.message}`);
|
|
31
|
+
this.sessions = [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
save() {
|
|
36
|
+
try {
|
|
37
|
+
const dir = path.dirname(this.historyFile);
|
|
38
|
+
if (!fs.existsSync(dir)) {
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sessionsToSave = this.sessions.slice(-this.maxSessions);
|
|
43
|
+
fs.writeFileSync(this.historyFile, JSON.stringify(sessionsToSave, null, 2));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
theme.printError(`Failed to save history: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
addSession(session) {
|
|
50
|
+
const historyEntry = {
|
|
51
|
+
id: Date.now().toString(36),
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
model: session.model,
|
|
54
|
+
prompt: session.prompt?.substring(0, 200) || '',
|
|
55
|
+
tokenCount: session.tokenCount || 0,
|
|
56
|
+
messageCount: session.messages?.length || 0,
|
|
57
|
+
duration: session.duration || 0,
|
|
58
|
+
workspace: this.config.workspaceRoot,
|
|
59
|
+
tags: session.tags || [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
this.sessions.push(historyEntry);
|
|
63
|
+
this.currentIndex = this.sessions.length - 1;
|
|
64
|
+
this.save();
|
|
65
|
+
|
|
66
|
+
return historyEntry;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
updateSession(id, updates) {
|
|
70
|
+
const index = this.sessions.findIndex((s) => s.id === id);
|
|
71
|
+
if (index === -1) return null;
|
|
72
|
+
|
|
73
|
+
this.sessions[index] = { ...this.sessions[index], ...updates };
|
|
74
|
+
this.save();
|
|
75
|
+
|
|
76
|
+
return this.sessions[index];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getSession(id) {
|
|
80
|
+
return this.sessions.find((s) => s.id === id) || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getRecentSessions(count = 10) {
|
|
84
|
+
return this.sessions.slice(-count).reverse();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
searchSessions(query) {
|
|
88
|
+
const lowerQuery = query.toLowerCase();
|
|
89
|
+
return this.sessions
|
|
90
|
+
.filter(
|
|
91
|
+
(s) =>
|
|
92
|
+
s.prompt.toLowerCase().includes(lowerQuery) ||
|
|
93
|
+
s.model.toLowerCase().includes(lowerQuery) ||
|
|
94
|
+
s.tags.some((t) => t.toLowerCase().includes(lowerQuery))
|
|
95
|
+
)
|
|
96
|
+
.reverse();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
clearHistory() {
|
|
100
|
+
this.sessions = [];
|
|
101
|
+
this.currentIndex = -1;
|
|
102
|
+
this.save();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getStats() {
|
|
106
|
+
const totalSessions = this.sessions.length;
|
|
107
|
+
const totalTokens = this.sessions.reduce((sum, s) => sum + (s.tokenCount || 0), 0);
|
|
108
|
+
const totalMessages = this.sessions.reduce((sum, s) => sum + (s.messageCount || 0), 0);
|
|
109
|
+
|
|
110
|
+
const modelCounts = {};
|
|
111
|
+
for (const session of this.sessions) {
|
|
112
|
+
modelCounts[session.model] = (modelCounts[session.model] || 0) + 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const tagCounts = {};
|
|
116
|
+
for (const session of this.sessions) {
|
|
117
|
+
for (const tag of session.tags) {
|
|
118
|
+
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
totalSessions,
|
|
124
|
+
totalTokens,
|
|
125
|
+
totalMessages,
|
|
126
|
+
modelCounts,
|
|
127
|
+
tagCounts,
|
|
128
|
+
averageTokensPerSession: totalSessions > 0 ? Math.round(totalTokens / totalSessions) : 0,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
exportHistory(format = 'json') {
|
|
133
|
+
if (format === 'json') {
|
|
134
|
+
return JSON.stringify(this.sessions, null, 2);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (format === 'markdown') {
|
|
138
|
+
let md = '# Session History\n\n';
|
|
139
|
+
for (const session of this.sessions.reverse()) {
|
|
140
|
+
md += `## ${session.timestamp}\n`;
|
|
141
|
+
md += `- **Model**: ${session.model}\n`;
|
|
142
|
+
md += `- **Prompt**: ${session.prompt}\n`;
|
|
143
|
+
md += `- **Tokens**: ${session.tokenCount}\n`;
|
|
144
|
+
md += `- **Messages**: ${session.messageCount}\n\n`;
|
|
145
|
+
}
|
|
146
|
+
return md;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (format === 'csv') {
|
|
150
|
+
const headers = ['timestamp', 'model', 'prompt', 'tokenCount', 'messageCount', 'duration'];
|
|
151
|
+
const rows = this.sessions.map((s) =>
|
|
152
|
+
headers.map((h) => `"${String(s[h] || '').replace(/"/g, '""')}"`).join(',')
|
|
153
|
+
);
|
|
154
|
+
return [headers.join(','), ...rows].join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { SessionHistory };
|