@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.
- package/AGENTS.md +298 -0
- package/LICENSE +190 -0
- package/README.md +336 -0
- package/bin/hits.js +56 -0
- package/config/schema.json +94 -0
- package/config/settings.yaml +102 -0
- package/data/dev_handover.yaml +143 -0
- package/hits_core/__init__.py +9 -0
- package/hits_core/ai/__init__.py +11 -0
- package/hits_core/ai/compressor.py +86 -0
- package/hits_core/ai/llm_client.py +65 -0
- package/hits_core/ai/slm_filter.py +126 -0
- package/hits_core/api/__init__.py +3 -0
- package/hits_core/api/routes/__init__.py +8 -0
- package/hits_core/api/routes/auth.py +211 -0
- package/hits_core/api/routes/handover.py +117 -0
- package/hits_core/api/routes/health.py +8 -0
- package/hits_core/api/routes/knowledge.py +177 -0
- package/hits_core/api/routes/node.py +121 -0
- package/hits_core/api/routes/work_log.py +174 -0
- package/hits_core/api/server.py +181 -0
- package/hits_core/auth/__init__.py +21 -0
- package/hits_core/auth/dependencies.py +61 -0
- package/hits_core/auth/manager.py +368 -0
- package/hits_core/auth/middleware.py +69 -0
- package/hits_core/collector/__init__.py +18 -0
- package/hits_core/collector/ai_session_collector.py +118 -0
- package/hits_core/collector/base.py +73 -0
- package/hits_core/collector/daemon.py +94 -0
- package/hits_core/collector/git_collector.py +177 -0
- package/hits_core/collector/hits_action_collector.py +110 -0
- package/hits_core/collector/shell_collector.py +178 -0
- package/hits_core/main.py +36 -0
- package/hits_core/mcp/__init__.py +20 -0
- package/hits_core/mcp/server.py +429 -0
- package/hits_core/models/__init__.py +18 -0
- package/hits_core/models/node.py +56 -0
- package/hits_core/models/tree.py +68 -0
- package/hits_core/models/work_log.py +64 -0
- package/hits_core/models/workflow.py +92 -0
- package/hits_core/platform/__init__.py +5 -0
- package/hits_core/platform/actions.py +225 -0
- package/hits_core/service/__init__.py +6 -0
- package/hits_core/service/handover_service.py +382 -0
- package/hits_core/service/knowledge_service.py +172 -0
- package/hits_core/service/tree_service.py +105 -0
- package/hits_core/storage/__init__.py +11 -0
- package/hits_core/storage/base.py +84 -0
- package/hits_core/storage/file_store.py +314 -0
- package/hits_core/storage/redis_store.py +123 -0
- package/hits_web/dist/assets/index-Bgx7F6m6.css +1 -0
- package/hits_web/dist/assets/index-D1B5E67G.js +3 -0
- package/hits_web/dist/index.html +16 -0
- package/package.json +60 -0
- package/requirements-core.txt +7 -0
- package/requirements.txt +1 -0
- package/run.sh +271 -0
- 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
|
+
}
|