blyuecode 0.1.9 → 0.1.11
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/dist/core/agent.js +36 -11
- package/dist/core/diff.js +63 -0
- package/dist/core/permission.js +44 -0
- package/dist/core/provider.js +39 -1
- package/dist/core/redact.js +42 -0
- package/dist/core/sandbox.js +35 -0
- package/dist/core/session.js +57 -0
- package/dist/core/tools.js +86 -86
- package/dist/index.js +10 -4
- package/dist/ui/Chat.js +68 -11
- package/dist/ui/Consent.js +26 -0
- package/dist/ui/PermissionPrompt.js +34 -0
- package/dist/ui/theme.js +2 -1
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -1,30 +1,54 @@
|
|
|
1
1
|
import { Provider } from './provider.js';
|
|
2
2
|
import { getToolDefs, executeTool } from './tools.js';
|
|
3
|
+
import { loadSession, saveSession, clearSession } from './session.js';
|
|
4
|
+
const SYSTEM_PROMPT = 'You are BLYUE, a coding assistant running in a terminal. ' +
|
|
5
|
+
'Use tools to read, write, and run commands. ' +
|
|
6
|
+
'If n8n tools are available, you can also manage n8n workflows. ' +
|
|
7
|
+
'Always explain what you are doing. Be concise.';
|
|
3
8
|
export class Agent {
|
|
4
9
|
provider = null;
|
|
10
|
+
messages;
|
|
11
|
+
constructor(resume = true) {
|
|
12
|
+
const sys = { role: 'system', content: SYSTEM_PROMPT };
|
|
13
|
+
const history = resume ? loadSession() : null;
|
|
14
|
+
if (history && history.length > 0) {
|
|
15
|
+
this.messages = [sys, ...history];
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this.messages = [sys];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
5
21
|
getProvider() {
|
|
6
22
|
if (!this.provider)
|
|
7
23
|
this.provider = new Provider();
|
|
8
24
|
return this.provider;
|
|
9
25
|
}
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
persist() {
|
|
27
|
+
try {
|
|
28
|
+
saveSession(this.messages.slice(1)); // skip system prompt
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
historySize() {
|
|
33
|
+
return this.messages.length - 1;
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
this.messages = [{ role: 'system', content: SYSTEM_PROMPT }];
|
|
37
|
+
clearSession();
|
|
38
|
+
}
|
|
19
39
|
async *run(userInput) {
|
|
20
40
|
this.messages.push({ role: 'user', content: userInput });
|
|
41
|
+
this.persist();
|
|
21
42
|
while (true) {
|
|
22
43
|
const tools = getToolDefs();
|
|
23
44
|
const res = await this.getProvider().chat(this.messages, tools);
|
|
24
|
-
if (res.content)
|
|
45
|
+
if (res.content) {
|
|
25
46
|
yield res.content;
|
|
26
|
-
|
|
47
|
+
}
|
|
48
|
+
if (res.toolCalls.length === 0) {
|
|
49
|
+
this.persist();
|
|
27
50
|
break;
|
|
51
|
+
}
|
|
28
52
|
this.messages.push({
|
|
29
53
|
role: 'assistant',
|
|
30
54
|
content: res.content,
|
|
@@ -50,6 +74,7 @@ export class Agent {
|
|
|
50
74
|
const preview = result.length > 500 ? result.slice(0, 500) + '...' : result;
|
|
51
75
|
yield '[Result]: ' + preview + '\n';
|
|
52
76
|
}
|
|
77
|
+
this.persist();
|
|
53
78
|
}
|
|
54
79
|
}
|
|
55
80
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Simple LCS-based line diff. Good enough for previews.
|
|
2
|
+
export function lineDiff(oldText, newText) {
|
|
3
|
+
const a = oldText.length ? oldText.split('\n') : [];
|
|
4
|
+
const b = newText.length ? newText.split('\n') : [];
|
|
5
|
+
const m = a.length;
|
|
6
|
+
const n = b.length;
|
|
7
|
+
// LCS table
|
|
8
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
9
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
10
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
11
|
+
if (a[i] === b[j])
|
|
12
|
+
dp[i][j] = dp[i + 1][j + 1] + 1;
|
|
13
|
+
else
|
|
14
|
+
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const out = [];
|
|
18
|
+
let i = 0;
|
|
19
|
+
let j = 0;
|
|
20
|
+
while (i < m && j < n) {
|
|
21
|
+
if (a[i] === b[j]) {
|
|
22
|
+
out.push({ type: 'same', text: a[i] });
|
|
23
|
+
i++;
|
|
24
|
+
j++;
|
|
25
|
+
}
|
|
26
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
27
|
+
out.push({ type: 'remove', text: a[i] });
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
out.push({ type: 'add', text: b[j] });
|
|
32
|
+
j++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
while (i < m)
|
|
36
|
+
out.push({ type: 'remove', text: a[i++] });
|
|
37
|
+
while (j < n)
|
|
38
|
+
out.push({ type: 'add', text: b[j++] });
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
// Compact display: only show changed regions with context
|
|
42
|
+
export function compactDiff(diff, context = 2) {
|
|
43
|
+
const keep = new Set();
|
|
44
|
+
diff.forEach((d, idx) => {
|
|
45
|
+
if (d.type !== 'same') {
|
|
46
|
+
for (let k = Math.max(0, idx - context); k <= Math.min(diff.length - 1, idx + context); k++) {
|
|
47
|
+
keep.add(k);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const out = [];
|
|
52
|
+
let lastKept = -1;
|
|
53
|
+
for (let i = 0; i < diff.length; i++) {
|
|
54
|
+
if (keep.has(i)) {
|
|
55
|
+
if (lastKept !== -1 && i > lastKept + 1) {
|
|
56
|
+
out.push({ type: 'same', text: '…' });
|
|
57
|
+
}
|
|
58
|
+
out.push(diff[i]);
|
|
59
|
+
lastKept = i;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
class PermissionBroker {
|
|
2
|
+
pending = null;
|
|
3
|
+
// Session-scoped "always allow" set (cleared on restart)
|
|
4
|
+
alwaysAllow = new Set();
|
|
5
|
+
listeners = new Set();
|
|
6
|
+
nextId = 0;
|
|
7
|
+
subscribe(fn) {
|
|
8
|
+
this.listeners.add(fn);
|
|
9
|
+
// Immediately notify with current state
|
|
10
|
+
fn(this.pending?.req ?? null);
|
|
11
|
+
return () => this.listeners.delete(fn);
|
|
12
|
+
}
|
|
13
|
+
notify(req) {
|
|
14
|
+
for (const fn of this.listeners)
|
|
15
|
+
fn(req);
|
|
16
|
+
}
|
|
17
|
+
isAlwaysAllowed(tool) {
|
|
18
|
+
return this.alwaysAllow.has(tool);
|
|
19
|
+
}
|
|
20
|
+
async ask(req) {
|
|
21
|
+
if (this.alwaysAllow.has(req.tool))
|
|
22
|
+
return 'yes';
|
|
23
|
+
const id = `p${++this.nextId}`;
|
|
24
|
+
const full = { ...req, id };
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
this.pending = { req: full, resolve };
|
|
27
|
+
this.notify(full);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
answer(a) {
|
|
31
|
+
if (!this.pending)
|
|
32
|
+
return;
|
|
33
|
+
const { req, resolve } = this.pending;
|
|
34
|
+
if (a === 'always')
|
|
35
|
+
this.alwaysAllow.add(req.tool);
|
|
36
|
+
this.pending = null;
|
|
37
|
+
this.notify(null);
|
|
38
|
+
resolve(a);
|
|
39
|
+
}
|
|
40
|
+
resetSession() {
|
|
41
|
+
this.alwaysAllow.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const permissions = new PermissionBroker();
|
package/dist/core/provider.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { redact } from './redact.js';
|
|
6
|
+
const DEBUG_DIR = path.join(os.homedir(), '.blyue');
|
|
7
|
+
const LAST_REQUEST = path.join(DEBUG_DIR, 'last-request.json');
|
|
2
8
|
export class Provider {
|
|
3
9
|
client;
|
|
4
10
|
model;
|
|
@@ -12,14 +18,46 @@ export class Provider {
|
|
|
12
18
|
baseURL: process.env.OPENAI_BASE_URL,
|
|
13
19
|
});
|
|
14
20
|
}
|
|
21
|
+
sanitize(messages) {
|
|
22
|
+
let redactedCount = 0;
|
|
23
|
+
const out = messages.map((m) => {
|
|
24
|
+
if (typeof m.content !== 'string')
|
|
25
|
+
return m;
|
|
26
|
+
const { text, count } = redact(m.content);
|
|
27
|
+
redactedCount += count;
|
|
28
|
+
return { ...m, content: text };
|
|
29
|
+
});
|
|
30
|
+
return { messages: out, redactedCount };
|
|
31
|
+
}
|
|
32
|
+
writeDebug(payload) {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(DEBUG_DIR))
|
|
35
|
+
fs.mkdirSync(DEBUG_DIR, { recursive: true });
|
|
36
|
+
fs.writeFileSync(LAST_REQUEST, JSON.stringify(payload, null, 2));
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
15
40
|
async chat(messages, tools) {
|
|
41
|
+
const { messages: safeMessages, redactedCount } = this.sanitize(messages);
|
|
16
42
|
try {
|
|
17
43
|
const res = await this.client.chat.completions.create({
|
|
18
44
|
model: this.model,
|
|
19
|
-
messages:
|
|
45
|
+
messages: safeMessages,
|
|
20
46
|
tools,
|
|
21
47
|
tool_choice: 'auto',
|
|
22
48
|
});
|
|
49
|
+
// Log debug info after successful call
|
|
50
|
+
const totalBytes = JSON.stringify(safeMessages).length;
|
|
51
|
+
this.writeDebug({
|
|
52
|
+
model: this.model,
|
|
53
|
+
baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
|
|
54
|
+
messages: safeMessages,
|
|
55
|
+
approxTokens: Math.round(totalBytes / 4),
|
|
56
|
+
approxBytes: totalBytes,
|
|
57
|
+
messageCount: safeMessages.length,
|
|
58
|
+
redactedCount,
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
});
|
|
23
61
|
const msg = res.choices[0].message;
|
|
24
62
|
return {
|
|
25
63
|
content: msg.content,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const PATTERNS = [
|
|
2
|
+
// OpenAI / OpenRouter
|
|
3
|
+
[/sk-or-v1-[a-zA-Z0-9]{32,}/g, 'sk-or-v1-***REDACTED***'],
|
|
4
|
+
[/sk-[a-zA-Z0-9]{20,}/g, 'sk-***REDACTED***'],
|
|
5
|
+
// Anthropic
|
|
6
|
+
[/sk-ant-[a-zA-Z0-9\-_]{20,}/g, 'sk-ant-***REDACTED***'],
|
|
7
|
+
// GitHub
|
|
8
|
+
[/ghp_[a-zA-Z0-9]{36,}/g, 'ghp_***REDACTED***'],
|
|
9
|
+
[/gho_[a-zA-Z0-9]{36,}/g, 'gho_***REDACTED***'],
|
|
10
|
+
[/ghs_[a-zA-Z0-9]{36,}/g, 'ghs_***REDACTED***'],
|
|
11
|
+
[/github_pat_[a-zA-Z0-9_]{60,}/g, 'github_pat_***REDACTED***'],
|
|
12
|
+
// AWS
|
|
13
|
+
[/AKIA[0-9A-Z]{16}/g, 'AKIA***REDACTED***'],
|
|
14
|
+
[/aws_secret_access_key\s*=\s*[A-Za-z0-9/+=]{40}/gi, 'aws_secret_access_key=***REDACTED***'],
|
|
15
|
+
// Google
|
|
16
|
+
[/AIza[0-9A-Za-z_\-]{35}/g, 'AIza***REDACTED***'],
|
|
17
|
+
// Slack
|
|
18
|
+
[/xox[baprs]-[a-zA-Z0-9-]{10,}/g, 'xox***REDACTED***'],
|
|
19
|
+
// Stripe
|
|
20
|
+
[/sk_live_[a-zA-Z0-9]{24,}/g, 'sk_live_***REDACTED***'],
|
|
21
|
+
[/pk_live_[a-zA-Z0-9]{24,}/g, 'pk_live_***REDACTED***'],
|
|
22
|
+
// JWT
|
|
23
|
+
[/eyJ[a-zA-Z0-9_\-]{10,}\.eyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}/g, '***REDACTED JWT***'],
|
|
24
|
+
// Bearer tokens
|
|
25
|
+
[/Bearer\s+[a-zA-Z0-9_\-\.]{20,}/g, 'Bearer ***REDACTED***'],
|
|
26
|
+
// PEM keys
|
|
27
|
+
[/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, '-----***REDACTED KEY***-----'],
|
|
28
|
+
// Generic env-style secrets (password=, token=, secret=)
|
|
29
|
+
[/(password|passwd|pwd|secret|token|api_key|apikey)\s*[:=]\s*["']?([A-Za-z0-9_\-\.]{16,})["']?/gi,
|
|
30
|
+
'$1=***REDACTED***'],
|
|
31
|
+
];
|
|
32
|
+
export function redact(text) {
|
|
33
|
+
let out = text;
|
|
34
|
+
let count = 0;
|
|
35
|
+
for (const [re, replacement] of PATTERNS) {
|
|
36
|
+
const before = out;
|
|
37
|
+
out = out.replace(re, replacement);
|
|
38
|
+
if (out !== before)
|
|
39
|
+
count++;
|
|
40
|
+
}
|
|
41
|
+
return { text: out, count };
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
const BLOCKED_NAMES = [
|
|
4
|
+
'.env', '.env.local', '.env.production', '.env.staging',
|
|
5
|
+
'id_rsa', 'id_ed25519', 'id_dsa', 'id_ecdsa',
|
|
6
|
+
'credentials', 'secrets.json', 'secrets.yml', 'secrets.yaml',
|
|
7
|
+
'.git-credentials', '.npmrc', '.pypirc', '.netrc', '.htpasswd',
|
|
8
|
+
'known_hosts', 'authorized_keys',
|
|
9
|
+
];
|
|
10
|
+
const BLOCKED_EXTENSIONS = [
|
|
11
|
+
'.pem', '.key', '.p12', '.pfx', '.keystore', '.jks',
|
|
12
|
+
];
|
|
13
|
+
const BLOCKED_DIRS = [
|
|
14
|
+
'.ssh', '.aws', '.gnupg', '.gpg', '.kube',
|
|
15
|
+
'.docker', '.config/gcloud', '.password-store',
|
|
16
|
+
];
|
|
17
|
+
export function isBlocked(p) {
|
|
18
|
+
const abs = path.resolve(p);
|
|
19
|
+
const base = path.basename(abs).toLowerCase();
|
|
20
|
+
if (BLOCKED_NAMES.some((n) => base === n.toLowerCase())) {
|
|
21
|
+
return `"${path.basename(abs)}" is a sensitive file (blocked)`;
|
|
22
|
+
}
|
|
23
|
+
const ext = path.extname(abs).toLowerCase();
|
|
24
|
+
if (BLOCKED_EXTENSIONS.includes(ext)) {
|
|
25
|
+
return `"${path.basename(abs)}" has a sensitive extension (blocked)`;
|
|
26
|
+
}
|
|
27
|
+
const home = os.homedir();
|
|
28
|
+
for (const d of BLOCKED_DIRS) {
|
|
29
|
+
const target = path.join(home, d);
|
|
30
|
+
if (abs === target || abs.startsWith(target + path.sep)) {
|
|
31
|
+
return `Path is inside ~/${d} (blocked)`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
const SESSIONS_DIR = path.join(os.homedir(), '.blyue', 'sessions');
|
|
6
|
+
export function sessionFile(cwd = process.cwd()) {
|
|
7
|
+
if (!fs.existsSync(SESSIONS_DIR))
|
|
8
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
9
|
+
const hash = crypto.createHash('sha1').update(cwd).digest('hex').slice(0, 12);
|
|
10
|
+
return path.join(SESSIONS_DIR, `${hash}.json`);
|
|
11
|
+
}
|
|
12
|
+
export function loadSession(cwd = process.cwd()) {
|
|
13
|
+
const file = sessionFile(cwd);
|
|
14
|
+
if (!fs.existsSync(file))
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
18
|
+
return data.messages || null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function saveSession(messages, cwd = process.cwd()) {
|
|
25
|
+
const file = sessionFile(cwd);
|
|
26
|
+
const data = {
|
|
27
|
+
project: cwd,
|
|
28
|
+
updatedAt: Date.now(),
|
|
29
|
+
messages,
|
|
30
|
+
};
|
|
31
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
32
|
+
}
|
|
33
|
+
export function clearSession(cwd = process.cwd()) {
|
|
34
|
+
const file = sessionFile(cwd);
|
|
35
|
+
if (fs.existsSync(file))
|
|
36
|
+
fs.unlinkSync(file);
|
|
37
|
+
}
|
|
38
|
+
export function listSessions() {
|
|
39
|
+
if (!fs.existsSync(SESSIONS_DIR))
|
|
40
|
+
return [];
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const f of fs.readdirSync(SESSIONS_DIR)) {
|
|
43
|
+
if (!f.endsWith('.json'))
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8'));
|
|
47
|
+
out.push({
|
|
48
|
+
file: f,
|
|
49
|
+
project: data.project,
|
|
50
|
+
updatedAt: data.updatedAt,
|
|
51
|
+
count: data.messages.length,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
return out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
57
|
+
}
|
package/dist/core/tools.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
3
|
import { exec } from 'child_process';
|
|
3
4
|
import { promisify } from 'util';
|
|
4
5
|
import { hasN8n } from './config.js';
|
|
6
|
+
import { permissions } from './permission.js';
|
|
7
|
+
import { lineDiff, compactDiff } from './diff.js';
|
|
8
|
+
import { isBlocked } from './sandbox.js';
|
|
5
9
|
import { n8nListWorkflows, n8nTriggerWorkflow, n8nListExecutions, n8nGetWorkflow, n8nActivateWorkflow, n8nDeactivateWorkflow, } from './n8n.js';
|
|
6
10
|
const execAsync = promisify(exec);
|
|
7
|
-
// Base tools — always available
|
|
8
11
|
const BASE_TOOLS = [
|
|
9
12
|
{
|
|
10
13
|
type: 'function',
|
|
11
14
|
function: {
|
|
12
15
|
name: 'read_file',
|
|
13
|
-
description: 'Read a file from disk',
|
|
16
|
+
description: 'Read a file from disk. Sensitive files (.env, .ssh, keys) are blocked.',
|
|
14
17
|
parameters: {
|
|
15
18
|
type: 'object',
|
|
16
19
|
properties: { path: { type: 'string' } },
|
|
@@ -57,105 +60,102 @@ const BASE_TOOLS = [
|
|
|
57
60
|
},
|
|
58
61
|
},
|
|
59
62
|
];
|
|
60
|
-
// n8n tools — only loaded when N8N_URL + N8N_API_KEY are set
|
|
61
63
|
const N8N_TOOLS = [
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
type: 'function',
|
|
72
|
-
function: {
|
|
73
|
-
name: 'n8n_get_workflow',
|
|
74
|
-
description: 'Get details of a specific n8n workflow by ID',
|
|
75
|
-
parameters: {
|
|
76
|
-
type: 'object',
|
|
77
|
-
properties: { id: { type: 'string' } },
|
|
78
|
-
required: ['id'],
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
type: 'function',
|
|
84
|
-
function: {
|
|
85
|
-
name: 'n8n_trigger_workflow',
|
|
86
|
-
description: 'Trigger an n8n workflow via its webhook URL',
|
|
87
|
-
parameters: {
|
|
88
|
-
type: 'object',
|
|
89
|
-
properties: {
|
|
90
|
-
workflowId: { type: 'string' },
|
|
91
|
-
data: { type: 'object' },
|
|
92
|
-
},
|
|
93
|
-
required: ['workflowId'],
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
type: 'function',
|
|
99
|
-
function: {
|
|
100
|
-
name: 'n8n_list_executions',
|
|
101
|
-
description: 'List recent n8n executions (runs)',
|
|
102
|
-
parameters: {
|
|
103
|
-
type: 'object',
|
|
104
|
-
properties: {
|
|
105
|
-
workflowId: { type: 'string' },
|
|
106
|
-
limit: { type: 'number' },
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
type: 'function',
|
|
113
|
-
function: {
|
|
114
|
-
name: 'n8n_activate_workflow',
|
|
115
|
-
description: 'Activate an n8n workflow',
|
|
116
|
-
parameters: {
|
|
117
|
-
type: 'object',
|
|
118
|
-
properties: { id: { type: 'string' } },
|
|
119
|
-
required: ['id'],
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
type: 'function',
|
|
125
|
-
function: {
|
|
126
|
-
name: 'n8n_deactivate_workflow',
|
|
127
|
-
description: 'Deactivate an n8n workflow',
|
|
128
|
-
parameters: {
|
|
129
|
-
type: 'object',
|
|
130
|
-
properties: { id: { type: 'string' } },
|
|
131
|
-
required: ['id'],
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
},
|
|
64
|
+
{ type: 'function', function: { name: 'n8n_list_workflows', description: 'List n8n workflows', parameters: { type: 'object', properties: {} } } },
|
|
65
|
+
{ type: 'function', function: { name: 'n8n_get_workflow', description: 'Get n8n workflow by ID', parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } },
|
|
66
|
+
{ type: 'function', function: { name: 'n8n_trigger_workflow', description: 'Trigger n8n workflow', parameters: { type: 'object', properties: { workflowId: { type: 'string' }, data: { type: 'object' } }, required: ['workflowId'] } } },
|
|
67
|
+
{ type: 'function', function: { name: 'n8n_list_executions', description: 'List n8n executions', parameters: { type: 'object', properties: { workflowId: { type: 'string' }, limit: { type: 'number' } } } } },
|
|
68
|
+
{ type: 'function', function: { name: 'n8n_activate_workflow', description: 'Activate n8n workflow', parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } },
|
|
69
|
+
{ type: 'function', function: { name: 'n8n_deactivate_workflow', description: 'Deactivate n8n workflow', parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } },
|
|
135
70
|
];
|
|
136
|
-
// Dynamic — includes n8n tools only if configured
|
|
137
71
|
export function getToolDefs() {
|
|
138
72
|
return hasN8n() ? [...BASE_TOOLS, ...N8N_TOOLS] : [...BASE_TOOLS];
|
|
139
73
|
}
|
|
140
|
-
// For backwards compat if anything imports toolDefs directly
|
|
141
74
|
export const toolDefs = BASE_TOOLS;
|
|
75
|
+
const DANGER_PATTERNS = [
|
|
76
|
+
{ re: /\brm\s+(-[a-z]*[rf][a-z]*\s+)?/, level: 'high' },
|
|
77
|
+
{ re: /\bsudo\b/, level: 'high' },
|
|
78
|
+
{ re: /\bmkfs\b|\bdd\s+if=/, level: 'high' },
|
|
79
|
+
{ re: />\s*\/dev\//, level: 'high' },
|
|
80
|
+
{ re: /:\(\)\s*\{/, level: 'high' },
|
|
81
|
+
{ re: /\bchmod\s+(-R\s+)?777\b/, level: 'medium' },
|
|
82
|
+
{ re: /\bgit\s+push\b/, level: 'medium' },
|
|
83
|
+
{ re: /\bnpm\s+(publish|unpublish)\b/, level: 'medium' },
|
|
84
|
+
{ re: /\bcurl\b.*\|\s*(sh|bash)\b/, level: 'high' },
|
|
85
|
+
];
|
|
86
|
+
function classify(cmd) {
|
|
87
|
+
for (const { re, level } of DANGER_PATTERNS) {
|
|
88
|
+
if (re.test(cmd))
|
|
89
|
+
return level;
|
|
90
|
+
}
|
|
91
|
+
return 'low';
|
|
92
|
+
}
|
|
142
93
|
export async function executeTool(name, args) {
|
|
143
94
|
try {
|
|
144
95
|
switch (name) {
|
|
145
|
-
case 'read_file':
|
|
96
|
+
case 'read_file': {
|
|
97
|
+
const blocked = isBlocked(args.path);
|
|
98
|
+
if (blocked)
|
|
99
|
+
return `Blocked by sandbox: ${blocked}`;
|
|
146
100
|
return await fs.readFile(args.path, 'utf8');
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
101
|
+
}
|
|
102
|
+
case 'write_file': {
|
|
103
|
+
const path = args.path;
|
|
104
|
+
const content = args.content;
|
|
105
|
+
const blockedWrite = isBlocked(path);
|
|
106
|
+
if (blockedWrite)
|
|
107
|
+
return `Blocked by sandbox: ${blockedWrite}`;
|
|
108
|
+
const existed = existsSync(path);
|
|
109
|
+
let oldText = '';
|
|
110
|
+
if (existed) {
|
|
111
|
+
try {
|
|
112
|
+
oldText = readFileSync(path, 'utf8');
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
}
|
|
116
|
+
if (!permissions.isAlwaysAllowed('write_file')) {
|
|
117
|
+
const lines = content.split('\n');
|
|
118
|
+
const body = [
|
|
119
|
+
existed ? `Overwrite ${path}` : `Create ${path}`,
|
|
120
|
+
`${lines.length} lines · ${content.length} bytes`,
|
|
121
|
+
];
|
|
122
|
+
let diffPreview;
|
|
123
|
+
if (existed && oldText !== content) {
|
|
124
|
+
const d = compactDiff(lineDiff(oldText, content), 2);
|
|
125
|
+
diffPreview = d.slice(0, 60);
|
|
126
|
+
}
|
|
127
|
+
const answer = await permissions.ask({
|
|
128
|
+
tool: 'write_file',
|
|
129
|
+
title: existed ? 'Write file' : 'Create file',
|
|
130
|
+
body,
|
|
131
|
+
diff: diffPreview,
|
|
132
|
+
danger: existed ? 'medium' : 'low',
|
|
133
|
+
});
|
|
134
|
+
if (answer === 'no')
|
|
135
|
+
return `Denied by user: write_file ${path}`;
|
|
136
|
+
}
|
|
137
|
+
await fs.writeFile(path, content, 'utf8');
|
|
138
|
+
return 'Wrote ' + path;
|
|
139
|
+
}
|
|
150
140
|
case 'list_files': {
|
|
151
141
|
const dir = args.path || '.';
|
|
152
142
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
153
|
-
return entries
|
|
154
|
-
.map((e) => (e.isDirectory() ? e.name + '/' : e.name))
|
|
155
|
-
.join('\n');
|
|
143
|
+
return entries.map((e) => (e.isDirectory() ? e.name + '/' : e.name)).join('\n');
|
|
156
144
|
}
|
|
157
145
|
case 'run_command': {
|
|
158
|
-
const
|
|
146
|
+
const cmd = args.command;
|
|
147
|
+
if (!permissions.isAlwaysAllowed('run_command')) {
|
|
148
|
+
const danger = classify(cmd);
|
|
149
|
+
const answer = await permissions.ask({
|
|
150
|
+
tool: 'run_command',
|
|
151
|
+
title: 'Run command',
|
|
152
|
+
body: ['$ ' + cmd, `cwd: ${process.cwd()}`, `risk: ${danger}`],
|
|
153
|
+
danger,
|
|
154
|
+
});
|
|
155
|
+
if (answer === 'no')
|
|
156
|
+
return `Denied by user: run_command`;
|
|
157
|
+
}
|
|
158
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
159
159
|
cwd: process.cwd(),
|
|
160
160
|
maxBuffer: 10 * 1024 * 1024,
|
|
161
161
|
});
|
package/dist/index.js
CHANGED
|
@@ -5,21 +5,27 @@ import { render, Box, Static } from 'ink';
|
|
|
5
5
|
import { Setup } from './ui/Setup.js';
|
|
6
6
|
import { Chat } from './ui/Chat.js';
|
|
7
7
|
import { Banner } from './ui/Banner.js';
|
|
8
|
+
import { Consent, hasConsented } from './ui/Consent.js';
|
|
8
9
|
import { loadConfig, applyConfig } from './core/config.js';
|
|
10
|
+
import { loadSession } from './core/session.js';
|
|
9
11
|
const App = () => {
|
|
10
12
|
const [ready, setReady] = useState(false);
|
|
13
|
+
const [resumedCount, setResumedCount] = useState(0);
|
|
14
|
+
const [consented, setConsented] = useState(hasConsented());
|
|
11
15
|
useEffect(() => {
|
|
16
|
+
if (!consented)
|
|
17
|
+
return;
|
|
12
18
|
const existing = loadConfig();
|
|
13
19
|
if (existing) {
|
|
14
20
|
applyConfig(existing);
|
|
21
|
+
const hist = loadSession();
|
|
22
|
+
setResumedCount(hist ? hist.length : 0);
|
|
15
23
|
setReady(true);
|
|
16
24
|
}
|
|
17
|
-
}, []);
|
|
18
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Static, { items: ['banner'], children: () => _jsx(Banner, {}) }), ready ? _jsx(Chat, {}) : _jsx(Setup, { onDone: () => setReady(true) })] }));
|
|
25
|
+
}, [consented]);
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Static, { items: ['banner'], children: () => _jsx(Banner, {}) }), !consented ? (_jsx(Consent, { onAccept: () => setConsented(true) })) : ready ? (_jsx(Chat, { resumedCount: resumedCount })) : (_jsx(Setup, { onDone: () => setReady(true) }))] }));
|
|
19
27
|
};
|
|
20
28
|
render(_jsx(App, {}), {
|
|
21
|
-
// Prevent console.log interference from npm/openai warnings
|
|
22
29
|
patchConsole: false,
|
|
23
|
-
// Only redraw when Ink state actually changes
|
|
24
30
|
exitOnCtrlC: true,
|
|
25
31
|
});
|
package/dist/ui/Chat.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useStdout, Static } from 'ink';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
4
7
|
import { Agent } from '../core/agent.js';
|
|
8
|
+
import { permissions } from '../core/permission.js';
|
|
5
9
|
import { InputBar } from './InputBar.js';
|
|
6
10
|
import { ModelPicker } from './ModelPicker.js';
|
|
7
11
|
import { Settings } from './Settings.js';
|
|
12
|
+
import { PermissionPrompt } from './PermissionPrompt.js';
|
|
8
13
|
import { theme } from './theme.js';
|
|
9
14
|
let nextId = 0;
|
|
10
|
-
const mkItem = (kind, text) => ({
|
|
11
|
-
kind,
|
|
12
|
-
text,
|
|
13
|
-
id: nextId++,
|
|
14
|
-
});
|
|
15
|
+
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
15
16
|
const HistoryLine = ({ item }) => {
|
|
16
17
|
if (item.kind === 'user') {
|
|
17
18
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(Text, { color: theme.text, bold: true, children: item.text })] }));
|
|
@@ -24,16 +25,50 @@ const HistoryLine = ({ item }) => {
|
|
|
24
25
|
}
|
|
25
26
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: item.text }) }));
|
|
26
27
|
};
|
|
27
|
-
|
|
28
|
+
const DEBUG_FILE = path.join(os.homedir(), '.blyue', 'last-request.json');
|
|
29
|
+
function readDebug() {
|
|
30
|
+
if (!fs.existsSync(DEBUG_FILE)) {
|
|
31
|
+
return ['No request sent yet in this session.'];
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const d = JSON.parse(fs.readFileSync(DEBUG_FILE, 'utf8'));
|
|
35
|
+
const kb = (d.approxBytes / 1024).toFixed(1);
|
|
36
|
+
return [
|
|
37
|
+
`Model: ${d.model}`,
|
|
38
|
+
`Provider: ${d.baseUrl}`,
|
|
39
|
+
`Messages: ${d.messageCount}`,
|
|
40
|
+
`Approx size: ~${d.approxTokens} tokens · ${kb} KB`,
|
|
41
|
+
`Redactions: ${d.redactedCount || 0} secrets scrubbed`,
|
|
42
|
+
`Timestamp: ${d.timestamp}`,
|
|
43
|
+
`Full payload: ${DEBUG_FILE}`,
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return ['Debug file unreadable: ' + e.message];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export const Chat = ({ resumedCount = 0 }) => {
|
|
28
51
|
const { stdout } = useStdout();
|
|
29
52
|
const width = stdout?.columns ?? 80;
|
|
30
|
-
const [history, setHistory] = useState(
|
|
53
|
+
const [history, setHistory] = useState(() => {
|
|
54
|
+
if (resumedCount > 0) {
|
|
55
|
+
return [mkItem('tool', `(resumed session · ${resumedCount} prior messages)`)];
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
});
|
|
31
59
|
const [input, setInput] = useState('');
|
|
32
60
|
const [loading, setLoading] = useState(false);
|
|
33
61
|
const [overlay, setOverlay] = useState('none');
|
|
62
|
+
const [permReq, setPermReq] = useState(null);
|
|
34
63
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
35
64
|
const [agent, setAgent] = useState(() => new Agent());
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const unsub = permissions.subscribe((req) => setPermReq(req));
|
|
67
|
+
return unsub;
|
|
68
|
+
}, []);
|
|
36
69
|
useInput((inputChar, key) => {
|
|
70
|
+
if (permReq)
|
|
71
|
+
return;
|
|
37
72
|
if (key.ctrl && inputChar === 'p')
|
|
38
73
|
setOverlay('model');
|
|
39
74
|
if (key.ctrl && inputChar === 's')
|
|
@@ -53,13 +88,32 @@ export const Chat = () => {
|
|
|
53
88
|
setOverlay('settings');
|
|
54
89
|
return;
|
|
55
90
|
}
|
|
91
|
+
if (trimmed === '/debug') {
|
|
92
|
+
setInput('');
|
|
93
|
+
const lines = readDebug();
|
|
94
|
+
setHistory((h) => [
|
|
95
|
+
...h,
|
|
96
|
+
mkItem('tool', 'Last request to provider:'),
|
|
97
|
+
...lines.map((l) => mkItem('tool', ' ' + l)),
|
|
98
|
+
]);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (trimmed === '/clear') {
|
|
102
|
+
setInput('');
|
|
103
|
+
agent.clear();
|
|
104
|
+
permissions.resetSession();
|
|
105
|
+
setHistory([mkItem('tool', '(conversation cleared)')]);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
56
108
|
if (trimmed === '/help') {
|
|
57
109
|
setInput('');
|
|
58
110
|
setHistory((h) => [
|
|
59
111
|
...h,
|
|
60
112
|
mkItem('tool', 'Commands:'),
|
|
61
|
-
mkItem('tool', ' /model switch model
|
|
62
|
-
mkItem('tool', ' /settings open settings
|
|
113
|
+
mkItem('tool', ' /model switch model (Ctrl+P)'),
|
|
114
|
+
mkItem('tool', ' /settings open settings (Ctrl+S)'),
|
|
115
|
+
mkItem('tool', ' /debug show last request to provider'),
|
|
116
|
+
mkItem('tool', ' /clear clear session and permissions'),
|
|
63
117
|
mkItem('tool', ' /help show this list'),
|
|
64
118
|
]);
|
|
65
119
|
return;
|
|
@@ -85,6 +139,9 @@ export const Chat = () => {
|
|
|
85
139
|
setAgent(new Agent());
|
|
86
140
|
setHistory((h) => [...h, mkItem('tool', `model → ${id}`)]);
|
|
87
141
|
};
|
|
142
|
+
if (permReq) {
|
|
143
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), _jsx(Box, { marginTop: 1, children: _jsx(PermissionPrompt, { req: permReq }) })] }));
|
|
144
|
+
}
|
|
88
145
|
if (overlay === 'model') {
|
|
89
146
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(ModelPicker, { current: currentModel, onSelect: handleModelSelect, onCancel: () => setOverlay('none') }) }));
|
|
90
147
|
}
|
|
@@ -98,5 +155,5 @@ export const Chat = () => {
|
|
|
98
155
|
const shortModel = currentModel.length > maxModelLen
|
|
99
156
|
? '…' + currentModel.slice(-(maxModelLen - 1))
|
|
100
157
|
: currentModel || 'unset';
|
|
101
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading }), _jsxs(Text, { children: [_jsx(Text, { color: theme.accent, children: " /model" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.accent, children: "/settings" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.text, children: shortModel })] })] })] }));
|
|
158
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading }), _jsxs(Text, { children: [_jsx(Text, { color: theme.accent, children: " /model" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.accent, children: "/settings" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.accent, children: "/debug" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.text, children: shortModel })] })] })] }));
|
|
102
159
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { theme } from './theme.js';
|
|
7
|
+
const FLAG = path.join(os.homedir(), '.blyue', '.consent');
|
|
8
|
+
export function hasConsented() {
|
|
9
|
+
return fs.existsSync(FLAG);
|
|
10
|
+
}
|
|
11
|
+
export function markConsented() {
|
|
12
|
+
fs.mkdirSync(path.dirname(FLAG), { recursive: true });
|
|
13
|
+
fs.writeFileSync(FLAG, new Date().toISOString());
|
|
14
|
+
}
|
|
15
|
+
export const Consent = ({ onAccept }) => {
|
|
16
|
+
useInput((input) => {
|
|
17
|
+
if (input === 'y' || input === 'Y') {
|
|
18
|
+
markConsented();
|
|
19
|
+
onAccept();
|
|
20
|
+
}
|
|
21
|
+
else if (input === 'n' || input === 'N') {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: theme.warning, bold: true, children: "\u26A0 Privacy notice" }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.text, children: "BLYUE sends your prompts, file contents, and command output to your configured LLM provider so it can help you." }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.muted, children: "Do NOT run this on directories containing secrets, keys, or private data unless you trust the provider." }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.muted, children: "Built-in protections:" }), _jsx(Text, { color: theme.muted, children: ' · .env, .ssh, .aws, *.pem, *.key are blocked from reading' }), _jsx(Text, { color: theme.muted, children: ' · API keys and tokens are redacted before sending' }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.text, children: " I understand, continue" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.error, bold: true, children: " [n]" }), _jsx(Text, { color: theme.text, children: " Exit" })] })] }));
|
|
26
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
3
|
+
import { permissions } from '../core/permission.js';
|
|
4
|
+
import { theme } from './theme.js';
|
|
5
|
+
export const PermissionPrompt = ({ req }) => {
|
|
6
|
+
const { stdout } = useStdout();
|
|
7
|
+
const width = Math.max(40, stdout?.columns ?? 60);
|
|
8
|
+
const inner = width - 4;
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
if (input === 'y' || input === 'Y')
|
|
11
|
+
permissions.answer('yes');
|
|
12
|
+
else if (input === 'n' || input === 'N' || key.escape)
|
|
13
|
+
permissions.answer('no');
|
|
14
|
+
else if (input === 'a' || input === 'A')
|
|
15
|
+
permissions.answer('always');
|
|
16
|
+
});
|
|
17
|
+
const dangerColor = req.danger === 'high' ? theme.error :
|
|
18
|
+
req.danger === 'medium' ? theme.warning :
|
|
19
|
+
theme.accent;
|
|
20
|
+
const title = `${req.danger === 'high' ? '⚠ ' : ''}${req.title}`;
|
|
21
|
+
const topPad = Math.max(1, inner - title.length - 3);
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: dangerColor, bold: true, children: title }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(topPad) + '╮' })] }), req.body.map((line, i) => {
|
|
23
|
+
const pad = Math.max(0, inner - line.length - 2);
|
|
24
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.text, children: line }), _jsx(Text, { children: ' '.repeat(pad) })] }, i));
|
|
25
|
+
}), req.diff && req.diff.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: '│' }), req.diff.map((d, i) => {
|
|
26
|
+
const sign = d.type === 'add' ? '+' : d.type === 'remove' ? '-' : ' ';
|
|
27
|
+
const color = d.type === 'add' ? theme.success :
|
|
28
|
+
d.type === 'remove' ? theme.error :
|
|
29
|
+
theme.muted;
|
|
30
|
+
const text = `${sign} ${d.text}`.slice(0, inner - 2);
|
|
31
|
+
const pad = Math.max(0, inner - text.length - 2);
|
|
32
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: color, children: text }), _jsx(Text, { children: ' '.repeat(pad) })] }, i));
|
|
33
|
+
})] })), _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }), _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.muted, children: " allow once " }), _jsx(Text, { color: theme.accent, bold: true, children: "[a]" }), _jsx(Text, { color: theme.muted, children: " always (this session) " }), _jsx(Text, { color: theme.error, bold: true, children: "[n]" }), _jsx(Text, { color: theme.muted, children: " deny" })] })] }));
|
|
34
|
+
};
|
package/dist/ui/theme.js
CHANGED