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 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};