blyuecode 0.1.6 → 0.1.8

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.
@@ -11,8 +11,8 @@ export class Agent {
11
11
  {
12
12
  role: 'system',
13
13
  content: 'You are BLYUE, a coding assistant running in a terminal. ' +
14
- 'Use tools to read, write, and run commands. Always explain what you are doing. ' +
15
- 'Ask before destructive commands. Be concise.',
14
+ 'Use tools to read, write, run commands, and manage n8n workflows. ' +
15
+ 'Always explain what you are doing. Be concise.',
16
16
  },
17
17
  ];
18
18
  async *run(userInput) {
@@ -0,0 +1,85 @@
1
+ const BASE = process.env.N8N_URL || '';
2
+ const KEY = process.env.N8N_API_KEY || '';
3
+ function headers() {
4
+ return {
5
+ 'X-N8N-API-KEY': KEY,
6
+ 'Content-Type': 'application/json',
7
+ Accept: 'application/json',
8
+ };
9
+ }
10
+ function ensureConfig() {
11
+ if (!BASE)
12
+ throw new Error('N8N_URL not set. Add to ~/.blyue/.env');
13
+ if (!KEY)
14
+ throw new Error('N8N_API_KEY not set. Add to ~/.blyue/.env');
15
+ }
16
+ export async function n8nListWorkflows() {
17
+ ensureConfig();
18
+ const res = await fetch(`${BASE}/api/v1/workflows`, { headers: headers() });
19
+ if (!res.ok)
20
+ throw new Error(`n8n API ${res.status}: ${await res.text()}`);
21
+ const json = await res.json();
22
+ return (json.data || []).map((w) => ({
23
+ id: w.id,
24
+ name: w.name,
25
+ active: w.active,
26
+ updatedAt: w.updatedAt,
27
+ }));
28
+ }
29
+ export async function n8nTriggerWorkflow(workflowId, data = {}) {
30
+ ensureConfig();
31
+ const res = await fetch(`${BASE}/webhook/${workflowId}`, {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify(data),
35
+ });
36
+ return await res.text();
37
+ }
38
+ export async function n8nListExecutions(workflowId, limit = 10) {
39
+ ensureConfig();
40
+ const url = new URL(`${BASE}/api/v1/executions`);
41
+ url.searchParams.set('limit', String(limit));
42
+ if (workflowId)
43
+ url.searchParams.set('workflowId', workflowId);
44
+ const res = await fetch(url.toString(), { headers: headers() });
45
+ if (!res.ok)
46
+ throw new Error(`n8n API ${res.status}: ${await res.text()}`);
47
+ const json = await res.json();
48
+ return (json.data || []).map((e) => ({
49
+ id: e.id,
50
+ status: e.finished
51
+ ? 'success'
52
+ : e.stoppedAt
53
+ ? 'failed'
54
+ : 'running',
55
+ startedAt: e.startedAt,
56
+ workflowId: e.workflowId,
57
+ }));
58
+ }
59
+ export async function n8nGetWorkflow(id) {
60
+ ensureConfig();
61
+ const res = await fetch(`${BASE}/api/v1/workflows/${id}`, { headers: headers() });
62
+ if (!res.ok)
63
+ throw new Error(`n8n API ${res.status}: ${await res.text()}`);
64
+ return await res.json();
65
+ }
66
+ export async function n8nActivateWorkflow(id) {
67
+ ensureConfig();
68
+ const res = await fetch(`${BASE}/api/v1/workflows/${id}/activate`, {
69
+ method: 'POST',
70
+ headers: headers(),
71
+ });
72
+ if (!res.ok)
73
+ throw new Error(`n8n API ${res.status}: ${await res.text()}`);
74
+ return 'activated';
75
+ }
76
+ export async function n8nDeactivateWorkflow(id) {
77
+ ensureConfig();
78
+ const res = await fetch(`${BASE}/api/v1/workflows/${id}/deactivate`, {
79
+ method: 'POST',
80
+ headers: headers(),
81
+ });
82
+ if (!res.ok)
83
+ throw new Error(`n8n API ${res.status}: ${await res.text()}`);
84
+ return 'deactivated';
85
+ }
@@ -1,8 +1,13 @@
1
1
  import fs from 'fs/promises';
2
2
  import { exec } from 'child_process';
3
3
  import { promisify } from 'util';
4
+ import { n8nListWorkflows, n8nTriggerWorkflow, n8nListExecutions, n8nGetWorkflow, n8nActivateWorkflow, n8nDeactivateWorkflow, } from './n8n.js';
4
5
  const execAsync = promisify(exec);
6
+ // ─────────────────────────────────────────────
7
+ // Tool definitions
8
+ // ─────────────────────────────────────────────
5
9
  export const toolDefs = [
10
+ // ───── File ops ─────
6
11
  {
7
12
  type: 'function',
8
13
  function: {
@@ -10,9 +15,7 @@ export const toolDefs = [
10
15
  description: 'Read a file from disk',
11
16
  parameters: {
12
17
  type: 'object',
13
- properties: {
14
- path: { type: 'string', description: 'File path' },
15
- },
18
+ properties: { path: { type: 'string' } },
16
19
  required: ['path'],
17
20
  },
18
21
  },
@@ -39,9 +42,7 @@ export const toolDefs = [
39
42
  description: 'List files in a directory',
40
43
  parameters: {
41
44
  type: 'object',
42
- properties: {
43
- path: { type: 'string', description: 'Directory path (default: .)' },
44
- },
45
+ properties: { path: { type: 'string' } },
45
46
  },
46
47
  },
47
48
  },
@@ -50,19 +51,98 @@ export const toolDefs = [
50
51
  function: {
51
52
  name: 'run_command',
52
53
  description: 'Run a shell command. Use carefully.',
54
+ parameters: {
55
+ type: 'object',
56
+ properties: { command: { type: 'string' } },
57
+ required: ['command'],
58
+ },
59
+ },
60
+ },
61
+ // ───── n8n ─────
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',
53
87
  parameters: {
54
88
  type: 'object',
55
89
  properties: {
56
- command: { type: 'string' },
90
+ workflowId: { type: 'string' },
91
+ data: {
92
+ type: 'object',
93
+ description: 'JSON payload to send to the webhook',
94
+ },
57
95
  },
58
- required: ['command'],
96
+ required: ['workflowId'],
97
+ },
98
+ },
99
+ },
100
+ {
101
+ type: 'function',
102
+ function: {
103
+ name: 'n8n_list_executions',
104
+ description: 'List recent n8n executions (runs)',
105
+ parameters: {
106
+ type: 'object',
107
+ properties: {
108
+ workflowId: { type: 'string', description: 'Filter by workflow ID' },
109
+ limit: { type: 'number', description: 'Max results (default 10)' },
110
+ },
111
+ },
112
+ },
113
+ },
114
+ {
115
+ type: 'function',
116
+ function: {
117
+ name: 'n8n_activate_workflow',
118
+ description: 'Activate an n8n workflow so it runs on its trigger',
119
+ parameters: {
120
+ type: 'object',
121
+ properties: { id: { type: 'string' } },
122
+ required: ['id'],
123
+ },
124
+ },
125
+ },
126
+ {
127
+ type: 'function',
128
+ function: {
129
+ name: 'n8n_deactivate_workflow',
130
+ description: 'Deactivate an n8n workflow',
131
+ parameters: {
132
+ type: 'object',
133
+ properties: { id: { type: 'string' } },
134
+ required: ['id'],
59
135
  },
60
136
  },
61
137
  },
62
138
  ];
139
+ // ─────────────────────────────────────────────
140
+ // Tool execution
141
+ // ─────────────────────────────────────────────
63
142
  export async function executeTool(name, args) {
64
143
  try {
65
144
  switch (name) {
145
+ // ───── File ops ─────
66
146
  case 'read_file':
67
147
  return await fs.readFile(args.path, 'utf8');
68
148
  case 'write_file':
@@ -82,6 +162,19 @@ export async function executeTool(name, args) {
82
162
  });
83
163
  return 'stdout:\n' + stdout + '\nstderr:\n' + stderr;
84
164
  }
165
+ // ───── n8n ─────
166
+ case 'n8n_list_workflows':
167
+ return JSON.stringify(await n8nListWorkflows(), null, 2);
168
+ case 'n8n_get_workflow':
169
+ return JSON.stringify(await n8nGetWorkflow(args.id), null, 2);
170
+ case 'n8n_trigger_workflow':
171
+ return await n8nTriggerWorkflow(args.workflowId, args.data || {});
172
+ case 'n8n_list_executions':
173
+ return JSON.stringify(await n8nListExecutions(args.workflowId, args.limit ?? 10), null, 2);
174
+ case 'n8n_activate_workflow':
175
+ return await n8nActivateWorkflow(args.id);
176
+ case 'n8n_deactivate_workflow':
177
+ return await n8nDeactivateWorkflow(args.id);
85
178
  default:
86
179
  return 'Unknown tool: ' + name;
87
180
  }
package/dist/index.js CHANGED
@@ -17,4 +17,9 @@ const App = () => {
17
17
  }, []);
18
18
  return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Static, { items: ['banner'], children: () => _jsx(Banner, {}) }), ready ? _jsx(Chat, {}) : _jsx(Setup, { onDone: () => setReady(true) })] }));
19
19
  };
20
- render(_jsx(App, {}));
20
+ render(_jsx(App, {}), {
21
+ // Prevent console.log interference from npm/openai warnings
22
+ patchConsole: false,
23
+ // Only redraw when Ink state actually changes
24
+ exitOnCtrlC: true,
25
+ });
package/dist/ui/Chat.js CHANGED
@@ -1,10 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
- import { Box, Text, useInput, useStdout } from 'ink';
3
+ import { Box, Text, useInput, useStdout, Static } from 'ink';
4
4
  import { Agent } from '../core/agent.js';
5
5
  import { InputBar } from './InputBar.js';
6
6
  import { ModelPicker } from './ModelPicker.js';
7
7
  import { theme } from './theme.js';
8
+ let nextId = 0;
9
+ const mkItem = (kind, text) => ({
10
+ kind,
11
+ text,
12
+ id: nextId++,
13
+ });
14
+ // ─────────────────────────────────────────────
15
+ // One history line — rendered ONCE via <Static>
16
+ // ─────────────────────────────────────────────
17
+ const HistoryLine = ({ item }) => {
18
+ if (item.kind === 'user') {
19
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(Text, { color: theme.text, bold: true, children: item.text })] }));
20
+ }
21
+ if (item.kind === 'error') {
22
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.error, children: '❯ ' }), _jsx(Text, { color: theme.error, children: item.text })] }));
23
+ }
24
+ if (item.kind === 'tool') {
25
+ return _jsx(Text, { color: theme.muted, children: item.text.trim() });
26
+ }
27
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: item.text }) }));
28
+ };
8
29
  export const Chat = () => {
9
30
  const { stdout } = useStdout();
10
31
  const width = stdout?.columns ?? 80;
@@ -15,9 +36,8 @@ export const Chat = () => {
15
36
  const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
16
37
  const [agent, setAgent] = useState(() => new Agent());
17
38
  useInput((inputChar, key) => {
18
- if (key.ctrl && inputChar === 'p') {
39
+ if (key.ctrl && inputChar === 'p')
19
40
  setModelPickerOpen(true);
20
- }
21
41
  });
22
42
  const handleSubmit = async (text) => {
23
43
  if (!text.trim() || loading)
@@ -29,16 +49,16 @@ export const Chat = () => {
29
49
  return;
30
50
  }
31
51
  setInput('');
32
- setHistory((h) => [...h, { kind: 'user', text: trimmed }]);
52
+ setHistory((h) => [...h, mkItem('user', trimmed)]);
33
53
  setLoading(true);
34
54
  try {
35
55
  for await (const chunk of agent.run(trimmed)) {
36
56
  const isTool = chunk.startsWith('\n[Calling') || chunk.startsWith('[Result]');
37
- setHistory((h) => [...h, { kind: isTool ? 'tool' : 'ai', text: chunk }]);
57
+ setHistory((h) => [...h, mkItem(isTool ? 'tool' : 'ai', chunk)]);
38
58
  }
39
59
  }
40
60
  catch (e) {
41
- setHistory((h) => [...h, { kind: 'error', text: e?.message || String(e) }]);
61
+ setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
42
62
  }
43
63
  setLoading(false);
44
64
  };
@@ -47,26 +67,14 @@ export const Chat = () => {
47
67
  setCurrentModel(id);
48
68
  setModelPickerOpen(false);
49
69
  setAgent(new Agent());
50
- setHistory((h) => [...h, { kind: 'tool', text: `model → ${id}` }]);
70
+ setHistory((h) => [...h, mkItem('tool', `model → ${id}`)]);
51
71
  };
52
72
  if (modelPickerOpen) {
53
73
  return (_jsx(Box, { flexDirection: "column", children: _jsx(ModelPicker, { current: currentModel, onSelect: handleModelSelect, onCancel: () => setModelPickerOpen(false) }) }));
54
74
  }
55
- // Aggressive truncation so footer fits on one line
56
75
  const maxModelLen = Math.max(10, width - 14);
57
76
  const shortModel = currentModel.length > maxModelLen
58
77
  ? '…' + currentModel.slice(-(maxModelLen - 1))
59
78
  : currentModel || 'unset';
60
- return (_jsxs(Box, { flexDirection: "column", children: [history.map((item, i) => {
61
- if (item.kind === 'user') {
62
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(Text, { color: theme.text, bold: true, children: item.text })] }, i));
63
- }
64
- if (item.kind === 'error') {
65
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.error, children: '❯ ' }), _jsx(Text, { color: theme.error, children: item.text })] }, i));
66
- }
67
- if (item.kind === 'tool') {
68
- return _jsx(Text, { color: theme.muted, children: item.text.trim() }, i);
69
- }
70
- return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: item.text }) }, i));
71
- }), _jsx(Box, { marginTop: 1, children: _jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading }) }), _jsxs(Text, { children: [_jsx(Text, { color: theme.accent, children: " /model" }), _jsxs(Text, { color: theme.muted, children: [" ", shortModel] })] })] }));
79
+ 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" }), _jsxs(Text, { color: theme.muted, children: [" ", shortModel] })] })] })] }));
72
80
  };
@@ -6,7 +6,8 @@ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '
6
6
  export const Spinner = ({ label = 'Thinking' }) => {
7
7
  const [i, setI] = useState(0);
8
8
  useEffect(() => {
9
- const t = setInterval(() => setI((v) => (v + 1) % FRAMES.length), 90);
9
+ // 200ms instead of 90ms → 5 fps not 11 fps
10
+ const t = setInterval(() => setI((v) => (v + 1) % FRAMES.length), 200);
10
11
  return () => clearInterval(t);
11
12
  }, []);
12
13
  return (_jsxs(Text, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsxs(Text, { color: theme.muted, children: [FRAMES[i], " ", label, "\u2026"] })] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blyuecode",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "BLYUE Code — a coding agent by Lumen",
5
5
  "type": "module",
6
6
  "license": "MIT",