stenotype 0.2.0 → 0.3.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.
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import session from 'express-session';
|
|
3
|
+
import bcrypt from 'bcryptjs';
|
|
4
|
+
import keytar from 'keytar';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
const SERVICE_NAME = 'stenotype-dashboard';
|
|
14
|
+
const PORT = parseInt(process.env.DASHBOARD_PORT || '3737', 10);
|
|
15
|
+
const HOST = '0.0.0.0';
|
|
16
|
+
|
|
17
|
+
// Load credentials from OS keychain
|
|
18
|
+
let USERNAME, PASSWORD_HASH, SESSION_SECRET;
|
|
19
|
+
try {
|
|
20
|
+
[USERNAME, PASSWORD_HASH, SESSION_SECRET] = await Promise.all([
|
|
21
|
+
keytar.getPassword(SERVICE_NAME, 'username'),
|
|
22
|
+
keytar.getPassword(SERVICE_NAME, 'password_hash'),
|
|
23
|
+
keytar.getPassword(SERVICE_NAME, 'session_secret'),
|
|
24
|
+
]);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('Failed to load dashboard credentials from keychain:', err.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!USERNAME || !PASSWORD_HASH || !SESSION_SECRET) {
|
|
31
|
+
console.error('\n Dashboard credentials not configured.');
|
|
32
|
+
console.error(' Run: stenotype dashboard setup\n');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Stenotype config paths
|
|
37
|
+
const STENO_DIR = path.join(os.homedir(), '.stenotype');
|
|
38
|
+
const CONFIG_PATH = path.join(STENO_DIR, 'stenotype.config.json');
|
|
39
|
+
const LICENSE_PATH = path.join(STENO_DIR, 'license.key');
|
|
40
|
+
|
|
41
|
+
// DB path
|
|
42
|
+
const DB_PATH = path.join(os.homedir(), '.stenotype', 'memories.db');
|
|
43
|
+
|
|
44
|
+
let db;
|
|
45
|
+
try {
|
|
46
|
+
db = new DatabaseSync(DB_PATH);
|
|
47
|
+
console.log(`Connected to DB: ${DB_PATH}`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`Failed to open DB at ${DB_PATH}:`, err.message);
|
|
50
|
+
console.error('Server will start but API routes will fail until DB is available.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const app = express();
|
|
54
|
+
|
|
55
|
+
app.use(express.json());
|
|
56
|
+
app.use(express.urlencoded({ extended: true }));
|
|
57
|
+
|
|
58
|
+
app.use(
|
|
59
|
+
session({
|
|
60
|
+
secret: SESSION_SECRET,
|
|
61
|
+
resave: false,
|
|
62
|
+
saveUninitialized: false,
|
|
63
|
+
cookie: {
|
|
64
|
+
secure: false,
|
|
65
|
+
httpOnly: true,
|
|
66
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Serve static files
|
|
72
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
73
|
+
|
|
74
|
+
// Root redirect
|
|
75
|
+
app.get('/', (req, res) => {
|
|
76
|
+
res.redirect('/login.html');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Auth routes (no auth required)
|
|
80
|
+
app.post('/auth/login', (req, res) => {
|
|
81
|
+
const { username, password } = req.body;
|
|
82
|
+
if (!username || !password) {
|
|
83
|
+
return res.status(400).json({ error: 'Username and password required.' });
|
|
84
|
+
}
|
|
85
|
+
if (username !== USERNAME) {
|
|
86
|
+
return res.status(401).json({ error: 'Invalid credentials.' });
|
|
87
|
+
}
|
|
88
|
+
const valid = bcrypt.compareSync(password, PASSWORD_HASH);
|
|
89
|
+
if (!valid) {
|
|
90
|
+
return res.status(401).json({ error: 'Invalid credentials.' });
|
|
91
|
+
}
|
|
92
|
+
req.session.user = { username };
|
|
93
|
+
res.json({ ok: true, user: { username } });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.post('/auth/logout', (req, res) => {
|
|
97
|
+
req.session.destroy(() => {
|
|
98
|
+
res.json({ ok: true });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.get('/auth/me', (req, res) => {
|
|
103
|
+
if (req.session && req.session.user) {
|
|
104
|
+
return res.json({ user: req.session.user });
|
|
105
|
+
}
|
|
106
|
+
res.json({ user: null });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Auth-only middleware (no db check) for setup/settings routes
|
|
110
|
+
function requireAuth(req, res, next) {
|
|
111
|
+
if (!req.session || !req.session.user) {
|
|
112
|
+
return res.status(401).json({ error: 'Unauthorized.' });
|
|
113
|
+
}
|
|
114
|
+
next();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// GET /api/setup-status
|
|
118
|
+
app.get('/api/setup-status', requireAuth, (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
if (!existsSync(CONFIG_PATH)) return res.json({ needsSetup: true });
|
|
121
|
+
const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
122
|
+
res.json({ needsSetup: !cfg.setup_complete });
|
|
123
|
+
} catch {
|
|
124
|
+
res.json({ needsSetup: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// POST /api/setup
|
|
129
|
+
app.post('/api/setup', requireAuth, (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const { licenseKey, geminiApiKey, embeddingChoice, platform } = req.body;
|
|
132
|
+
if (!licenseKey || !geminiApiKey) {
|
|
133
|
+
return res.status(400).json({ error: 'License key and Gemini API key are required.' });
|
|
134
|
+
}
|
|
135
|
+
mkdirSync(STENO_DIR, { recursive: true });
|
|
136
|
+
writeFileSync(LICENSE_PATH, licenseKey, { mode: 0o600 });
|
|
137
|
+
|
|
138
|
+
let existing = {};
|
|
139
|
+
if (existsSync(CONFIG_PATH)) {
|
|
140
|
+
try { existing = JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); } catch {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const config = {
|
|
144
|
+
...existing,
|
|
145
|
+
database: DB_PATH,
|
|
146
|
+
extractionProvider: 'openai',
|
|
147
|
+
extractionModel: 'gemini-2.5-flash-lite',
|
|
148
|
+
extractionApiKey: geminiApiKey,
|
|
149
|
+
extractionApiBase: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
150
|
+
embeddingProvider: embeddingChoice || 'none',
|
|
151
|
+
embeddingModel: embeddingChoice === 'local' ? 'Qwen3-Embedding-0.6B-Q8_0' : '',
|
|
152
|
+
embeddingApiKey: '',
|
|
153
|
+
embeddingApiBase: '',
|
|
154
|
+
systemIntegration: platform || 'ductor',
|
|
155
|
+
logWatchPaths: [path.join(os.homedir(), '.claude', 'projects')],
|
|
156
|
+
ductorSessionsWatch: true,
|
|
157
|
+
setup_complete: true,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
161
|
+
res.json({ ok: true });
|
|
162
|
+
} catch (err) {
|
|
163
|
+
res.status(500).json({ error: err.message });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// GET /api/settings
|
|
168
|
+
app.get('/api/settings', requireAuth, (req, res) => {
|
|
169
|
+
try {
|
|
170
|
+
if (!existsSync(CONFIG_PATH)) return res.json({ settings: null });
|
|
171
|
+
const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
172
|
+
const masked = cfg.extractionApiKey
|
|
173
|
+
? '••••••••' + cfg.extractionApiKey.slice(-4)
|
|
174
|
+
: '';
|
|
175
|
+
res.json({
|
|
176
|
+
settings: {
|
|
177
|
+
geminiApiKeyMasked: masked,
|
|
178
|
+
embeddingProvider: cfg.embeddingProvider || 'none',
|
|
179
|
+
systemIntegration: cfg.systemIntegration || 'ductor',
|
|
180
|
+
extractionModel: cfg.extractionModel || 'gemini-2.5-flash-lite',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
res.status(500).json({ error: err.message });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// PUT /api/settings
|
|
189
|
+
app.put('/api/settings', requireAuth, (req, res) => {
|
|
190
|
+
try {
|
|
191
|
+
let existing = {};
|
|
192
|
+
if (existsSync(CONFIG_PATH)) {
|
|
193
|
+
try { existing = JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); } catch {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { geminiApiKey, embeddingProvider, systemIntegration } = req.body;
|
|
197
|
+
if (geminiApiKey && geminiApiKey.length >= 10) {
|
|
198
|
+
existing.extractionApiKey = geminiApiKey;
|
|
199
|
+
}
|
|
200
|
+
if (embeddingProvider) {
|
|
201
|
+
existing.embeddingProvider = embeddingProvider;
|
|
202
|
+
existing.embeddingModel = embeddingProvider === 'local' ? 'Qwen3-Embedding-0.6B-Q8_0' : '';
|
|
203
|
+
}
|
|
204
|
+
if (systemIntegration) {
|
|
205
|
+
existing.systemIntegration = systemIntegration;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(existing, null, 2));
|
|
209
|
+
res.json({ ok: true });
|
|
210
|
+
} catch (err) {
|
|
211
|
+
res.status(500).json({ error: err.message });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Auth middleware for /api routes
|
|
216
|
+
app.use('/api', (req, res, next) => {
|
|
217
|
+
if (!req.session || !req.session.user) {
|
|
218
|
+
return res.status(401).json({ error: 'Unauthorized.' });
|
|
219
|
+
}
|
|
220
|
+
if (!db) {
|
|
221
|
+
return res.status(503).json({ error: 'Database not available.' });
|
|
222
|
+
}
|
|
223
|
+
next();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// GET /api/stats
|
|
227
|
+
app.get('/api/stats', (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
const totalRow = db.prepare('SELECT COUNT(*) as count FROM memories').get();
|
|
230
|
+
const activeRow = db.prepare('SELECT COUNT(*) as count FROM memories WHERE is_active = 1').get();
|
|
231
|
+
const recentRow = db.prepare(
|
|
232
|
+
"SELECT COUNT(*) as count FROM memories WHERE is_active = 1 AND created_at >= datetime('now', '-7 days')"
|
|
233
|
+
).get();
|
|
234
|
+
|
|
235
|
+
const byCategory = db
|
|
236
|
+
.prepare('SELECT category, COUNT(*) as count FROM memories WHERE is_active = 1 GROUP BY category ORDER BY count DESC')
|
|
237
|
+
.all();
|
|
238
|
+
|
|
239
|
+
const byTemperature = db
|
|
240
|
+
.prepare('SELECT temperature, COUNT(*) as count FROM memories WHERE is_active = 1 GROUP BY temperature ORDER BY count DESC')
|
|
241
|
+
.all();
|
|
242
|
+
|
|
243
|
+
const byAgent = db
|
|
244
|
+
.prepare('SELECT agent_id, COUNT(*) as count FROM memories WHERE is_active = 1 GROUP BY agent_id ORDER BY count DESC')
|
|
245
|
+
.all();
|
|
246
|
+
|
|
247
|
+
const archivedRow = db.prepare('SELECT COUNT(*) as count FROM memories WHERE is_active = 0').get();
|
|
248
|
+
|
|
249
|
+
res.json({
|
|
250
|
+
total: totalRow.count,
|
|
251
|
+
active: activeRow.count,
|
|
252
|
+
archived: archivedRow.count,
|
|
253
|
+
recentCount: recentRow.count,
|
|
254
|
+
byCategory,
|
|
255
|
+
byTemperature,
|
|
256
|
+
byAgent,
|
|
257
|
+
});
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error('Stats error:', err);
|
|
260
|
+
res.status(500).json({ error: err.message });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// GET /api/memories
|
|
265
|
+
app.get('/api/memories', (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const { category, temperature, agent_id, page = 1, limit = 20, q, archived, date_from } = req.query;
|
|
268
|
+
const isArchived = archived === 'true';
|
|
269
|
+
const activeFilter = isArchived ? 0 : 1;
|
|
270
|
+
const offset = (parseInt(page) - 1) * parseInt(limit);
|
|
271
|
+
const lim = parseInt(limit);
|
|
272
|
+
|
|
273
|
+
const SELECT_COLS = `
|
|
274
|
+
m.id, m.category, m.content, m.subject, m.confidence, m.importance,
|
|
275
|
+
m.tags, m.source_channel, m.agent_id, m.temperature, m.created_at,
|
|
276
|
+
m.updated_at, m.is_active, m.access_count, m.last_accessed_at,
|
|
277
|
+
m.supersedes_id, m.superseded_by, m.extracted_by
|
|
278
|
+
`;
|
|
279
|
+
|
|
280
|
+
if (q && q.trim()) {
|
|
281
|
+
// FTS search
|
|
282
|
+
const searchTerm = q.trim();
|
|
283
|
+
let ftsQuery = `
|
|
284
|
+
SELECT ${SELECT_COLS}
|
|
285
|
+
FROM memories m
|
|
286
|
+
JOIN memories_fts fts ON m.id = fts.rowid
|
|
287
|
+
WHERE memories_fts MATCH ? AND m.is_active = ?
|
|
288
|
+
`;
|
|
289
|
+
const params = [searchTerm, activeFilter];
|
|
290
|
+
|
|
291
|
+
if (category && category !== 'all') {
|
|
292
|
+
ftsQuery += ` AND m.category = ?`;
|
|
293
|
+
params.push(category);
|
|
294
|
+
}
|
|
295
|
+
if (temperature && temperature !== 'all') {
|
|
296
|
+
ftsQuery += ` AND m.temperature = ?`;
|
|
297
|
+
params.push(temperature);
|
|
298
|
+
}
|
|
299
|
+
if (agent_id && agent_id !== 'all') {
|
|
300
|
+
ftsQuery += ` AND m.agent_id = ?`;
|
|
301
|
+
params.push(agent_id);
|
|
302
|
+
}
|
|
303
|
+
if (date_from) {
|
|
304
|
+
ftsQuery += ` AND m.created_at >= ?`;
|
|
305
|
+
params.push(date_from);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
ftsQuery += ` ORDER BY rank LIMIT ? OFFSET ?`;
|
|
309
|
+
params.push(lim, offset);
|
|
310
|
+
|
|
311
|
+
let memories;
|
|
312
|
+
try {
|
|
313
|
+
memories = db.prepare(ftsQuery).all(...params);
|
|
314
|
+
} catch (ftsErr) {
|
|
315
|
+
// FTS table might not exist or rowid join might fail - fall back to LIKE search
|
|
316
|
+
console.warn('FTS search failed, falling back to LIKE:', ftsErr.message);
|
|
317
|
+
const likePattern = `%${searchTerm}%`;
|
|
318
|
+
let fallbackQuery = `
|
|
319
|
+
SELECT ${SELECT_COLS}
|
|
320
|
+
FROM memories m
|
|
321
|
+
WHERE m.is_active = ? AND (m.content LIKE ? OR m.subject LIKE ? OR m.tags LIKE ?)
|
|
322
|
+
`;
|
|
323
|
+
const fbParams = [activeFilter, likePattern, likePattern, likePattern];
|
|
324
|
+
if (category && category !== 'all') { fallbackQuery += ` AND m.category = ?`; fbParams.push(category); }
|
|
325
|
+
if (temperature && temperature !== 'all') { fallbackQuery += ` AND m.temperature = ?`; fbParams.push(temperature); }
|
|
326
|
+
if (agent_id && agent_id !== 'all') { fallbackQuery += ` AND m.agent_id = ?`; fbParams.push(agent_id); }
|
|
327
|
+
if (date_from) { fallbackQuery += ` AND m.created_at >= ?`; fbParams.push(date_from); }
|
|
328
|
+
fallbackQuery += ` ORDER BY m.created_at DESC LIMIT ? OFFSET ?`;
|
|
329
|
+
fbParams.push(lim, offset);
|
|
330
|
+
memories = db.prepare(fallbackQuery).all(...fbParams);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Count for pagination
|
|
334
|
+
let countQuery = `
|
|
335
|
+
SELECT COUNT(*) as count
|
|
336
|
+
FROM memories m
|
|
337
|
+
WHERE m.is_active = ? AND (m.content LIKE ? OR m.subject LIKE ? OR m.tags LIKE ?)
|
|
338
|
+
`;
|
|
339
|
+
const likePattern = `%${searchTerm}%`;
|
|
340
|
+
const countParams = [activeFilter, likePattern, likePattern, likePattern];
|
|
341
|
+
if (category && category !== 'all') { countQuery += ` AND m.category = ?`; countParams.push(category); }
|
|
342
|
+
if (temperature && temperature !== 'all') { countQuery += ` AND m.temperature = ?`; countParams.push(temperature); }
|
|
343
|
+
if (agent_id && agent_id !== 'all') { countQuery += ` AND m.agent_id = ?`; countParams.push(agent_id); }
|
|
344
|
+
if (date_from) { countQuery += ` AND m.created_at >= ?`; countParams.push(date_from); }
|
|
345
|
+
const countRow = db.prepare(countQuery).get(...countParams);
|
|
346
|
+
|
|
347
|
+
return res.json({ memories, total: countRow.count, page: parseInt(page) });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Regular filtered query
|
|
351
|
+
let query = `SELECT ${SELECT_COLS} FROM memories m WHERE m.is_active = ?`;
|
|
352
|
+
const params = [activeFilter];
|
|
353
|
+
|
|
354
|
+
if (category && category !== 'all') {
|
|
355
|
+
query += ` AND m.category = ?`;
|
|
356
|
+
params.push(category);
|
|
357
|
+
}
|
|
358
|
+
if (temperature && temperature !== 'all') {
|
|
359
|
+
query += ` AND m.temperature = ?`;
|
|
360
|
+
params.push(temperature);
|
|
361
|
+
}
|
|
362
|
+
if (agent_id && agent_id !== 'all') {
|
|
363
|
+
query += ` AND m.agent_id = ?`;
|
|
364
|
+
params.push(agent_id);
|
|
365
|
+
}
|
|
366
|
+
if (date_from) {
|
|
367
|
+
query += ` AND m.created_at >= ?`;
|
|
368
|
+
params.push(date_from);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
query += ` ORDER BY m.created_at DESC LIMIT ? OFFSET ?`;
|
|
372
|
+
params.push(lim, offset);
|
|
373
|
+
|
|
374
|
+
const memories = db.prepare(query).all(...params);
|
|
375
|
+
|
|
376
|
+
// Count
|
|
377
|
+
let countQuery = `SELECT COUNT(*) as count FROM memories m WHERE m.is_active = ?`;
|
|
378
|
+
const countParams = [activeFilter];
|
|
379
|
+
if (category && category !== 'all') { countQuery += ` AND m.category = ?`; countParams.push(category); }
|
|
380
|
+
if (temperature && temperature !== 'all') { countQuery += ` AND m.temperature = ?`; countParams.push(temperature); }
|
|
381
|
+
if (agent_id && agent_id !== 'all') { countQuery += ` AND m.agent_id = ?`; countParams.push(agent_id); }
|
|
382
|
+
if (date_from) { countQuery += ` AND m.created_at >= ?`; countParams.push(date_from); }
|
|
383
|
+
|
|
384
|
+
const countRow = db.prepare(countQuery).get(...countParams);
|
|
385
|
+
|
|
386
|
+
res.json({ memories, total: countRow.count, page: parseInt(page) });
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error('Memories error:', err);
|
|
389
|
+
res.status(500).json({ error: err.message });
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// GET /api/memories/:id
|
|
394
|
+
app.get('/api/memories/:id', (req, res) => {
|
|
395
|
+
try {
|
|
396
|
+
const memory = db.prepare(`
|
|
397
|
+
SELECT id, category, content, subject, confidence, importance,
|
|
398
|
+
tags, source_channel, agent_id, temperature, created_at,
|
|
399
|
+
updated_at, is_active, access_count, last_accessed_at,
|
|
400
|
+
supersedes_id, superseded_by, extracted_by
|
|
401
|
+
FROM memories WHERE id = ?
|
|
402
|
+
`).get(req.params.id);
|
|
403
|
+
|
|
404
|
+
if (!memory) {
|
|
405
|
+
return res.status(404).json({ error: 'Memory not found.' });
|
|
406
|
+
}
|
|
407
|
+
res.json(memory);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.error('Get memory error:', err);
|
|
410
|
+
res.status(500).json({ error: err.message });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// DELETE /api/memories/:id (soft delete)
|
|
415
|
+
app.delete('/api/memories/:id', (req, res) => {
|
|
416
|
+
try {
|
|
417
|
+
const result = db.prepare(
|
|
418
|
+
`UPDATE memories SET is_active = 0, updated_at = datetime('now') WHERE id = ?`
|
|
419
|
+
).run(req.params.id);
|
|
420
|
+
|
|
421
|
+
if (result.changes === 0) {
|
|
422
|
+
return res.status(404).json({ error: 'Memory not found.' });
|
|
423
|
+
}
|
|
424
|
+
res.json({ ok: true });
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.error('Delete memory error:', err);
|
|
427
|
+
res.status(500).json({ error: err.message });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// PUT /api/memories/:id/restore
|
|
432
|
+
app.put('/api/memories/:id/restore', (req, res) => {
|
|
433
|
+
try {
|
|
434
|
+
const result = db.prepare(
|
|
435
|
+
`UPDATE memories SET is_active = 1, temperature = 'hot',
|
|
436
|
+
last_accessed_at = datetime('now'), access_count = access_count + 1,
|
|
437
|
+
updated_at = datetime('now') WHERE id = ?`
|
|
438
|
+
).run(req.params.id);
|
|
439
|
+
if (result.changes === 0) return res.status(404).json({ error: 'Memory not found.' });
|
|
440
|
+
res.json({ ok: true });
|
|
441
|
+
} catch (err) {
|
|
442
|
+
console.error('Restore error:', err);
|
|
443
|
+
res.status(500).json({ error: err.message });
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// GET /api/pinned
|
|
448
|
+
app.get('/api/pinned', (req, res) => {
|
|
449
|
+
try {
|
|
450
|
+
const pinned = db.prepare(`
|
|
451
|
+
SELECT id, memory_id, content, category, agent_id, created_at, updated_at
|
|
452
|
+
FROM pinned_memories
|
|
453
|
+
ORDER BY created_at DESC
|
|
454
|
+
`).all();
|
|
455
|
+
res.json({ pinned });
|
|
456
|
+
} catch (err) {
|
|
457
|
+
console.error('Pinned error:', err);
|
|
458
|
+
res.status(500).json({ error: err.message });
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// GET /api/commitments
|
|
463
|
+
app.get('/api/commitments', (req, res) => {
|
|
464
|
+
try {
|
|
465
|
+
const commitments = db.prepare(`
|
|
466
|
+
SELECT id, commitment, source, state, agent_id, created_at, updated_at
|
|
467
|
+
FROM commitment_ledger
|
|
468
|
+
ORDER BY created_at DESC
|
|
469
|
+
`).all();
|
|
470
|
+
res.json({ commitments });
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.error('Commitments error:', err);
|
|
473
|
+
res.status(500).json({ error: err.message });
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
app.listen(PORT, HOST, () => {
|
|
478
|
+
console.log(`Stenotype Dashboard running at http://localhost:${PORT}`);
|
|
479
|
+
});
|
package/dist/stenotype.jsc
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stenotype",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Automatic memory capture and recall for AI agent workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/index.cjs",
|
|
12
|
-
"dist/stenotype.jsc"
|
|
12
|
+
"dist/stenotype.jsc",
|
|
13
|
+
"dist/dashboard"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "tsc",
|
|
@@ -19,7 +20,11 @@
|
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"@anthropic-ai/sdk": "^0.90.0",
|
|
23
|
+
"bcryptjs": "^2.4.3",
|
|
22
24
|
"bytenode": "^1.5.7",
|
|
25
|
+
"express": "^4.18.2",
|
|
26
|
+
"express-session": "^1.17.3",
|
|
27
|
+
"keytar": "^7.9.0",
|
|
23
28
|
"openai": "^4.78.1",
|
|
24
29
|
"sqlite-vec": "^0.1.7",
|
|
25
30
|
"sqlite-vec-linux-x64": "^0.1.7"
|