agen-vektor 0.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/dist/agent/agent.js +90 -0
  4. package/dist/agent/context.js +106 -0
  5. package/dist/agent/loop.js +152 -0
  6. package/dist/agent/memory.js +87 -0
  7. package/dist/agent/planner.js +38 -0
  8. package/dist/agent/prompts.js +41 -0
  9. package/dist/agent/session.js +125 -0
  10. package/dist/cli/index.js +304 -0
  11. package/dist/cli/keyboard.js +283 -0
  12. package/dist/config/config.js +92 -0
  13. package/dist/config/credentials.js +127 -0
  14. package/dist/config/models.js +47 -0
  15. package/dist/providers/anthropic.js +276 -0
  16. package/dist/providers/custom.js +24 -0
  17. package/dist/providers/factory.js +58 -0
  18. package/dist/providers/gemini.js +223 -0
  19. package/dist/providers/ollama.js +195 -0
  20. package/dist/providers/openai-compat.js +296 -0
  21. package/dist/providers/openai.js +17 -0
  22. package/dist/providers/openrouter.js +23 -0
  23. package/dist/providers/provider.js +39 -0
  24. package/dist/security/command-policy.js +99 -0
  25. package/dist/security/permissions.js +66 -0
  26. package/dist/tools/filesystem.js +297 -0
  27. package/dist/tools/git.js +66 -0
  28. package/dist/tools/registry.js +39 -0
  29. package/dist/tools/search.js +184 -0
  30. package/dist/tools/shell.js +108 -0
  31. package/dist/tools/web.js +76 -0
  32. package/dist/tui/app.js +913 -0
  33. package/dist/tui/chat.js +76 -0
  34. package/dist/tui/components.js +85 -0
  35. package/dist/tui/diff.js +105 -0
  36. package/dist/tui/input.js +121 -0
  37. package/dist/tui/statusbar.js +32 -0
  38. package/dist/utils/logger.js +96 -0
  39. package/dist/utils/paths.js +90 -0
  40. package/dist/utils/terminal.js +191 -0
  41. package/package.json +56 -0
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OllamaProvider = void 0;
4
+ /**
5
+ * Ollama provider — local models via http://localhost:11434/api/chat
6
+ * (or OLLAMA_HOST). No API key required.
7
+ */
8
+ const provider_1 = require("./provider");
9
+ const models_1 = require("../config/models");
10
+ function baseUrl() {
11
+ return (process.env.OLLAMA_HOST || 'http://localhost:11434').replace(/\/+$/, '');
12
+ }
13
+ function toOllamaMessages(messages) {
14
+ return messages
15
+ .filter((m) => m.role !== 'tool')
16
+ .map((m) => {
17
+ if (m.toolCalls && m.toolCalls.length > 0) {
18
+ return {
19
+ role: 'assistant',
20
+ content: m.content || '',
21
+ tool_calls: m.toolCalls.map((tc) => ({
22
+ function: { name: tc.name, arguments: safeParse(tc.arguments) },
23
+ })),
24
+ };
25
+ }
26
+ return { role: m.role, content: m.content };
27
+ });
28
+ }
29
+ function safeParse(s) {
30
+ try {
31
+ return JSON.parse(s);
32
+ }
33
+ catch {
34
+ return { _raw: s };
35
+ }
36
+ }
37
+ function toToolDefs(tools) {
38
+ return tools.map((t) => ({
39
+ type: 'function',
40
+ function: {
41
+ name: t.name,
42
+ description: t.description,
43
+ parameters: t.parameters,
44
+ },
45
+ }));
46
+ }
47
+ class OllamaProvider {
48
+ id = 'ollama';
49
+ name = 'Ollama';
50
+ capabilities() {
51
+ return { streaming: true, tools: true, contextWindow: 128000 };
52
+ }
53
+ async post(body, params) {
54
+ const controller = new AbortController();
55
+ const timer = setTimeout(() => controller.abort(), 180_000);
56
+ const onOuterAbort = () => controller.abort();
57
+ if (params.signal) {
58
+ if (params.signal.aborted)
59
+ controller.abort();
60
+ else
61
+ params.signal.addEventListener('abort', onOuterAbort);
62
+ }
63
+ const res = await fetch(`${baseUrl()}/api/chat`, {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify(body),
67
+ signal: controller.signal,
68
+ });
69
+ clearTimeout(timer);
70
+ if (params.signal)
71
+ params.signal.removeEventListener('abort', onOuterAbort);
72
+ return { res };
73
+ }
74
+ async chat(params) {
75
+ const body = {
76
+ model: params.model,
77
+ messages: toOllamaMessages(params.messages),
78
+ stream: false,
79
+ };
80
+ if (params.tools && params.tools.length > 0)
81
+ body.tools = toToolDefs(params.tools);
82
+ const { res } = await this.post(body, params);
83
+ const text = await res.text();
84
+ if (!res.ok) {
85
+ throw new provider_1.ProviderError(`Ollama HTTP ${res.status}: ${text.slice(0, 300)}`, res.status, res.status >= 500);
86
+ }
87
+ const data = JSON.parse(text);
88
+ const message = (data.message || {});
89
+ const toolCalls = (message.tool_calls || []).map((tc, i) => {
90
+ const fn = (tc.function || {});
91
+ return {
92
+ id: `ollama_${i}`,
93
+ name: String(fn.name || ''),
94
+ arguments: typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments ?? {}),
95
+ };
96
+ });
97
+ return {
98
+ content: String(message.content || ''),
99
+ toolCalls,
100
+ stopReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
101
+ model: String(data.model || params.model),
102
+ };
103
+ }
104
+ async *stream(params) {
105
+ const body = {
106
+ model: params.model,
107
+ messages: toOllamaMessages(params.messages),
108
+ stream: true,
109
+ };
110
+ if (params.tools && params.tools.length > 0)
111
+ body.tools = toToolDefs(params.tools);
112
+ const { res } = await this.post(body, params);
113
+ if (!res.ok || !res.body) {
114
+ const text = await res.text().catch(() => '');
115
+ throw new provider_1.ProviderError(`Ollama HTTP ${res.status}: ${text.slice(0, 200)}`, res.status, res.status >= 500);
116
+ }
117
+ const reader = res.body.getReader();
118
+ const decoder = new TextDecoder();
119
+ let buffer = '';
120
+ let content = '';
121
+ const toolCalls = [];
122
+ for (;;) {
123
+ const { done, value } = await reader.read();
124
+ if (done)
125
+ break;
126
+ buffer += decoder.decode(value, { stream: true });
127
+ const lines = buffer.split('\n');
128
+ buffer = lines.pop() || '';
129
+ for (const line of lines) {
130
+ const trimmed = line.trim();
131
+ if (!trimmed)
132
+ continue;
133
+ const json = safeJson(trimmed);
134
+ if (!json)
135
+ continue;
136
+ if (json.done) {
137
+ yield {
138
+ type: 'done',
139
+ result: {
140
+ content,
141
+ toolCalls,
142
+ stopReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
143
+ model: params.model,
144
+ },
145
+ };
146
+ return;
147
+ }
148
+ const message = (json.message || {});
149
+ if (typeof message.content === 'string' && message.content) {
150
+ content += message.content;
151
+ yield { type: 'delta', delta: message.content };
152
+ }
153
+ const tcs = message.tool_calls || [];
154
+ for (const tc of tcs) {
155
+ const fn = (tc.function || {});
156
+ const call = {
157
+ id: `ollama_${toolCalls.length}`,
158
+ name: String(fn.name || ''),
159
+ arguments: typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments ?? {}),
160
+ };
161
+ toolCalls.push(call);
162
+ yield { type: 'tool_call', toolCall: call };
163
+ }
164
+ }
165
+ }
166
+ yield {
167
+ type: 'done',
168
+ result: { content, toolCalls, stopReason: 'stop', model: params.model },
169
+ };
170
+ }
171
+ async models() {
172
+ try {
173
+ const res = await fetch(`${baseUrl()}/api/tags`);
174
+ if (res.ok) {
175
+ const data = (await res.json());
176
+ if (Array.isArray(data.models)) {
177
+ return data.models.map((m) => String(m.name)).slice(0, 100);
178
+ }
179
+ }
180
+ }
181
+ catch {
182
+ /* offline */
183
+ }
184
+ return (0, models_1.modelsForProvider)(this.id).map((m) => m.id);
185
+ }
186
+ }
187
+ exports.OllamaProvider = OllamaProvider;
188
+ function safeJson(text) {
189
+ try {
190
+ return JSON.parse(text);
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ }
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenAICompatProvider = void 0;
4
+ /**
5
+ * OpenAI-compatible provider — used by OpenAI, OpenRouter and Custom.
6
+ * Speaks /v1/chat/completions (and /models) with tool calling + streaming.
7
+ */
8
+ const provider_1 = require("./provider");
9
+ const credentials_1 = require("../config/credentials");
10
+ const models_1 = require("../config/models");
11
+ function toOpenAIMessages(messages) {
12
+ const out = [];
13
+ for (const m of messages) {
14
+ if (m.role === 'tool') {
15
+ out.push({
16
+ role: 'tool',
17
+ tool_call_id: m.toolCallId,
18
+ content: m.content,
19
+ });
20
+ continue;
21
+ }
22
+ const base = { role: m.role, content: m.content };
23
+ if (m.toolCalls && m.toolCalls.length > 0) {
24
+ base.tool_calls = m.toolCalls.map((tc) => ({
25
+ id: tc.id,
26
+ type: 'function',
27
+ function: { name: tc.name, arguments: tc.arguments },
28
+ }));
29
+ }
30
+ if (m.name)
31
+ base.name = m.name;
32
+ out.push(base);
33
+ }
34
+ return out;
35
+ }
36
+ function toToolDefs(tools) {
37
+ return tools.map((t) => ({
38
+ type: 'function',
39
+ function: {
40
+ name: t.name,
41
+ description: t.description,
42
+ parameters: t.parameters,
43
+ },
44
+ }));
45
+ }
46
+ function parseToolCalls(raw) {
47
+ const calls = [];
48
+ if (!Array.isArray(raw))
49
+ return calls;
50
+ for (const item of raw) {
51
+ const fn = item.function;
52
+ if (!fn)
53
+ continue;
54
+ calls.push({
55
+ id: String(item.id || `call_${Math.random().toString(36).slice(2)}`),
56
+ name: String(fn.name || 'unknown'),
57
+ arguments: typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments),
58
+ });
59
+ }
60
+ return calls;
61
+ }
62
+ class OpenAICompatProvider {
63
+ id;
64
+ name;
65
+ baseUrl;
66
+ apiKey;
67
+ defaultModel;
68
+ contextWindow;
69
+ constructor(opts) {
70
+ this.id = opts.id;
71
+ this.name = opts.name;
72
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
73
+ this.apiKey = opts.apiKey !== undefined ? opts.apiKey : (0, credentials_1.getApiKey)(opts.id);
74
+ this.defaultModel = opts.defaultModel || 'gpt-4o-mini';
75
+ this.contextWindow = opts.contextWindow || 128000;
76
+ }
77
+ capabilities() {
78
+ return {
79
+ streaming: true,
80
+ tools: true,
81
+ contextWindow: this.contextWindow,
82
+ };
83
+ }
84
+ endpoint(path) {
85
+ return `${this.baseUrl}${path}`;
86
+ }
87
+ headers() {
88
+ const h = {
89
+ 'Content-Type': 'application/json',
90
+ };
91
+ if (this.apiKey)
92
+ h.Authorization = `Bearer ${this.apiKey}`;
93
+ return h;
94
+ }
95
+ buildBody(params, stream) {
96
+ const body = {
97
+ model: params.model || this.defaultModel,
98
+ messages: toOpenAIMessages(params.messages),
99
+ stream,
100
+ };
101
+ if (params.tools && params.tools.length > 0) {
102
+ body.tools = toToolDefs(params.tools);
103
+ }
104
+ if (params.temperature !== undefined)
105
+ body.temperature = params.temperature;
106
+ if (params.maxTokens)
107
+ body.max_tokens = params.maxTokens;
108
+ return body;
109
+ }
110
+ async postJson(path, body, signal, timeoutMs = 120_000) {
111
+ const controller = new AbortController();
112
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
113
+ const onOuterAbort = () => controller.abort();
114
+ if (signal) {
115
+ if (signal.aborted)
116
+ controller.abort();
117
+ else
118
+ signal.addEventListener('abort', onOuterAbort);
119
+ }
120
+ try {
121
+ const res = await fetch(this.endpoint(path), {
122
+ method: 'POST',
123
+ headers: this.headers(),
124
+ body: JSON.stringify(body),
125
+ signal: controller.signal,
126
+ });
127
+ const text = await res.text();
128
+ if (!res.ok) {
129
+ const detail = safeJson(text);
130
+ const errObj = (detail && detail.error);
131
+ const msg = errObj?.message || text.slice(0, 300);
132
+ throw new provider_1.ProviderError(msg, res.status, res.status >= 500 || res.status === 429);
133
+ }
134
+ return safeJson(text) || {};
135
+ }
136
+ finally {
137
+ clearTimeout(timer);
138
+ if (signal)
139
+ signal.removeEventListener('abort', onOuterAbort);
140
+ }
141
+ }
142
+ async chat(params) {
143
+ const data = await this.postJson('/chat/completions', this.buildBody(params, false), params.signal, params.maxTokens ? 180_000 : undefined);
144
+ const choice = data.choices?.[0];
145
+ const msg = (choice?.message || {});
146
+ const finish = String(choice?.finish_reason || 'stop');
147
+ const usage = data.usage;
148
+ return {
149
+ content: typeof msg.content === 'string' ? msg.content : '',
150
+ toolCalls: parseToolCalls(msg.tool_calls),
151
+ stopReason: finish,
152
+ usage: usage
153
+ ? {
154
+ inputTokens: usage.prompt_tokens,
155
+ outputTokens: usage.completion_tokens,
156
+ totalTokens: usage.total_tokens,
157
+ }
158
+ : undefined,
159
+ model: String(data.model || params.model),
160
+ };
161
+ }
162
+ async *stream(params) {
163
+ const controller = new AbortController();
164
+ const timer = setTimeout(() => controller.abort(), 180_000);
165
+ const onOuterAbort = () => controller.abort();
166
+ if (params.signal) {
167
+ if (params.signal.aborted)
168
+ controller.abort();
169
+ else
170
+ params.signal.addEventListener('abort', onOuterAbort);
171
+ }
172
+ try {
173
+ const res = await fetch(this.endpoint('/chat/completions'), {
174
+ method: 'POST',
175
+ headers: this.headers(),
176
+ body: JSON.stringify(this.buildBody(params, true)),
177
+ signal: controller.signal,
178
+ });
179
+ if (!res.ok || !res.body) {
180
+ const text = await res.text().catch(() => '');
181
+ const detail = safeJson(text);
182
+ const errObj = (detail && detail.error);
183
+ throw new provider_1.ProviderError(errObj?.message || `HTTP ${res.status}: ${text.slice(0, 200)}`, res.status, res.status >= 500 || res.status === 429);
184
+ }
185
+ const reader = res.body.getReader();
186
+ const decoder = new TextDecoder();
187
+ let buffer = '';
188
+ let content = '';
189
+ const toolCalls = [];
190
+ for (;;) {
191
+ const { done, value } = await reader.read();
192
+ if (done)
193
+ break;
194
+ buffer += decoder.decode(value, { stream: true });
195
+ const lines = buffer.split('\n');
196
+ buffer = lines.pop() || '';
197
+ for (const line of lines) {
198
+ const trimmed = line.trim();
199
+ if (!trimmed.startsWith('data:'))
200
+ continue;
201
+ const payload = trimmed.slice(5).trim();
202
+ if (payload === '[DONE]') {
203
+ yield {
204
+ type: 'done',
205
+ result: {
206
+ content,
207
+ toolCalls,
208
+ stopReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
209
+ model: params.model,
210
+ },
211
+ };
212
+ return;
213
+ }
214
+ const json = safeJson(payload);
215
+ if (!json)
216
+ continue;
217
+ const choice = json.choices?.[0];
218
+ if (!choice)
219
+ continue;
220
+ const delta = (choice.delta || {});
221
+ if (typeof delta.content === 'string' && delta.content) {
222
+ content += delta.content;
223
+ yield { type: 'delta', delta: delta.content };
224
+ }
225
+ const tc = delta.tool_calls;
226
+ if (Array.isArray(tc)) {
227
+ for (const part of tc) {
228
+ const idx = Number(part.index || 0);
229
+ const fn = (part.function || {});
230
+ const existing = toolCalls[idx];
231
+ if (!existing) {
232
+ toolCalls[idx] = {
233
+ id: String(part.id || `call_${idx}_${Math.random().toString(36).slice(2)}`),
234
+ name: String(fn.name || ''),
235
+ arguments: String(fn.arguments || ''),
236
+ };
237
+ }
238
+ else {
239
+ if (part.id)
240
+ existing.id = String(part.id);
241
+ if (fn.name)
242
+ existing.name = String(fn.name);
243
+ if (fn.arguments)
244
+ existing.arguments += String(fn.arguments);
245
+ }
246
+ }
247
+ const finished = toolCalls.filter((t) => t.name).map((t) => ({
248
+ ...t,
249
+ arguments: t.arguments || '{}',
250
+ }));
251
+ if (finished.length > 0) {
252
+ yield { type: 'tool_call', toolCall: finished[finished.length - 1] };
253
+ }
254
+ }
255
+ }
256
+ }
257
+ // Stream ended without [DONE]
258
+ yield {
259
+ type: 'done',
260
+ result: {
261
+ content,
262
+ toolCalls: toolCalls.filter((t) => t.name),
263
+ stopReason: 'stop',
264
+ model: params.model,
265
+ },
266
+ };
267
+ }
268
+ finally {
269
+ clearTimeout(timer);
270
+ if (params.signal)
271
+ params.signal.removeEventListener('abort', onOuterAbort);
272
+ }
273
+ }
274
+ async models() {
275
+ try {
276
+ const data = await this.postJson('/models', {});
277
+ const list = data.data;
278
+ if (Array.isArray(list) && list.length > 0) {
279
+ return list.map((m) => String(m.id)).slice(0, 100);
280
+ }
281
+ }
282
+ catch {
283
+ /* fall back to known list */
284
+ }
285
+ return (0, models_1.modelsForProvider)(this.id).map((m) => m.id);
286
+ }
287
+ }
288
+ exports.OpenAICompatProvider = OpenAICompatProvider;
289
+ function safeJson(text) {
290
+ try {
291
+ return JSON.parse(text);
292
+ }
293
+ catch {
294
+ return null;
295
+ }
296
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenAIProvider = void 0;
4
+ const openai_compat_1 = require("./openai-compat");
5
+ class OpenAIProvider extends openai_compat_1.OpenAICompatProvider {
6
+ constructor(apiKey) {
7
+ super({
8
+ id: 'openai',
9
+ name: 'OpenAI',
10
+ baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
11
+ apiKey,
12
+ defaultModel: 'gpt-4o-mini',
13
+ contextWindow: 128000,
14
+ });
15
+ }
16
+ }
17
+ exports.OpenAIProvider = OpenAIProvider;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenRouterProvider = void 0;
4
+ const openai_compat_1 = require("./openai-compat");
5
+ class OpenRouterProvider extends openai_compat_1.OpenAICompatProvider {
6
+ constructor(apiKey) {
7
+ super({
8
+ id: 'openrouter',
9
+ name: 'OpenRouter',
10
+ baseUrl: 'https://openrouter.ai/api/v1',
11
+ apiKey,
12
+ defaultModel: 'deepseek/deepseek-chat',
13
+ contextWindow: 128000,
14
+ });
15
+ }
16
+ headers() {
17
+ const h = super.headers();
18
+ h['HTTP-Referer'] = 'https://github.com/clickmamaheti-prog/vector-agent';
19
+ h['X-Title'] = 'VECTOR AI Coding Agent';
20
+ return h;
21
+ }
22
+ }
23
+ exports.OpenRouterProvider = OpenRouterProvider;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProviderError = void 0;
4
+ exports.withRetry = withRetry;
5
+ /** Error thrown by providers for user-facing issues. */
6
+ class ProviderError extends Error {
7
+ status;
8
+ retryable;
9
+ constructor(message, status, retryable = false) {
10
+ super(message);
11
+ this.status = status;
12
+ this.retryable = retryable;
13
+ this.name = 'ProviderError';
14
+ }
15
+ }
16
+ exports.ProviderError = ProviderError;
17
+ /** Simple retry wrapper for provider calls. */
18
+ async function withRetry(fn, { retries = 3, delayMs = 1000, signal } = {}) {
19
+ let attempt = 0;
20
+ for (;;) {
21
+ try {
22
+ return await fn();
23
+ }
24
+ catch (err) {
25
+ attempt++;
26
+ const retryable = err instanceof ProviderError
27
+ ? err.retryable
28
+ : err.name === 'AbortError'
29
+ ? false
30
+ : true;
31
+ if (!retryable || attempt > retries)
32
+ throw err;
33
+ const wait = delayMs * Math.pow(2, attempt - 1);
34
+ await new Promise((r) => setTimeout(r, wait));
35
+ if (signal?.aborted)
36
+ throw new Error('aborted');
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyCommand = classifyCommand;
4
+ const BLOCKED_PATTERNS = [
5
+ /(^|[;&|]\s*)\s*rm\s+(-[a-z]*[rf][a-z]*\s+)*\/\s*($|[^a-zA-Z])/,
6
+ /(^|[;&|]\s*)\s*rm\s+(-[a-z]*[rf][a-z]*\s+)*~(\/|$)/,
7
+ /(^|[;&|]\s*)\s*rm\s+(-[a-z]*[rf][a-z]*\s+)*\/home(\/|$)/,
8
+ /(^|[;&|]\s*)\s*mkfs/,
9
+ /(^|[;&|]\s*)\s*dd\s+if=.*of=\/dev\/(sd|nvme|vd|mmcblk)/,
10
+ /(^|[;&|]\s*)\s*shutdown/,
11
+ /(^|[;&|]\s*)\s*reboot/,
12
+ /(^|[;&|]\s*)\s*halt/,
13
+ /(^|[;&|]\s*)\s*poweroff/,
14
+ /(^|[;&|]\s*)\s*:\(\)\s*{\s*:\|:&\s*}\s*;/,
15
+ /(^|[;&|]\s*)\s*chmod\s+(-R\s+)?[0-7]{3,4}\s*\/\s*($| )/,
16
+ /(^|[;&|]\s*)\s*chown\s+(-R\s+)?[^\s]+\s*\/\s*($| )/,
17
+ /(^|[;&|]\s*)\s*sudo\s+rm\s+-rf\s+\//,
18
+ /(^|[;&|]\s*)\s*git\s+push\s+(-f|--force)/,
19
+ /(^|[;&|]\s*)\s*git\s+reset\s+--hard\s+HEAD~?[0-9]*.*(--)?$/,
20
+ ];
21
+ const DANGEROUS_PATTERNS = [
22
+ /(^|[;&|]\s*)\s*rm\s+(-[a-z]*[rf][a-z]*\s+)*/,
23
+ /(^|[;&|]\s*)\s*mv\s+.*\/\.?git/,
24
+ /(^|[;&|]\s*)\s*git\s+(push|reset|rebase|clean|gc|prune)/,
25
+ /(^|[;&|]\s*)\s*drop\s+(table|database)/,
26
+ /(^|[;&|]\s*)\s*DELETE\s+FROM/i,
27
+ /(^|[;&|]\s*)\s*TRUNCATE\s/i,
28
+ /(^|[;&|]\s*)\s*curl\s+.*\|\s*(sudo\s+)?(sh|bash)/,
29
+ /(^|[;&|]\s*)\s*wget\s+.*\|\s*(sudo\s+)?(sh|bash)/,
30
+ /(^|[;&|]\s*)\s*chmod\s+[0-7]{3,4}/,
31
+ /(^|[;&|]\s*)\s*chown\s+/,
32
+ /(^|[;&|]\s*)\s*sudo\s+(apt|apt-get|dnf|yum|pacman)\s+(install|remove|purge|autoremove)/,
33
+ /(^|[;&|]\s*)\s*npm\s+(uninstall|remove|rm)\s+/,
34
+ /(^|[;&|]\s*)\s*pip(\d)?\s+uninstall\s+/,
35
+ /(^|[;&|]\s*)\s*killall\s+/,
36
+ /(^|[;&|]\s*)\s*pkill\s+/,
37
+ /(^|[;&|]\s*)\s*rmdir\s+/,
38
+ ];
39
+ const ASK_PATTERNS = [
40
+ /(^|[;&|]\s*)\s*npm\s+(install|i|add)\s+/,
41
+ /(^|[;&|]\s*)\s*npm\s+run\s+(build|test|dev|start|preview)/,
42
+ /(^|[;&|]\s*)\s*npx\s+/,
43
+ /(^|[;&|]\s*)\s*pnpm\s+/,
44
+ /(^|[;&|]\s*)\s*yarn\s+/,
45
+ /(^|[;&|]\s*)\s*pip(\d)?\s+install\s+/,
46
+ /(^|[;&|]\s*)\s*poetry\s+install/,
47
+ /(^|[;&|]\s*)\s*go\s+(mod\s+download|get\s+)/,
48
+ /(^|[;&|]\s*)\s*cargo\s+(install|add)\s+/,
49
+ /(^|[;&|]\s*)\s*sudo\s+/,
50
+ /(^|[;&|]\s*)\s*git\s+commit\s+/,
51
+ /(^|[;&|]\s*)\s*make\s+(install|deploy)/,
52
+ /(^|[;&|]\s*)\s*docker\s+/,
53
+ /(^|[;&|]\s*)\s*curl\s+/,
54
+ /(^|[;&|]\s*)\s*wget\s+/,
55
+ /(^|[;&|]\s*)\s*ssh\s+/,
56
+ /(^|[;&|]\s*)\s*scp\s+/,
57
+ /(^|[;&|]\s*)\s*rsync\s+/,
58
+ ];
59
+ /** Commands that are safe to run without asking. */
60
+ const SAFE_PATTERNS = [
61
+ /(^|[;&|]\s*)\s*(ls|cat|head|tail|less|more|wc|grep|find|locate|which|type|file|stat|du|df|tree)\s/,
62
+ /(^|[;&|]\s*)\s*echo\s/,
63
+ /(^|[;&|]\s*)\s*node\s+(-v|--version)/,
64
+ /(^|[;&|]\s*)\s*npm\s+(-v|--version|ls|list)\s*/,
65
+ /(^|[;&|]\s*)\s*git\s+(status|diff|log|branch|show|remote|rev-parse|config)\s*/,
66
+ /(^|[;&|]\s*)\s*tsc\s+--noEmit/,
67
+ /(^|[;&|]\s*)\s*python3?\s+(-V|--version)/,
68
+ /(^|[;&|]\s*)\s*date\s*/,
69
+ /(^|[;&|]\s*)\s*pwd\s*/,
70
+ /(^|[;&|]\s*)\s*env\s*/,
71
+ /(^|[;&|]\s*)\s*uname\s*/,
72
+ /(^|[;&|]\s*)\s*whoami\s*/,
73
+ ];
74
+ function classifyCommand(command) {
75
+ // Blocked patterns take priority
76
+ for (const re of BLOCKED_PATTERNS) {
77
+ if (re.test(command)) {
78
+ return { level: 'blocked', reason: `command matches blocked pattern: ${re.source}` };
79
+ }
80
+ }
81
+ for (const re of DANGEROUS_PATTERNS) {
82
+ if (re.test(command)) {
83
+ return { level: 'dangerous', reason: `command matches dangerous pattern: ${re.source}` };
84
+ }
85
+ }
86
+ for (const re of ASK_PATTERNS) {
87
+ if (re.test(command)) {
88
+ return { level: 'ask', reason: `command matches ask pattern: ${re.source}` };
89
+ }
90
+ }
91
+ // Default safe classification for read-only style commands
92
+ for (const re of SAFE_PATTERNS) {
93
+ if (re.test(command)) {
94
+ return { level: 'safe', reason: 'command matches safe pattern' };
95
+ }
96
+ }
97
+ // Unknown commands: ask by default (safe by default philosophy)
98
+ return { level: 'ask', reason: 'unknown command — asking by default' };
99
+ }