@purpleraven/hits 0.2.0

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.
Files changed (58) hide show
  1. package/AGENTS.md +298 -0
  2. package/LICENSE +190 -0
  3. package/README.md +336 -0
  4. package/bin/hits.js +56 -0
  5. package/config/schema.json +94 -0
  6. package/config/settings.yaml +102 -0
  7. package/data/dev_handover.yaml +143 -0
  8. package/hits_core/__init__.py +9 -0
  9. package/hits_core/ai/__init__.py +11 -0
  10. package/hits_core/ai/compressor.py +86 -0
  11. package/hits_core/ai/llm_client.py +65 -0
  12. package/hits_core/ai/slm_filter.py +126 -0
  13. package/hits_core/api/__init__.py +3 -0
  14. package/hits_core/api/routes/__init__.py +8 -0
  15. package/hits_core/api/routes/auth.py +211 -0
  16. package/hits_core/api/routes/handover.py +117 -0
  17. package/hits_core/api/routes/health.py +8 -0
  18. package/hits_core/api/routes/knowledge.py +177 -0
  19. package/hits_core/api/routes/node.py +121 -0
  20. package/hits_core/api/routes/work_log.py +174 -0
  21. package/hits_core/api/server.py +181 -0
  22. package/hits_core/auth/__init__.py +21 -0
  23. package/hits_core/auth/dependencies.py +61 -0
  24. package/hits_core/auth/manager.py +368 -0
  25. package/hits_core/auth/middleware.py +69 -0
  26. package/hits_core/collector/__init__.py +18 -0
  27. package/hits_core/collector/ai_session_collector.py +118 -0
  28. package/hits_core/collector/base.py +73 -0
  29. package/hits_core/collector/daemon.py +94 -0
  30. package/hits_core/collector/git_collector.py +177 -0
  31. package/hits_core/collector/hits_action_collector.py +110 -0
  32. package/hits_core/collector/shell_collector.py +178 -0
  33. package/hits_core/main.py +36 -0
  34. package/hits_core/mcp/__init__.py +20 -0
  35. package/hits_core/mcp/server.py +429 -0
  36. package/hits_core/models/__init__.py +18 -0
  37. package/hits_core/models/node.py +56 -0
  38. package/hits_core/models/tree.py +68 -0
  39. package/hits_core/models/work_log.py +64 -0
  40. package/hits_core/models/workflow.py +92 -0
  41. package/hits_core/platform/__init__.py +5 -0
  42. package/hits_core/platform/actions.py +225 -0
  43. package/hits_core/service/__init__.py +6 -0
  44. package/hits_core/service/handover_service.py +382 -0
  45. package/hits_core/service/knowledge_service.py +172 -0
  46. package/hits_core/service/tree_service.py +105 -0
  47. package/hits_core/storage/__init__.py +11 -0
  48. package/hits_core/storage/base.py +84 -0
  49. package/hits_core/storage/file_store.py +314 -0
  50. package/hits_core/storage/redis_store.py +123 -0
  51. package/hits_web/dist/assets/index-Bgx7F6m6.css +1 -0
  52. package/hits_web/dist/assets/index-D1B5E67G.js +3 -0
  53. package/hits_web/dist/index.html +16 -0
  54. package/package.json +60 -0
  55. package/requirements-core.txt +7 -0
  56. package/requirements.txt +1 -0
  57. package/run.sh +271 -0
  58. package/server.js +234 -0
package/server.js ADDED
@@ -0,0 +1,234 @@
1
+ /**
2
+ * HITS Node.js Server
3
+ *
4
+ * Serves:
5
+ * - Static frontend (hits_web/dist/)
6
+ * - API proxy to Python FastAPI backend (localhost:8765)
7
+ *
8
+ * Manages:
9
+ * - Python venv creation + dependency installation
10
+ * - Python backend lifecycle (start/stop/restart)
11
+ * - Graceful shutdown
12
+ */
13
+
14
+ import express from 'express';
15
+ import { createProxyMiddleware } from 'http-proxy-middleware';
16
+ import { spawn, execSync } from 'node:child_process';
17
+ import { existsSync } from 'node:fs';
18
+ import { join, dirname } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { platform } from 'node:os';
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const ROOT = __dirname;
24
+ const isWin = platform() === 'win32';
25
+
26
+ // ─── Python Detection ───────────────────────────────────────────
27
+
28
+ function findPython() {
29
+ const envPython = process.env.HITS_PYTHON;
30
+ if (envPython && existsSync(envPython)) return envPython;
31
+
32
+ const candidates = isWin
33
+ ? ['python', 'python3', 'py']
34
+ : ['python3', 'python'];
35
+
36
+ for (const cmd of candidates) {
37
+ try {
38
+ const ver = execSync(`${cmd} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
39
+ const match = ver.match(/Python (\d+)\.(\d+)/);
40
+ if (match && (parseInt(match[1]) > 3 || (parseInt(match[1]) === 3 && parseInt(match[2]) >= 10))) {
41
+ return cmd;
42
+ }
43
+ } catch {}
44
+ }
45
+ return null;
46
+ }
47
+
48
+ // ─── Venv Management ────────────────────────────────────────────
49
+
50
+ const VENV_DIR = join(ROOT, 'venv');
51
+ const PYTHON_BIN = isWin
52
+ ? join(VENV_DIR, 'Scripts', 'python.exe')
53
+ : join(VENV_DIR, 'bin', 'python');
54
+
55
+ function venvExists() {
56
+ return existsSync(PYTHON_BIN);
57
+ }
58
+
59
+ function pipPath() {
60
+ return isWin
61
+ ? join(VENV_DIR, 'Scripts', 'pip.exe')
62
+ : join(VENV_DIR, 'bin', 'pip');
63
+ }
64
+
65
+ function runCommand(cmd, args, opts = {}) {
66
+ return new Promise((resolve, reject) => {
67
+ const proc = spawn(cmd, args, { stdio: 'inherit', ...opts });
68
+ proc.on('close', (code) => {
69
+ if (code === 0) resolve();
70
+ else reject(new Error(`Command failed: ${cmd} ${args.join(' ')} (exit ${code})`));
71
+ });
72
+ proc.on('error', reject);
73
+ });
74
+ }
75
+
76
+ async function setupPython() {
77
+ const pythonCmd = findPython();
78
+ if (!pythonCmd) {
79
+ console.error('Error: Python 3.10+ not found.');
80
+ console.error('Install: https://www.python.org/downloads/');
81
+ process.exit(1);
82
+ }
83
+
84
+ if (!venvExists()) {
85
+ console.log(`Creating virtual environment... (${pythonCmd})`);
86
+ await runCommand(pythonCmd, ['-m', 'venv', VENV_DIR], { cwd: ROOT });
87
+ }
88
+
89
+ if (!existsSync(pipPath())) {
90
+ console.error('Error: pip not found in venv.');
91
+ process.exit(1);
92
+ }
93
+
94
+ // Check if deps are installed
95
+ try {
96
+ execSync(`${PYTHON_BIN} -c "import fastapi, pydantic, argon2, jose"`, { stdio: 'ignore' });
97
+ } catch {
98
+ console.log('Installing Python dependencies...');
99
+ await runCommand(PYTHON_BIN, ['-m', 'pip', 'install', '-q', '--upgrade', 'pip'], { cwd: ROOT });
100
+ await runCommand(PYTHON_BIN, ['-m', 'pip', 'install', '-q', '-r', 'requirements.txt'], { cwd: ROOT });
101
+ console.log('Dependencies installed.');
102
+ }
103
+ }
104
+
105
+ // ─── Python Backend Process ─────────────────────────────────────
106
+
107
+ let backendProc = null;
108
+ const API_PORT = 8765;
109
+
110
+ function startBackend(dev) {
111
+ return new Promise((resolve, reject) => {
112
+ const args = ['-m', 'hits_core.main', '--port', String(API_PORT)];
113
+ if (dev) args.push('--dev');
114
+
115
+ console.log(`Starting Python backend on port ${API_PORT}...`);
116
+ backendProc = spawn(PYTHON_BIN, args, {
117
+ cwd: ROOT,
118
+ stdio: ['ignore', 'pipe', 'pipe'],
119
+ env: { ...process.env },
120
+ });
121
+
122
+ let started = false;
123
+
124
+ backendProc.stdout.on('data', (data) => {
125
+ const msg = data.toString().trim();
126
+ if (dev && msg) console.log(`[api] ${msg}`);
127
+ if (!started && (msg.includes('Uvicorn running') || msg.includes('Application startup complete') || msg.includes('Started server'))) {
128
+ started = true;
129
+ resolve();
130
+ }
131
+ });
132
+
133
+ backendProc.stderr.on('data', (data) => {
134
+ const msg = data.toString().trim();
135
+ if (dev && msg) console.error(`[api] ${msg}`);
136
+ if (!started && (msg.includes('Uvicorn running') || msg.includes('Application startup complete') || msg.includes('Started server'))) {
137
+ started = true;
138
+ resolve();
139
+ }
140
+ });
141
+
142
+ backendProc.on('error', (err) => {
143
+ console.error('Backend error:', err.message);
144
+ if (!started) reject(err);
145
+ });
146
+
147
+ backendProc.on('close', (code) => {
148
+ if (code && code !== 0 && !started) {
149
+ reject(new Error(`Backend exited with code ${code}`));
150
+ }
151
+ });
152
+
153
+ // Fallback: if no startup message within 5s, assume it's running
154
+ setTimeout(() => {
155
+ if (!started) {
156
+ started = true;
157
+ resolve();
158
+ }
159
+ }, 5000);
160
+ });
161
+ }
162
+
163
+ function stopBackend() {
164
+ if (backendProc && !backendProc.killed) {
165
+ console.log('Stopping backend...');
166
+ backendProc.kill('SIGTERM');
167
+ backendProc = null;
168
+ }
169
+ }
170
+
171
+ // ─── Express Server ─────────────────────────────────────────────
172
+
173
+ export async function startServer({ port, dev = false, setupOnly = false }) {
174
+ // 1. Setup Python
175
+ await setupPython();
176
+
177
+ if (setupOnly) {
178
+ console.log('Setup complete. Run "npx hits" to start.');
179
+ return;
180
+ }
181
+
182
+ // 2. Check frontend
183
+ const distDir = join(ROOT, 'hits_web', 'dist');
184
+ if (!existsSync(join(distDir, 'index.html'))) {
185
+ console.error('Error: Frontend not built.');
186
+ console.error('Run: cd hits_web && npm install && npm run build');
187
+ process.exit(1);
188
+ }
189
+
190
+ // 3. Start Python backend
191
+ await startBackend(dev);
192
+
193
+ // 4. Start Express
194
+ const app = express();
195
+
196
+ // API proxy → Python FastAPI
197
+ app.use('/api', createProxyMiddleware({
198
+ target: `http://127.0.0.1:${API_PORT}`,
199
+ changeOrigin: true,
200
+ logger: dev ? console : undefined,
201
+ }));
202
+
203
+ // Static files
204
+ app.use(express.static(distDir));
205
+
206
+ // SPA fallback
207
+ app.get('*', (req, res) => {
208
+ const indexPath = join(distDir, 'index.html');
209
+ res.sendFile(indexPath);
210
+ });
211
+
212
+ const server = app.listen(port, () => {
213
+ console.log('');
214
+ console.log(` 🌳 HITS Web Server`);
215
+ console.log(` ${'─'.repeat(30)}`);
216
+ console.log(` Local: http://127.0.0.1:${port}`);
217
+ console.log(` API: http://127.0.0.1:${API_PORT}`);
218
+ if (dev) console.log(` Mode: development`);
219
+ console.log('');
220
+ console.log(' Press Ctrl+C to stop');
221
+ console.log('');
222
+ });
223
+
224
+ // Graceful shutdown
225
+ const shutdown = () => {
226
+ console.log('\nShutting down...');
227
+ stopBackend();
228
+ server.close(() => process.exit(0));
229
+ setTimeout(() => process.exit(0), 3000);
230
+ };
231
+
232
+ process.on('SIGINT', shutdown);
233
+ process.on('SIGTERM', shutdown);
234
+ }