blyuecode 0.1.9 → 0.1.10

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.
@@ -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
- messages = [
11
- {
12
- role: 'system',
13
- content: 'You are BLYUE, a coding assistant running in a terminal. ' +
14
- 'Use tools to read, write, and run commands. ' +
15
- 'If n8n tools are available, you can also manage n8n workflows. ' +
16
- 'Always explain what you are doing. Be concise.',
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
- if (res.toolCalls.length === 0)
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();
@@ -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
+ }
@@ -1,10 +1,12 @@
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';
5
8
  import { n8nListWorkflows, n8nTriggerWorkflow, n8nListExecutions, n8nGetWorkflow, n8nActivateWorkflow, n8nDeactivateWorkflow, } from './n8n.js';
6
9
  const execAsync = promisify(exec);
7
- // Base tools — always available
8
10
  const BASE_TOOLS = [
9
11
  {
10
12
  type: 'function',
@@ -57,105 +59,96 @@ const BASE_TOOLS = [
57
59
  },
58
60
  },
59
61
  ];
60
- // n8n tools — only loaded when N8N_URL + N8N_API_KEY are set
61
62
  const N8N_TOOLS = [
62
- {
63
- type: 'function',
64
- function: {
65
- name: 'n8n_list_workflows',
66
- description: 'List all n8n workflows. Returns id, name, active status, updatedAt.',
67
- parameters: { type: 'object', properties: {} },
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
- },
63
+ { type: 'function', function: { name: 'n8n_list_workflows', description: 'List n8n workflows', parameters: { type: 'object', properties: {} } } },
64
+ { type: 'function', function: { name: 'n8n_get_workflow', description: 'Get n8n workflow by ID', parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } },
65
+ { type: 'function', function: { name: 'n8n_trigger_workflow', description: 'Trigger n8n workflow', parameters: { type: 'object', properties: { workflowId: { type: 'string' }, data: { type: 'object' } }, required: ['workflowId'] } } },
66
+ { type: 'function', function: { name: 'n8n_list_executions', description: 'List n8n executions', parameters: { type: 'object', properties: { workflowId: { type: 'string' }, limit: { type: 'number' } } } } },
67
+ { type: 'function', function: { name: 'n8n_activate_workflow', description: 'Activate n8n workflow', parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } },
68
+ { type: 'function', function: { name: 'n8n_deactivate_workflow', description: 'Deactivate n8n workflow', parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } },
135
69
  ];
136
- // Dynamic — includes n8n tools only if configured
137
70
  export function getToolDefs() {
138
71
  return hasN8n() ? [...BASE_TOOLS, ...N8N_TOOLS] : [...BASE_TOOLS];
139
72
  }
140
- // For backwards compat if anything imports toolDefs directly
141
73
  export const toolDefs = BASE_TOOLS;
74
+ // Dangerous command patterns
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' }, // fork bomb
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
96
  case 'read_file':
146
97
  return await fs.readFile(args.path, 'utf8');
147
- case 'write_file':
148
- await fs.writeFile(args.path, args.content, 'utf8');
149
- return 'Wrote ' + args.path;
98
+ case 'write_file': {
99
+ const path = args.path;
100
+ const content = args.content;
101
+ const existed = existsSync(path);
102
+ let oldText = '';
103
+ if (existed) {
104
+ try {
105
+ oldText = readFileSync(path, 'utf8');
106
+ }
107
+ catch { }
108
+ }
109
+ if (!permissions.isAlwaysAllowed('write_file')) {
110
+ const lines = content.split('\n');
111
+ const body = [
112
+ existed ? `Overwrite ${path}` : `Create ${path}`,
113
+ `${lines.length} lines · ${content.length} bytes`,
114
+ ];
115
+ let diffPreview;
116
+ if (existed && oldText !== content) {
117
+ const d = compactDiff(lineDiff(oldText, content), 2);
118
+ diffPreview = d.slice(0, 60);
119
+ }
120
+ const answer = await permissions.ask({
121
+ tool: 'write_file',
122
+ title: existed ? 'Write file' : 'Create file',
123
+ body,
124
+ diff: diffPreview,
125
+ danger: existed ? 'medium' : 'low',
126
+ });
127
+ if (answer === 'no')
128
+ return `Denied by user: write_file ${path}`;
129
+ }
130
+ await fs.writeFile(path, content, 'utf8');
131
+ return 'Wrote ' + path;
132
+ }
150
133
  case 'list_files': {
151
134
  const dir = args.path || '.';
152
135
  const entries = await fs.readdir(dir, { withFileTypes: true });
153
- return entries
154
- .map((e) => (e.isDirectory() ? e.name + '/' : e.name))
155
- .join('\n');
136
+ return entries.map((e) => (e.isDirectory() ? e.name + '/' : e.name)).join('\n');
156
137
  }
157
138
  case 'run_command': {
158
- const { stdout, stderr } = await execAsync(args.command, {
139
+ const cmd = args.command;
140
+ if (!permissions.isAlwaysAllowed('run_command')) {
141
+ const danger = classify(cmd);
142
+ const answer = await permissions.ask({
143
+ tool: 'run_command',
144
+ title: 'Run command',
145
+ body: ['$ ' + cmd, `cwd: ${process.cwd()}`, `risk: ${danger}`],
146
+ danger,
147
+ });
148
+ if (answer === 'no')
149
+ return `Denied by user: run_command`;
150
+ }
151
+ const { stdout, stderr } = await execAsync(cmd, {
159
152
  cwd: process.cwd(),
160
153
  maxBuffer: 10 * 1024 * 1024,
161
154
  });
package/dist/index.js CHANGED
@@ -6,20 +6,22 @@ import { Setup } from './ui/Setup.js';
6
6
  import { Chat } from './ui/Chat.js';
7
7
  import { Banner } from './ui/Banner.js';
8
8
  import { loadConfig, applyConfig } from './core/config.js';
9
+ import { loadSession } from './core/session.js';
9
10
  const App = () => {
10
11
  const [ready, setReady] = useState(false);
12
+ const [resumedCount, setResumedCount] = useState(0);
11
13
  useEffect(() => {
12
14
  const existing = loadConfig();
13
15
  if (existing) {
14
16
  applyConfig(existing);
17
+ const hist = loadSession();
18
+ setResumedCount(hist ? hist.length : 0);
15
19
  setReady(true);
16
20
  }
17
21
  }, []);
18
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Static, { items: ['banner'], children: () => _jsx(Banner, {}) }), ready ? _jsx(Chat, {}) : _jsx(Setup, { onDone: () => setReady(true) })] }));
22
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Static, { items: ['banner'], children: () => _jsx(Banner, {}) }), ready ? (_jsx(Chat, { resumedCount: resumedCount })) : (_jsx(Setup, { onDone: () => setReady(true) }))] }));
19
23
  };
20
24
  render(_jsx(App, {}), {
21
- // Prevent console.log interference from npm/openai warnings
22
25
  patchConsole: false,
23
- // Only redraw when Ink state actually changes
24
26
  exitOnCtrlC: true,
25
27
  });
package/dist/ui/Chat.js CHANGED
@@ -1,17 +1,15 @@
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
4
  import { Agent } from '../core/agent.js';
5
+ import { permissions } from '../core/permission.js';
5
6
  import { InputBar } from './InputBar.js';
6
7
  import { ModelPicker } from './ModelPicker.js';
7
8
  import { Settings } from './Settings.js';
9
+ import { PermissionPrompt } from './PermissionPrompt.js';
8
10
  import { theme } from './theme.js';
9
11
  let nextId = 0;
10
- const mkItem = (kind, text) => ({
11
- kind,
12
- text,
13
- id: nextId++,
14
- });
12
+ const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
15
13
  const HistoryLine = ({ item }) => {
16
14
  if (item.kind === 'user') {
17
15
  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 +22,29 @@ const HistoryLine = ({ item }) => {
24
22
  }
25
23
  return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: item.text }) }));
26
24
  };
27
- export const Chat = () => {
25
+ export const Chat = ({ resumedCount = 0 }) => {
28
26
  const { stdout } = useStdout();
29
27
  const width = stdout?.columns ?? 80;
30
- const [history, setHistory] = useState([]);
28
+ const [history, setHistory] = useState(() => {
29
+ if (resumedCount > 0) {
30
+ return [mkItem('tool', `(resumed session · ${resumedCount} prior messages)`)];
31
+ }
32
+ return [];
33
+ });
31
34
  const [input, setInput] = useState('');
32
35
  const [loading, setLoading] = useState(false);
33
36
  const [overlay, setOverlay] = useState('none');
37
+ const [permReq, setPermReq] = useState(null);
34
38
  const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
35
39
  const [agent, setAgent] = useState(() => new Agent());
40
+ // Subscribe to permission requests
41
+ useEffect(() => {
42
+ const unsub = permissions.subscribe((req) => setPermReq(req));
43
+ return unsub;
44
+ }, []);
36
45
  useInput((inputChar, key) => {
46
+ if (permReq)
47
+ return; // permission prompt handles its own input
37
48
  if (key.ctrl && inputChar === 'p')
38
49
  setOverlay('model');
39
50
  if (key.ctrl && inputChar === 's')
@@ -53,13 +64,21 @@ export const Chat = () => {
53
64
  setOverlay('settings');
54
65
  return;
55
66
  }
67
+ if (trimmed === '/clear') {
68
+ setInput('');
69
+ agent.clear();
70
+ permissions.resetSession();
71
+ setHistory([mkItem('tool', '(conversation cleared)')]);
72
+ return;
73
+ }
56
74
  if (trimmed === '/help') {
57
75
  setInput('');
58
76
  setHistory((h) => [
59
77
  ...h,
60
78
  mkItem('tool', 'Commands:'),
61
- mkItem('tool', ' /model switch model (Ctrl+P)'),
62
- mkItem('tool', ' /settings open settings (Ctrl+S)'),
79
+ mkItem('tool', ' /model switch model (Ctrl+P)'),
80
+ mkItem('tool', ' /settings open settings (Ctrl+S)'),
81
+ mkItem('tool', ' /clear clear session and permissions'),
63
82
  mkItem('tool', ' /help show this list'),
64
83
  ]);
65
84
  return;
@@ -85,6 +104,10 @@ export const Chat = () => {
85
104
  setAgent(new Agent());
86
105
  setHistory((h) => [...h, mkItem('tool', `model → ${id}`)]);
87
106
  };
107
+ // Permission prompt takes priority over everything
108
+ if (permReq) {
109
+ 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 }) })] }));
110
+ }
88
111
  if (overlay === 'model') {
89
112
  return (_jsx(Box, { flexDirection: "column", children: _jsx(ModelPicker, { current: currentModel, onSelect: handleModelSelect, onCancel: () => setOverlay('none') }) }));
90
113
  }
@@ -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
@@ -1,8 +1,9 @@
1
1
  export const theme = {
2
- accent: 'magentaBright', // the only non-neutral color
2
+ accent: 'cyan',
3
3
  text: 'white',
4
4
  muted: 'gray',
5
5
  dim: 'gray',
6
6
  error: 'red',
7
+ warning: 'yellow',
7
8
  success: 'green',
8
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blyuecode",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "BLYUE Code — a coding agent by Lumen",
5
5
  "type": "module",
6
6
  "license": "MIT",