stacks-ai 0.1.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 +93 -0
- package/bin/stacks.cjs +215 -0
- package/dist/assets/aiProvider-DYAx3DVK.js +1 -0
- package/dist/assets/index-C1agmKFP.js +76 -0
- package/dist/assets/index-C8w_QbzK.js +15 -0
- package/dist/assets/index-Dp9e0AZR.js +5252 -0
- package/dist/assets/index-Q3CD-OmM.css +1 -0
- package/dist/assets/indexedDB-dA1h4x9Q.js +1 -0
- package/dist/assets/visionAnalysis-BGbWI8eX.js +9 -0
- package/dist/fonts/frank-the-architect.ttf +0 -0
- package/dist/index.html +13 -0
- package/dist/mcp/proxy.js +404 -0
- package/dist/mcp/server.js +529 -0
- package/electron/main.cjs +183 -0
- package/package.json +86 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Stacks
|
|
2
|
+
|
|
3
|
+
AI-powered infinite canvas workspace for notes, images, and creative organization.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Infinite canvas with zoom and pan
|
|
8
|
+
- Notes, sticky notes, and text items
|
|
9
|
+
- Image support with EXIF metadata
|
|
10
|
+
- AI-powered organization and analysis
|
|
11
|
+
- Connect items with visual links
|
|
12
|
+
- Auto-arrange with ELK layout engine
|
|
13
|
+
- Auto-save to local storage
|
|
14
|
+
- MCP server for AI integration
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
### Run with npx (no installation)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx stacks-ai
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
On first run, Electron will be downloaded automatically (~100MB). Subsequent runs are instant.
|
|
25
|
+
|
|
26
|
+
### Install globally
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g stacks-ai
|
|
30
|
+
stacks-ai
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Install from source
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/jasonkneen/stacks.git
|
|
37
|
+
cd stacks
|
|
38
|
+
npm install
|
|
39
|
+
npm run build
|
|
40
|
+
npm run electron
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Development
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Install dependencies
|
|
47
|
+
npm install
|
|
48
|
+
|
|
49
|
+
# Run dev server (web only)
|
|
50
|
+
npm run dev
|
|
51
|
+
|
|
52
|
+
# Run Electron in dev mode
|
|
53
|
+
npm run electron:dev
|
|
54
|
+
|
|
55
|
+
# Build for production
|
|
56
|
+
npm run build:prod
|
|
57
|
+
|
|
58
|
+
# Run MCP server
|
|
59
|
+
npm run mcp:dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Environment Variables
|
|
63
|
+
|
|
64
|
+
Create a `.env` file in the root directory:
|
|
65
|
+
|
|
66
|
+
```env
|
|
67
|
+
GEMINI_API_KEY=your_gemini_api_key_here
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Package Structure
|
|
71
|
+
|
|
72
|
+
- `bin/` - CLI launcher script
|
|
73
|
+
- `electron/` - Electron main process
|
|
74
|
+
- `dist/` - Built application assets
|
|
75
|
+
- `mcp/` - Model Context Protocol server
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
When you run `npx stacks-ai` or `stacks-ai`:
|
|
80
|
+
|
|
81
|
+
1. The launcher script checks if Electron is installed
|
|
82
|
+
2. If not, it downloads Electron (~100MB, cached in `~/.stacks/`)
|
|
83
|
+
3. The MCP proxy starts for AI integration
|
|
84
|
+
4. Electron loads the built app from `dist/`
|
|
85
|
+
5. The app runs entirely locally - no internet required (except for AI features)
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
90
|
+
|
|
91
|
+
## Author
|
|
92
|
+
|
|
93
|
+
Jason Kneen
|
package/bin/stacks.cjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* npx launcher for Stacks
|
|
5
|
+
* Downloads and caches Electron binary on first run, then launches the app
|
|
6
|
+
* Electron is cached in ~/.stacks/ for persistence across npx runs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { spawn, execFileSync } = require('child_process')
|
|
10
|
+
const path = require('path')
|
|
11
|
+
const fs = require('fs')
|
|
12
|
+
const os = require('os')
|
|
13
|
+
|
|
14
|
+
const APP_NAME = 'stacks'
|
|
15
|
+
const APP_DIR = path.join(__dirname, '..')
|
|
16
|
+
const CACHE_DIR = path.join(os.homedir(), '.stacks')
|
|
17
|
+
const ELECTRON_CACHE = path.join(CACHE_DIR, 'electron')
|
|
18
|
+
|
|
19
|
+
let proxyProcess = null
|
|
20
|
+
|
|
21
|
+
function ensureCacheDir() {
|
|
22
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
23
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true })
|
|
24
|
+
}
|
|
25
|
+
if (!fs.existsSync(ELECTRON_CACHE)) {
|
|
26
|
+
fs.mkdirSync(ELECTRON_CACHE, { recursive: true })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getElectronPath() {
|
|
31
|
+
const platform = os.platform()
|
|
32
|
+
const electronBin = platform === 'win32' ? 'electron.cmd' : 'electron'
|
|
33
|
+
return path.join(ELECTRON_CACHE, 'node_modules', '.bin', electronBin)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getNpmCommand() {
|
|
37
|
+
return os.platform() === 'win32' ? 'npm.cmd' : 'npm'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getNodeCommand() {
|
|
41
|
+
return process.execPath
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function ensureElectron() {
|
|
45
|
+
ensureCacheDir()
|
|
46
|
+
|
|
47
|
+
const electronPath = getElectronPath()
|
|
48
|
+
|
|
49
|
+
// Check if electron is already cached
|
|
50
|
+
if (fs.existsSync(electronPath)) {
|
|
51
|
+
return electronPath
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('☐ Installing Electron (first run only)...')
|
|
55
|
+
console.log(` Cache location: ${ELECTRON_CACHE}`)
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Create a minimal package.json for electron installation
|
|
59
|
+
const pkgPath = path.join(ELECTRON_CACHE, 'package.json')
|
|
60
|
+
fs.writeFileSync(pkgPath, JSON.stringify({
|
|
61
|
+
name: 'stacks-electron-cache',
|
|
62
|
+
version: '1.0.0',
|
|
63
|
+
private: true
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
// Install electron to cache directory using execFileSync (safer than execSync)
|
|
67
|
+
const npm = getNpmCommand()
|
|
68
|
+
execFileSync(npm, ['install', 'electron@latest', '--no-save', '--no-audit', '--no-fund'], {
|
|
69
|
+
cwd: ELECTRON_CACHE,
|
|
70
|
+
stdio: 'inherit'
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
console.log('✓ Electron installed successfully!\n')
|
|
74
|
+
return electronPath
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('✗ Failed to install Electron:', error.message)
|
|
77
|
+
console.error('\nTry installing manually:')
|
|
78
|
+
console.error(` cd ${ELECTRON_CACHE} && npm install electron`)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function checkBuilt() {
|
|
84
|
+
const distDir = path.join(APP_DIR, 'dist')
|
|
85
|
+
const indexHtml = path.join(distDir, 'index.html')
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(indexHtml)) {
|
|
88
|
+
console.error('✗ App not built. dist/index.html not found.')
|
|
89
|
+
console.error('\nIf you cloned from source, run:')
|
|
90
|
+
console.error(' npm install && npm run build')
|
|
91
|
+
process.exit(1)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function startProxy() {
|
|
96
|
+
const proxyPath = path.join(APP_DIR, 'dist', 'mcp', 'proxy.js')
|
|
97
|
+
|
|
98
|
+
if (!fs.existsSync(proxyPath)) {
|
|
99
|
+
console.log('⚠ MCP proxy not found, skipping...')
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('☐ Starting MCP proxy...')
|
|
104
|
+
|
|
105
|
+
const node = getNodeCommand()
|
|
106
|
+
proxyProcess = spawn(node, [proxyPath], {
|
|
107
|
+
cwd: APP_DIR,
|
|
108
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
109
|
+
env: {
|
|
110
|
+
...process.env,
|
|
111
|
+
NODE_ENV: 'production'
|
|
112
|
+
},
|
|
113
|
+
detached: false
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
proxyProcess.stdout.on('data', () => {
|
|
117
|
+
// Proxy logs go to stderr by design, stdout is for JSON-RPC
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
proxyProcess.stderr.on('data', (data) => {
|
|
121
|
+
const msg = data.toString().trim()
|
|
122
|
+
if (msg.includes('ready')) {
|
|
123
|
+
console.log('✓ MCP proxy ready')
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
proxyProcess.on('error', (err) => {
|
|
128
|
+
console.error('⚠ Proxy error:', err.message)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return proxyProcess
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stopProxy() {
|
|
135
|
+
if (proxyProcess) {
|
|
136
|
+
proxyProcess.kill()
|
|
137
|
+
proxyProcess = null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function launch() {
|
|
142
|
+
try {
|
|
143
|
+
console.log(`\n🚀 Starting Stacks...\n`)
|
|
144
|
+
|
|
145
|
+
checkBuilt()
|
|
146
|
+
startProxy()
|
|
147
|
+
const electronPath = await ensureElectron()
|
|
148
|
+
|
|
149
|
+
// Launch Electron with the app
|
|
150
|
+
const child = spawn(electronPath, [APP_DIR], {
|
|
151
|
+
stdio: 'inherit',
|
|
152
|
+
env: {
|
|
153
|
+
...process.env,
|
|
154
|
+
NODE_ENV: 'production',
|
|
155
|
+
ELECTRON_DISABLE_SECURITY_WARNINGS: 'true'
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
child.on('exit', (code) => {
|
|
160
|
+
stopProxy()
|
|
161
|
+
process.exit(code || 0)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
child.on('error', (err) => {
|
|
165
|
+
console.error('Failed to start Electron:', err.message)
|
|
166
|
+
stopProxy()
|
|
167
|
+
process.exit(1)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Handle signals
|
|
171
|
+
process.on('SIGINT', () => {
|
|
172
|
+
stopProxy()
|
|
173
|
+
child.kill('SIGINT')
|
|
174
|
+
})
|
|
175
|
+
process.on('SIGTERM', () => {
|
|
176
|
+
stopProxy()
|
|
177
|
+
child.kill('SIGTERM')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Failed to launch Stacks:', error.message)
|
|
182
|
+
stopProxy()
|
|
183
|
+
process.exit(1)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// CLI arguments
|
|
188
|
+
const args = process.argv.slice(2)
|
|
189
|
+
|
|
190
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
191
|
+
console.log(`
|
|
192
|
+
Stacks - AI-powered infinite canvas
|
|
193
|
+
|
|
194
|
+
Usage:
|
|
195
|
+
npx stacks-ai Launch the app
|
|
196
|
+
npx stacks-ai --clean Clear cached Electron installation
|
|
197
|
+
npx stacks-ai --help Show this help message
|
|
198
|
+
|
|
199
|
+
Cache location: ${CACHE_DIR}
|
|
200
|
+
`)
|
|
201
|
+
process.exit(0)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (args.includes('--clean')) {
|
|
205
|
+
console.log('Cleaning Electron cache...')
|
|
206
|
+
if (fs.existsSync(ELECTRON_CACHE)) {
|
|
207
|
+
fs.rmSync(ELECTRON_CACHE, { recursive: true })
|
|
208
|
+
console.log('✓ Cache cleared')
|
|
209
|
+
} else {
|
|
210
|
+
console.log('Cache already empty')
|
|
211
|
+
}
|
|
212
|
+
process.exit(0)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
launch()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{s as g}from"./index-C1agmKFP.js";import{c as s,o as c,a as y}from"./index-C8w_QbzK.js";const t={},i=e=>{const r={anthropic:"anthropic-api-key",openai:"openai-api-key",google:"gemini-api-key"}[e],o=localStorage.getItem(r);return console.log(`[aiProvider] Getting API key for ${e}:`,{keyName:r,hasKey:!!o,keyLength:(o==null?void 0:o.length)||0,keyPreview:o?`${o.substring(0,10)}...`:"none"}),o||{anthropic:t==null?void 0:t.VITE_ANTHROPIC_API_KEY,openai:t==null?void 0:t.VITE_OPENAI_API_KEY,google:t==null?void 0:t.VITE_GEMINI_API_KEY}[e]||""},h=e=>{const a=e.apiKey||i(e.provider);if(console.log("[aiProvider] getLanguageModel called:",{provider:e.provider,model:e.model,hasApiKey:!!a,apiKeyLength:(a==null?void 0:a.length)||0,configApiKey:!!e.apiKey}),!a)throw new Error(`${e.provider} API key not configured. Please add your API key in Settings → Providers.`);switch(e.provider){case"anthropic":return y(e.model||"claude-sonnet-4-5-20250929",{apiKey:a});case"openai":return c(e.model||"gpt-4o",{apiKey:a});case"google":return s({apiKey:a})(e.model||"gemini-2.0-flash-exp");default:throw new Error(`Unknown provider: ${e.provider}`)}},I=async(e,a,r)=>{const o=(r==null?void 0:r.provider)||"google",l=h({provider:o,model:r==null?void 0:r.model}),n=await g({model:l,system:r==null?void 0:r.systemPrompt,prompt:e});for await(const d of n.textStream)a(d)};export{I as generateTextStream,h as getLanguageModel};
|