@beunec/rivine 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -138,10 +138,10 @@ rivine mcp login <name> sign in to a remote MCP server
138
138
  rivine --output-format json one JSON object on stdout, for CI
139
139
  ```
140
140
 
141
- In a session: type `/` and press Tab. `/help` `/mode` `/plan` `/accept`
142
- `/status` `/diff` `/pr` `/mcp` `/credits` `/clear` `/exit`. `/mode` opens the
143
- permission picker. Shift+Tab cycles plan / default / accept edits. Ctrl+C
144
- quits cleanly.
141
+ In a session: type `/` and press Tab. `/help` `/agent` `/mode` `/plan` `/accept`
142
+ `/status` `/diff` `/pr` `/mcp` `/credits` `/clear` `/exit`. Startup asks for
143
+ **Mode** (which agent) then **Rate** (Manual or Bypass). `/agent` and `/mode`
144
+ reopen those pickers. Shift+Tab toggles Manual ⇄ Bypass. Ctrl+C quits cleanly.
145
145
 
146
146
  `/diff` shows what changed in the working tree. `/pr` prepares a pull request
147
147
  through your own `gh` CLI. It **never pushes without being asked**: on a
package/dist/index.js CHANGED
@@ -263,7 +263,7 @@ program
263
263
  .argument('[prompt...]', 'send one prompt and exit')
264
264
  .option('-p, --print <prompt>', 'send one prompt, print the answer, and exit')
265
265
  .option('-m, --mode <mode>', `which agent runs: ${CODE_MODES.join(' | ')}`)
266
- .option('--rate <rate>', `how edits apply: ${RATES.join(' | ')}`, 'manual')
266
+ .option('--rate <rate>', `how edits apply: ${RATES.join(' | ')}`)
267
267
  .option('--plan', 'shorthand for --mode simple (Plan/Ask Only)')
268
268
  .option('--bypass', 'never ask for approval — for CI only, on a machine you are willing to have changed')
269
269
  .option('-C, --cwd <dir>', 'run against a different directory')
@@ -259,9 +259,8 @@ export function modeName(mode) {
259
259
  case 'plan':
260
260
  return 'plan';
261
261
  case 'default':
262
- return 'default';
263
262
  case 'acceptEdits':
264
- return 'accept edits';
263
+ return 'manual';
265
264
  case 'bypass':
266
265
  return 'bypass';
267
266
  }
@@ -271,10 +270,23 @@ export function describeMode(mode) {
271
270
  case 'plan':
272
271
  return 'plan · reads only';
273
272
  case 'default':
274
- return 'default · asks before changes';
275
273
  case 'acceptEdits':
276
- return 'accept edits · writes apply, commands ask';
274
+ return 'manual · asks before edits';
277
275
  case 'bypass':
278
- return 'bypass · no prompts';
276
+ return 'bypass · edits without asking';
277
+ }
278
+ }
279
+ export function codeModeName(mode) {
280
+ switch (mode) {
281
+ case 'frontend':
282
+ return 'Frontend';
283
+ case 'backend':
284
+ return 'Backend';
285
+ case 'fullstack':
286
+ return 'Full Stack';
287
+ case 'simple':
288
+ return 'Plan/Ask';
289
+ case 'security':
290
+ return 'Security';
279
291
  }
280
292
  }
package/dist/ui/App.js CHANGED
@@ -20,7 +20,7 @@ import { CODE_MODE_LABELS, describeMode, isBatchable, mcpServerOf, nextMode, } f
20
20
  import { creditShortfall, startSession } from '../session.js';
21
21
  import { sanitizeForModel } from '../protocol/shell.js';
22
22
  import { diff as gitDiff, gitBranchLine, prCreate } from '../protocol/git.js';
23
- import { Activity, ApprovalPrompt, Banner, CodeModePicker, ModePicker, WORDMARK_MIN_COLUMNS, ChoicePrompt, Completions, HelpPanel, StatusBar, TranscriptLine, ShellPane, SHELL_PANE_LINES, } from './components.js';
23
+ import { Activity, ApprovalPrompt, Banner, CodeModePicker, RatePicker, WORDMARK_MIN_COLUMNS, ChoicePrompt, Completions, HelpPanel, StatusBar, TranscriptLine, ShellPane, SHELL_PANE_LINES, } from './components.js';
24
24
  import { TokenBuffer, terminalWidth } from './render.js';
25
25
  import { color } from './theme.js';
26
26
  import { completeInput, completionsFor } from './slashCommands.js';
@@ -131,7 +131,7 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
131
131
  modeChosenRef.current = true;
132
132
  runnerRef.current?.setMode(chosen);
133
133
  setPhase(sessionReadyRef.current ? 'ready' : 'starting');
134
- push('system', `Mode: ${describeMode(chosen)}`);
134
+ push('system', `Rate: ${describeMode(chosen)}`);
135
135
  if (chosen === 'bypass') {
136
136
  push('notice', 'Running without prompts. Files outside this project are still refused.');
137
137
  }
@@ -146,7 +146,7 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
146
146
  const cycleMode = useCallback(() => {
147
147
  setMode((current) => {
148
148
  const next = nextMode(current);
149
- push('system', `Mode: ${describeMode(next)}`);
149
+ push('system', `Rate: ${describeMode(next)}`);
150
150
  return next;
151
151
  });
152
152
  }, [push]);
@@ -168,16 +168,23 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
168
168
  setShowHelp(false);
169
169
  setScreenGeneration((n) => n + 1);
170
170
  return true;
171
+ case '/agent':
172
+ setPhase('codeMode');
173
+ return true;
171
174
  case '/mode':
172
175
  setPhase('mode');
173
176
  return true;
174
177
  case '/plan':
175
- setMode('plan');
176
- push('system', `Mode: ${describeMode('plan')}`);
178
+ setCodeMode('simple');
179
+ codeModeChosenRef.current = true;
180
+ runnerRef.current?.setCodeMode('simple');
181
+ push('system', `Mode: ${CODE_MODE_LABELS.simple}`);
177
182
  return true;
178
183
  case '/accept':
179
- setMode('acceptEdits');
180
- push('system', `Mode: ${describeMode('acceptEdits')}`);
184
+ setMode('bypass');
185
+ runnerRef.current?.setMode('bypass');
186
+ push('system', `Rate: ${describeMode('bypass')}`);
187
+ push('notice', 'Accept-edits is gone — Bypass applies in-project edits without asking.');
181
188
  return true;
182
189
  case '/credits':
183
190
  openBrowser(`${siteUrl()}/credits`);
@@ -197,7 +204,8 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
197
204
  }
198
205
  case '/status': {
199
206
  push('system', `${status.cloudhandle || 'signed in'} ${status.bcidMasked} · ${status.project} · ` +
200
- `${status.balance !== null ? `${status.balance.toFixed(2)}` : 'balance unknown'} · ${describeMode(mode)}`);
207
+ `${status.balance !== null ? `${status.balance.toFixed(2)}` : 'balance unknown'} · ` +
208
+ `${codeMode ? CODE_MODE_LABELS[codeMode] : 'Mode unset'} · ${describeMode(mode)}`);
201
209
  const facts = sessionFactsRef.current;
202
210
  push('system', `${facts.continuing ? "continuing this directory's thread" : 'new thread'} · ` +
203
211
  `${facts.hasLocalMemory ? 'project notes on disk' : 'no project notes yet'}`);
@@ -227,7 +235,7 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
227
235
  push('notice', `Unknown command ${cmd} — /help for the list.`);
228
236
  return true;
229
237
  }
230
- }, [cwd, cycleMode, mode, push, quit, refreshBalance, status]);
238
+ }, [codeMode, cwd, cycleMode, mode, push, quit, refreshBalance, status]);
231
239
  const submit = useCallback(async (message) => {
232
240
  const text = message.trim();
233
241
  setPromptText('');
@@ -415,7 +423,7 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
415
423
  push('notice', 'Project notes found in .rivine/local_memory.md.');
416
424
  }
417
425
  if (!oneShot && modeChosenRef.current && !askForMode) {
418
- push('system', `Mode: ${describeMode(mode)}`);
426
+ push('system', `Rate: ${describeMode(mode)}`);
419
427
  push('system', 'Ask for anything. /help for commands.');
420
428
  }
421
429
  if (oneShot)
@@ -517,10 +525,11 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
517
525
  const compactBanner = terminalWidth() < WORDMARK_MIN_COLUMNS || Boolean(oneShot);
518
526
  const matches = completionsFor(input);
519
527
  const busy = phase !== 'ready';
520
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => item.kind === 'banner' ? (_jsx(Banner, { compact: compactBanner }, "banner")) : (_jsx(TranscriptLine, { line: item.line }, item.line.id)) }, screenGeneration), live ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: color('text'), children: live }) })) : null, shellView ? _jsx(ShellPane, { text: shellView, live: shellActive }) : null, phase === 'running' && stage ? _jsx(Activity, { stage: stage, tool: tool }) : null, phase === 'codeMode' ? (_jsx(CodeModePicker, { onPick: pickCodeMode, current: codeModeChosenRef.current ? codeMode : undefined, onCancel: codeModeChosenRef.current ? cancelCodeModePick : undefined })) : null, phase === 'mode' ? (_jsx(ModePicker, { onPick: pickMode, current: modeChosenRef.current ? mode : undefined, onCancel: modeChosenRef.current ? cancelModePick : undefined })) : null, phase === 'approval' && approval ? (_jsx(ApprovalPrompt, { approval: approval, allowAlways: isBatchable(approval.category), onChoice: answerApproval })) : null, phase === 'question' && question ? (question.options?.length ? (_jsx(ChoicePrompt, { prompt: question.prompt, options: question.options, onPick: answerQuestion })) : (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('accent'), bold: true, children: question.prompt }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { placeholder: "your answer", onSubmit: answerQuestion })] })] }))) : null, phase === 'hitl' && hitl ? (_jsx(ChoicePrompt, { prompt: hitl.tool ? `${hitl.tool} — ${hitl.message}` : hitl.message, options: hitl.options?.length ? hitl.options : ['Approve', 'Decline'], onPick: (value) => answerHitl(hitl.options?.length
528
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => item.kind === 'banner' ? (_jsx(Banner, { compact: compactBanner }, "banner")) : (_jsx(TranscriptLine, { line: item.line }, item.line.id)) }, screenGeneration), live ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: color('text'), children: live }) })) : null, shellView ? _jsx(ShellPane, { text: shellView, live: shellActive }) : null, phase === 'running' && stage ? _jsx(Activity, { stage: stage, tool: tool }) : null, phase === 'codeMode' ? (_jsx(CodeModePicker, { onPick: pickCodeMode, current: codeModeChosenRef.current ? codeMode : undefined, onCancel: codeModeChosenRef.current ? cancelCodeModePick : undefined })) : null, phase === 'mode' ? (_jsx(RatePicker, { onPick: pickMode, current: modeChosenRef.current ? mode : undefined, onCancel: modeChosenRef.current ? cancelModePick : undefined })) : null, phase === 'approval' && approval ? (_jsx(ApprovalPrompt, { approval: approval, allowAlways: isBatchable(approval.category), onChoice: answerApproval })) : null, phase === 'question' && question ? (question.options?.length ? (_jsx(ChoicePrompt, { prompt: question.prompt, options: question.options, onPick: answerQuestion })) : (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('accent'), bold: true, children: question.prompt }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { placeholder: "your answer", onSubmit: answerQuestion })] })] }))) : null, phase === 'hitl' && hitl ? (_jsx(ChoicePrompt, { prompt: hitl.tool ? `${hitl.tool} — ${hitl.message}` : hitl.message, options: hitl.options?.length ? hitl.options : ['Approve', 'Decline'], onPick: (value) => answerHitl(hitl.options?.length
521
529
  ? { approved: true, feedback: value }
522
530
  : { approved: value === 'Approve' }) })) : null, showHelp && phase === 'ready' ? _jsx(HelpPanel, {}) : null, phase === 'ready' && !oneShot ? (_jsxs(_Fragment, { children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { defaultValue: inputSeed, placeholder: "What should Rivine build?", onChange: setInput, onSubmit: (value) => void submit(value) }, inputNonce)] }), _jsx(Completions, { matches: matches })] })) : null, phase === 'starting' ? _jsx(Activity, { stage: "Starting\u2026", tool: "" }) : null, _jsx(StatusBar, { info: {
523
531
  mode,
532
+ codeMode,
524
533
  project: status.project,
525
534
  cloudhandle: status.cloudhandle,
526
535
  bcidMasked: status.bcidMasked,
@@ -15,7 +15,7 @@ import { Spinner } from '@inkjs/ui';
15
15
  import { ASCII_ONLY, BOX, color, glyph, lineColor } from './theme.js';
16
16
  import { fit, terminalWidth, wrap, terminalColumns } from './render.js';
17
17
  import { SLASH_COMMANDS } from './slashCommands.js';
18
- import { describeMode, modeName } from '../protocol/permissions.js';
18
+ import { codeModeName, describeMode, modeName, } from '../protocol/permissions.js';
19
19
  const WORDMARK = [
20
20
  '█▀▄ ▀█▀ █ █ ▀█▀ █▄ █ █▀▀ █▀▀ █▀█ █▀▄ █▀▀',
21
21
  '█▀▄ █ ▀▄ ▄▀ █ █ ▀█ █▀ █ █ █ █ █ █▀ ',
@@ -67,11 +67,11 @@ export function CodeModePicker({ onPick, current, onCancel, }) {
67
67
  return (_jsxs(Box, { children: [_jsx(Text, { color: active ? color('success') : color('faint'), children: active ? ' \u203a' : ' ' }), _jsx(Text, { color: color('accent'), children: o.key }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: active ? color('text') : color('muted'), bold: active, children: o.label }), _jsx(Text, { color: color('faint'), children: " \u2014 " }), _jsx(Text, { color: color('faint'), children: fit(o.detail, Math.max(10, width - 34)) })] }, o.key));
68
68
  }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: current ? 'esc keeps the current mode' : 'pick a number to begin' }) })] }));
69
69
  }
70
- export function ModePicker({ onPick, current, onCancel, }) {
70
+ export function RatePicker({ onPick, current, onCancel, }) {
71
71
  const width = terminalWidth();
72
72
  const options = [
73
- { key: '1', mode: 'default', label: 'Manual', detail: 'ask for approval before each edit' },
74
- { key: '2', mode: 'bypass', label: 'Bypass', detail: 'edit files in this project without asking' },
73
+ { key: '1', mode: 'default', label: 'Manual', detail: 'ask before each edit' },
74
+ { key: '2', mode: 'bypass', label: 'Bypass', detail: 'edit in this project without asking' },
75
75
  ];
76
76
  useInput((input, key) => {
77
77
  if (key.escape && onCancel) {
@@ -88,12 +88,12 @@ export function ModePicker({ onPick, current, onCancel, }) {
88
88
  });
89
89
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Rate \u2014 how should Rivine apply edits?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => {
90
90
  const active = o.mode === current;
91
- return (_jsxs(Box, { children: [_jsx(Text, { color: active ? color('success') : color('faint'), children: active ? ' ›' : ' ' }), _jsx(Text, { color: color('accent'), children: o.key }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: active ? color('text') : color('muted'), bold: active, children: o.label.padEnd(13) }), _jsx(Text, { color: color('muted'), children: fit(o.detail, Math.max(10, width - 26)) })] }, o.key));
91
+ return (_jsxs(Box, { children: [_jsx(Text, { color: active ? color('success') : color('faint'), children: active ? ' ›' : ' ' }), _jsx(Text, { color: color('accent'), children: o.key }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: active ? color('text') : color('muted'), bold: active, children: o.label.padEnd(8) }), _jsx(Text, { color: color('faint'), children: " \u2014 " }), _jsx(Text, { color: color('muted'), children: fit(o.detail, Math.max(10, width - 24)) })] }, o.key));
92
92
  }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: fit(current
93
- ?
94
- `press a number · enter keeps ${options.find((o) => o.mode === current)?.label ?? 'this'} · esc cancels`
95
- : 'press a number · enter for accept edits · shift+tab changes it later', width - 4) }) })] }));
93
+ ? `press a number · enter keeps ${options.find((o) => o.mode === current)?.label ?? 'this'} · esc cancels`
94
+ : 'press a number · enter for Manual · shift+tab toggles later', width - 4) }) })] }));
96
95
  }
96
+ export const ModePicker = RatePicker;
97
97
  export const SHELL_PANE_LINES = 12;
98
98
  export function ShellPane({ text, live }) {
99
99
  if (!text)
@@ -137,7 +137,7 @@ export function Completions({ matches }) {
137
137
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [matches.slice(0, 6).map((c) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: c.name.padEnd(10) }), _jsx(Text, { color: color('faint'), children: fit(c.summary, width - 14) })] }, c.name))), matches.length === 1 ? (_jsx(Text, { color: color('faint'), children: " tab to complete" })) : null] }));
138
138
  }
139
139
  export function HelpPanel() {
140
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Commands" }), SLASH_COMMANDS.map((c) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: ` ${c.name}`.padEnd(12) }), _jsx(Text, { color: color('muted'), children: c.summary })] }, c.name))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: color('faint'), children: " shift+tab change permission mode" }), _jsx(Text, { color: color('faint'), children: " esc stop the current turn" }), _jsx(Text, { color: color('faint'), children: " ctrl+c quit" })] })] }));
140
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Commands" }), SLASH_COMMANDS.map((c) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: ` ${c.name}`.padEnd(12) }), _jsx(Text, { color: color('muted'), children: c.summary })] }, c.name))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: color('faint'), children: " shift+tab toggle Rate (Manual \u21C4 Bypass)" }), _jsx(Text, { color: color('faint'), children: " esc stop the current turn" }), _jsx(Text, { color: color('faint'), children: " ctrl+c quit" })] })] }));
141
141
  }
142
142
  export function StatusBar({ info }) {
143
143
  const width = terminalColumns();
@@ -157,18 +157,27 @@ export function StatusBar({ info }) {
157
157
  segments.push({ node, text });
158
158
  return true;
159
159
  };
160
- const modeText = describeMode(info.mode);
161
- const modeShort = modeName(info.mode);
160
+ const rateText = describeMode(info.mode);
161
+ const rateShort = modeName(info.mode);
162
+ const agentText = info.codeMode ? codeModeName(info.codeMode) : '';
162
163
  const brand = 'Rivine';
163
- const fitsBrandAndFullMode = 2 + brand.length + 3 + modeText.length <= width - 2;
164
- if (2 + brand.length + 3 + modeShort.length <= width - 2) {
164
+ const fitsBrandAndFullRate = 2 +
165
+ brand.length +
166
+ (agentText ? 3 + agentText.length : 0) +
167
+ 3 +
168
+ rateText.length <=
169
+ width - 2;
170
+ if (2 + brand.length + 3 + rateShort.length <= width - 2) {
165
171
  add(brand, (_jsx(Text, { color: color('brand'), bold: true, children: brand })));
166
172
  }
167
- const shownMode = fitsBrandAndFullMode ? modeText : modeShort;
168
- used += (segments.length > 0 ? 3 : 0) + shownMode.length;
173
+ if (agentText) {
174
+ add(agentText, _jsx(Text, { color: color('accent'), children: agentText }));
175
+ }
176
+ const shownRate = fitsBrandAndFullRate ? rateText : rateShort;
177
+ used += (segments.length > 0 ? 3 : 0) + shownRate.length;
169
178
  segments.push({
170
- node: _jsx(Text, { color: color('muted'), children: shownMode }),
171
- text: shownMode,
179
+ node: _jsx(Text, { color: color('muted'), children: shownRate }),
180
+ text: shownRate,
172
181
  });
173
182
  if (info.balance !== null) {
174
183
  const money = `$${info.balance.toFixed(2)}`;
@@ -10,14 +10,27 @@
10
10
  */
11
11
  export const SLASH_COMMANDS = [
12
12
  { name: '/help', summary: 'show these commands' },
13
+ {
14
+ name: '/agent',
15
+ summary: 'pick which engine Mode runs',
16
+ detail: 'Frontend, Backend, Full Stack, Plan/Ask, or Security.',
17
+ },
13
18
  {
14
19
  name: '/mode',
15
- summary: 'show or cycle the permission mode',
16
- detail: 'Shift+Tab cycles plan → default → accept edits.',
20
+ summary: 'pick the Rate — Manual or Bypass',
21
+ detail: 'Shift+Tab toggles Manual ⇄ Bypass.',
22
+ },
23
+ {
24
+ name: '/plan',
25
+ summary: 'switch to Plan/Ask Only Mode',
26
+ detail: 'Same as picking Plan/Ask in /agent.',
27
+ },
28
+ {
29
+ name: '/accept',
30
+ summary: 'switch Rate to Bypass',
31
+ detail: 'Accept-edits is gone; Bypass applies in-project edits without asking.',
17
32
  },
18
- { name: '/plan', summary: 'switch to plan mode (reads only)' },
19
- { name: '/accept', summary: 'switch to accept-edits mode' },
20
- { name: '/status', summary: 'account, project and balance' },
33
+ { name: '/status', summary: 'account, project, Mode, Rate and balance' },
21
34
  { name: '/diff', summary: 'what changed in the working tree' },
22
35
  {
23
36
  name: '/pr',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beunec/rivine",
3
- "version": "0.2.0",
4
- "description": "Rivine Code in your terminal \u2014 Beunec's software engineering runtime constructs, working on the files you already have open.",
3
+ "version": "0.2.1",
4
+ "description": "Rivine Code in your terminal — Beunec's software engineering runtime constructs, working on the files you already have open.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Beunec Technologies",
7
7
  "homepage": "https://aselius.beunec.com",