feega 0.1.4 → 0.1.5

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
@@ -7,3 +7,5 @@ npx feega my-board.feega
7
7
  Starts feega's local mode on http://localhost:5173 and saves the canvas to `my-board.feega` in the current directory. Needs Node 22+. Each start checks for a newer app and downloads it into `~/.feega/app/<version>/`; offline it uses the newest cached one.
8
8
 
9
9
  Options: `--port <n>`, `--no-open`. `FEEGA_APP_BUNDLE=<path or url>` uses another bundle.
10
+
11
+ AI keys: put them in the environment or in a `.env` in the folder you run `npx feega` from. Kie.ai, OpenRouter, the text LLM, Higgsfield, Wiro and ElevenLabs are supported; see the local mode docs for the variables.
package/bin/feega.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
- import { createWriteStream, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
3
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
4
4
  import { homedir, tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { Readable } from 'node:stream';
7
7
  import { pipeline } from 'node:stream/promises';
8
8
 
9
- import { USAGE, appDir, bundleSource, boundUrl, cachedVersions, freePort, isCached, latestAppVersion, markCached, parseArgs, pickVersion } from '../launcher.mjs';
9
+ import { MCP_ENTRY, USAGE, appDir, bundleSource, boundUrl, cachedVersions, freePort, isCached, latestAppVersion, markCached, mcpEnv, parseArgs, pickVersion } from '../launcher.mjs';
10
10
 
11
11
  const MIN_NODE_MAJOR = 22;
12
12
  const WEBSTORAGE_NODE_MAJOR = 25;
@@ -15,6 +15,7 @@ const OPENERS = { darwin: ['open'], win32: ['cmd', '/c', 'start', ''] };
15
15
  const RELEASES_API = 'https://api.github.com/repos/andreabuttarelli/feega/releases?per_page=50';
16
16
  const CHECK_TIMEOUT_MS = 2000;
17
17
  const LOCAL_BUNDLE_VERSION = 'local';
18
+ const TO_STDERR = ['ignore', 2, 2];
18
19
 
19
20
  const nodeMajor = Number(process.versions.node.split('.')[0]);
20
21
 
@@ -51,20 +52,20 @@ async function chooseVersion(home) {
51
52
  }
52
53
 
53
54
  async function install(dir, version) {
54
- console.log(`feega: downloading the app ${version} (once per version)…`);
55
+ console.error(`feega: downloading the app ${version} (once per version)…`);
55
56
  rmSync(dir, { recursive: true, force: true });
56
57
  mkdirSync(dir, { recursive: true });
57
58
 
58
59
  const archive = await download(bundleSource(version, process.env), join(tmpdir(), `feega-app-${version}-${process.pid}.tar.gz`));
59
- const untar = spawnSync('tar', ['-xzf', archive, '-C', dir], { stdio: 'inherit' });
60
+ const untar = spawnSync('tar', ['-xzf', archive, '-C', dir], { stdio: TO_STDERR });
60
61
  if (untar.status !== 0) {
61
62
  throw new Error('could not extract the app bundle');
62
63
  }
63
64
 
64
- console.log('feega: installing its dependencies for this machine…');
65
+ console.error('feega: installing its dependencies for this machine…');
65
66
  const deps = spawnSync('npm', ['ci', '--omit=dev', '--no-audit', '--no-fund', '--loglevel=error'], {
66
67
  cwd: dir,
67
- stdio: 'inherit',
68
+ stdio: TO_STDERR,
68
69
  shell: process.platform === 'win32'
69
70
  });
70
71
  if (deps.status !== 0) {
@@ -83,6 +84,32 @@ async function openWhenReady(url) {
83
84
  spawn(cmd, [...args, url], { stdio: 'ignore', detached: true }).on('error', () => {}).unref();
84
85
  }
85
86
 
87
+ async function readyApp() {
88
+ const { version, action } = await chooseVersion(homedir());
89
+ const dir = appDir(homedir(), version);
90
+ if (action === 'download') {
91
+ await install(dir, version);
92
+ }
93
+
94
+ return dir;
95
+ }
96
+
97
+ function forward(child) {
98
+ for (const signal of ['SIGINT', 'SIGTERM']) {
99
+ process.on(signal, () => child.kill(signal));
100
+ }
101
+ child.on('exit', (code) => process.exit(code ?? 0));
102
+ }
103
+
104
+ function serveMcp(dir, port) {
105
+ const entry = join(dir, MCP_ENTRY);
106
+ if (!existsSync(entry)) {
107
+ throw new Error('this app version has no MCP server: run `npx feega@latest mcp`');
108
+ }
109
+
110
+ forward(spawn(process.execPath, [entry], { stdio: 'inherit', env: mcpEnv(port, process.env) }));
111
+ }
112
+
86
113
  async function main() {
87
114
  if (nodeMajor < MIN_NODE_MAJOR) {
88
115
  throw new Error(`Node ${MIN_NODE_MAJOR}+ is required (found ${process.versions.node})`);
@@ -99,13 +126,14 @@ async function main() {
99
126
  return;
100
127
  }
101
128
 
129
+ const dir = await readyApp();
130
+ if (args.command === 'mcp') {
131
+ serveMcp(dir, args.port);
132
+ return;
133
+ }
134
+
102
135
  const { file, browser } = args;
103
136
  const port = await freePort(args.port);
104
- const { version, action } = await chooseVersion(homedir());
105
- const dir = appDir(homedir(), version);
106
- if (action === 'download') {
107
- await install(dir, version);
108
- }
109
137
 
110
138
  const url = boundUrl(port);
111
139
  console.log(`feega: saving to ${file}`);
@@ -114,10 +142,7 @@ async function main() {
114
142
  const nodeOptions = nodeMajor >= WEBSTORAGE_NODE_MAJOR ? '--no-experimental-webstorage' : '';
115
143
  const env = { ...process.env, NODE_OPTIONS: [process.env.NODE_OPTIONS, nodeOptions].filter(Boolean).join(' ') };
116
144
  const server = spawn(process.execPath, [realpathSync(join(dir, 'scripts/local.mjs')), file, `--port=${port}`], { stdio: 'inherit', env });
117
- for (const signal of ['SIGINT', 'SIGTERM']) {
118
- process.on(signal, () => server.kill(signal));
119
- }
120
- server.on('exit', (code) => process.exit(code ?? 0));
145
+ forward(server);
121
146
 
122
147
  if (browser === 'open') {
123
148
  await openWhenReady(url);
package/launcher.mjs CHANGED
@@ -5,7 +5,10 @@ import { join, resolve } from 'node:path';
5
5
  const DEFAULT_FILE = 'canvas.feega';
6
6
  const DEFAULT_PORT = 0;
7
7
  export const LOOPBACK = '127.0.0.1';
8
- export const USAGE = `usage: feega [file.feega] [--port <n>] [--no-open] [--help] [--version]`;
8
+ export const MCP_PORT = 5173;
9
+ export const MCP_ENTRY = 'mcp.mjs';
10
+ export const USAGE = `usage: feega [file.feega] [--port <n>] [--no-open] [--help] [--version]
11
+ feega mcp [--port <n>] MCP server (stdio) for the local feega on that port (default ${MCP_PORT})`;
9
12
  const CACHED_MARKER = '.complete';
10
13
  const APP_TAG = 'app-v';
11
14
  const RELEASES = 'https://github.com/andreabuttarelli/feega/releases/download';
@@ -20,8 +23,41 @@ const portOf = (raw) => {
20
23
  return port;
21
24
  };
22
25
 
26
+ const portFlag = (arg, rest) => {
27
+ if (arg === '--port') {
28
+ return portOf(rest.shift());
29
+ }
30
+
31
+ if (arg.startsWith('--port=')) {
32
+ return portOf(arg.slice('--port='.length));
33
+ }
34
+
35
+ return null;
36
+ };
37
+
38
+ function parseMcp(rest) {
39
+ let port = MCP_PORT;
40
+ while (rest.length) {
41
+ const arg = rest.shift();
42
+ const flagged = portFlag(arg, rest);
43
+ if (flagged === null) {
44
+ throw new Error(`unknown option: ${arg}\n${USAGE}`);
45
+ }
46
+
47
+ port = flagged;
48
+ }
49
+
50
+ return { command: 'mcp', port };
51
+ }
52
+
53
+ export const mcpEnv = (port, env) => ({ ...env, PUBLIC_APP_URL: boundUrl(port) });
54
+
23
55
  export function parseArgs(argv, cwd) {
24
56
  const rest = [...argv];
57
+ if (rest[0] === 'mcp') {
58
+ return parseMcp(rest.slice(1));
59
+ }
60
+
25
61
  const files = [];
26
62
  let port = DEFAULT_PORT;
27
63
  let browser = 'open';
@@ -41,13 +77,9 @@ export function parseArgs(argv, cwd) {
41
77
  continue;
42
78
  }
43
79
 
44
- if (arg === '--port') {
45
- port = portOf(rest.shift());
46
- continue;
47
- }
48
-
49
- if (arg.startsWith('--port=')) {
50
- port = portOf(arg.slice('--port='.length));
80
+ const flagged = portFlag(arg, rest);
81
+ if (flagged !== null) {
82
+ port = flagged;
51
83
  continue;
52
84
  }
53
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feega",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Run feega's infinite canvas locally, saved to a .feega file. No account, no database.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",