stenotype 0.1.1 → 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.
- package/README.md +23 -197
- package/dist/dashboard/public/app.js +1301 -0
- package/dist/dashboard/public/dashboard.html +1966 -0
- package/dist/dashboard/public/login.html +357 -0
- package/dist/dashboard/public/styles.css +2 -0
- package/dist/dashboard/server.mjs +479 -0
- package/dist/stenotype.jsc +0 -0
- package/package.json +23 -44
- package/LICENSE +0 -21
- package/dist/index.js +0 -10
|
@@ -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,62 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stenotype",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Automatic memory capture and recall for AI agent
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Automatic memory capture and recall for AI agent workflows",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
7
|
"bin": {
|
|
8
8
|
"stenotype": "dist/index.cjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/index.cjs",
|
|
12
|
-
"dist/stenotype.jsc"
|
|
12
|
+
"dist/stenotype.jsc",
|
|
13
|
+
"dist/dashboard"
|
|
13
14
|
],
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=20.0.0"
|
|
16
|
-
},
|
|
17
15
|
"scripts": {
|
|
18
16
|
"build": "tsc",
|
|
19
17
|
"build:protected": "node scripts/build-protected.mjs",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"recall": "node dist/index.js recall",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"test:watch": "vitest",
|
|
25
|
-
"test:recall-harness": "vitest run tests/recall-harness.test.ts --reporter=verbose",
|
|
26
|
-
"typecheck": "tsc --noEmit",
|
|
27
|
-
"baseline:report": "node scripts/generate-baseline-report.mjs main ./artifacts/baseline",
|
|
28
|
-
"bench:local-vs-cloud": "node scripts/benchmark-local-vs-cloud.mjs --dataset ./bench/benchmark-dataset.json"
|
|
29
|
-
},
|
|
30
|
-
"openclaw": {
|
|
31
|
-
"extensions": [
|
|
32
|
-
"./index.ts"
|
|
33
|
-
]
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"prepublishOnly": "node scripts/preflight-publish.mjs"
|
|
34
20
|
},
|
|
35
21
|
"dependencies": {
|
|
36
|
-
"@
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"express": "^
|
|
41
|
-
"jose": "^6.2.3",
|
|
22
|
+
"@anthropic-ai/sdk": "^0.90.0",
|
|
23
|
+
"bcryptjs": "^2.4.3",
|
|
24
|
+
"bytenode": "^1.5.7",
|
|
25
|
+
"express": "^4.18.2",
|
|
26
|
+
"express-session": "^1.17.3",
|
|
42
27
|
"keytar": "^7.9.0",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"pgvector": "^0.2.1",
|
|
47
|
-
"prompts": "^2.4.2",
|
|
48
|
-
"bytenode": "^1.5.7"
|
|
28
|
+
"openai": "^4.78.1",
|
|
29
|
+
"sqlite-vec": "^0.1.7",
|
|
30
|
+
"sqlite-vec-linux-x64": "^0.1.7"
|
|
49
31
|
},
|
|
50
32
|
"devDependencies": {
|
|
51
|
-
"@
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
"supertest": "^7.2.2",
|
|
59
|
-
"typescript": "^5.9.3",
|
|
60
|
-
"vitest": "^4.0.18"
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
|
+
"esbuild": "^0.24.0",
|
|
35
|
+
"tsx": "^4.19.0",
|
|
36
|
+
"typescript": "^5.7.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20.0.0"
|
|
61
40
|
}
|
|
62
|
-
}
|
|
41
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Stenotype contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/dist/index.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Stenotype standalone entrypoint (post-OpenClaw port).
|
|
4
|
-
*
|
|
5
|
-
* The original OpenClaw plugin registration lives in index.openclaw.ts.archive
|
|
6
|
-
* for reference. That file depended on `openclaw/plugin-sdk` and assumed an
|
|
7
|
-
* in-process runtime. Ductor runs Stenotype out-of-process as a CLI, so this
|
|
8
|
-
* file simply delegates to the subcommand dispatcher in src/cli.ts.
|
|
9
|
-
*/
|
|
10
|
-
import "./src/cli.js";
|