waymark-hub 1.0.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/LICENSE +21 -0
- package/README.md +176 -0
- package/dashboard/README.md +71 -0
- package/dashboard/index.html +520 -0
- package/dashboard/server.mjs +343 -0
- package/dashboard/vendor/tailwind.js +83 -0
- package/dist/cli/benchmark.d.ts +3 -0
- package/dist/cli/benchmark.d.ts.map +1 -0
- package/dist/cli/benchmark.js +222 -0
- package/dist/cli/benchmark.js.map +1 -0
- package/dist/cli/waymark.d.ts +3 -0
- package/dist/cli/waymark.d.ts.map +1 -0
- package/dist/cli/waymark.js +216 -0
- package/dist/cli/waymark.js.map +1 -0
- package/dist/context/builder.d.ts +67 -0
- package/dist/context/builder.d.ts.map +1 -0
- package/dist/context/builder.js +270 -0
- package/dist/context/builder.js.map +1 -0
- package/dist/db/client.d.ts +5 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +67 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/migrations/001_initial.sql +85 -0
- package/dist/db/migrations/002_usage_experiments.sql +52 -0
- package/dist/db/migrations/003_agent_identity.sql +37 -0
- package/dist/db/migrations/004_memory_lifecycle.sql +25 -0
- package/dist/db/migrations/005_task_coordination.sql +17 -0
- package/dist/db/schema.d.ts +132 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +3 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +164 -0
- package/dist/server.js.map +1 -0
- package/dist/telemetry/tokens.d.ts +9 -0
- package/dist/telemetry/tokens.d.ts.map +1 -0
- package/dist/telemetry/tokens.js +23 -0
- package/dist/telemetry/tokens.js.map +1 -0
- package/dist/tools/agents.d.ts +3 -0
- package/dist/tools/agents.d.ts.map +1 -0
- package/dist/tools/agents.js +126 -0
- package/dist/tools/agents.js.map +1 -0
- package/dist/tools/context.d.ts +3 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +53 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +273 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/projects.d.ts +3 -0
- package/dist/tools/projects.d.ts.map +1 -0
- package/dist/tools/projects.js +92 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/sessions.d.ts +3 -0
- package/dist/tools/sessions.d.ts.map +1 -0
- package/dist/tools/sessions.js +113 -0
- package/dist/tools/sessions.js.map +1 -0
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +304 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/telemetry.d.ts +3 -0
- package/dist/tools/telemetry.d.ts.map +1 -0
- package/dist/tools/telemetry.js +291 -0
- package/dist/tools/telemetry.js.map +1 -0
- package/docs/AGENT_IDENTITY.md +67 -0
- package/docs/BENCHMARKING.md +161 -0
- package/docs/CONTEXT.md +95 -0
- package/docs/MEMORY_LIFECYCLE.md +65 -0
- package/docs/TASK_COORDINATION.md +27 -0
- package/package.json +70 -0
- package/scripts/hooks/session-start-resume.cjs +70 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Waymark Hub — read-only observability dashboard.
|
|
3
|
+
//
|
|
4
|
+
// A standalone viewer for the shared hub DB (projects, tasks, memory, sessions,
|
|
5
|
+
// agents). It opens the SQLite file in READ-ONLY mode, so it can never mutate
|
|
6
|
+
// the shared state agents depend on (atomic task claim, FTS triggers). Reuses
|
|
7
|
+
// the hub's already-installed express + better-sqlite3 — no extra deps.
|
|
8
|
+
// Writes, if ever added, must go through the hub's MCP tools, not this server.
|
|
9
|
+
|
|
10
|
+
import express from 'express';
|
|
11
|
+
import Database from 'better-sqlite3';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
|
|
22
|
+
// Same DB resolution as the hub itself (DB_PATH → WAYMARK_HOME → install dir → ~/.waymark).
|
|
23
|
+
const { resolveDbPath } = require(path.join(__dirname, '..', 'dist', 'db', 'client.js'));
|
|
24
|
+
const DB_PATH = resolveDbPath();
|
|
25
|
+
const PORT = parseInt(process.env.DASH_PORT || '4747', 10);
|
|
26
|
+
const HOST = '127.0.0.1'; // loopback only
|
|
27
|
+
|
|
28
|
+
const db = new Database(DB_PATH, { readonly: true, fileMustExist: true });
|
|
29
|
+
|
|
30
|
+
/** Parse a column that stores a JSON array/object as text; tolerate plain text. */
|
|
31
|
+
function parseJson(value, fallback) {
|
|
32
|
+
if (value == null || value === '') return fallback;
|
|
33
|
+
try { return JSON.parse(value); } catch { return fallback; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- Optional admin mode ----------------------------------------------------
|
|
37
|
+
// WAYMARK_ADMIN=1 enables a small set of write actions. Writes go through the
|
|
38
|
+
// hub's own MCP tools (in-process client), never raw SQL — so task_update /
|
|
39
|
+
// memory_set_status semantics (completed_at, FTS, supersede) stay intact.
|
|
40
|
+
const ADMIN = process.env.WAYMARK_ADMIN === '1';
|
|
41
|
+
let adminClient = null;
|
|
42
|
+
async function initAdmin() {
|
|
43
|
+
process.env.HUB_TOOLS = 'full';
|
|
44
|
+
const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
|
|
45
|
+
const { InMemoryTransport } = require('@modelcontextprotocol/sdk/inMemory.js');
|
|
46
|
+
const { createMcpServer } = require(path.join(__dirname, '..', 'dist', 'server.js'));
|
|
47
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
48
|
+
const client = new Client({ name: 'waymark-dashboard-admin', version: '1.0.0' });
|
|
49
|
+
const mcpServer = createMcpServer();
|
|
50
|
+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
|
|
51
|
+
return client;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function adminTool(res, name, args) {
|
|
55
|
+
if (!adminClient) return res.status(403).json({ error: 'admin mode is off (set WAYMARK_ADMIN=1)' });
|
|
56
|
+
try {
|
|
57
|
+
const result = await adminClient.callTool({ name, arguments: args });
|
|
58
|
+
const text = result.content?.find(c => c.type === 'text')?.text ?? '';
|
|
59
|
+
if (result.isError) return res.status(400).json({ error: text });
|
|
60
|
+
res.json({ ok: true, result: text });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
res.status(500).json({ error: String(e.message || e) });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const app = express();
|
|
67
|
+
|
|
68
|
+
app.get('/api/capabilities', (_req, res) => {
|
|
69
|
+
res.json({ admin: ADMIN, db_path: DB_PATH });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Cheap change detection for live refresh: data_version increments whenever
|
|
73
|
+
// another connection commits to the DB.
|
|
74
|
+
app.get('/api/stats', (_req, res) => {
|
|
75
|
+
res.json({
|
|
76
|
+
data_version: db.pragma('data_version', { simple: true }),
|
|
77
|
+
now: new Date().toISOString(),
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Trail: the handoff timeline for a project — sessions in reverse chronology
|
|
82
|
+
// plus the active auto-handoff (what the next agent should pick up).
|
|
83
|
+
app.get('/api/trail', (req, res) => {
|
|
84
|
+
const projectId = req.query.project_id || null;
|
|
85
|
+
const where = projectId ? 'WHERE s.project_id = ?' : '';
|
|
86
|
+
const params = projectId ? [projectId] : [];
|
|
87
|
+
const sessions = db.prepare(`
|
|
88
|
+
SELECT s.id, s.project_id, s.started_at, s.ended_at, s.summary, s.outcome,
|
|
89
|
+
s.agent_id, s.provider, s.model, s.client, s.surface,
|
|
90
|
+
s.files_touched, s.commits_made,
|
|
91
|
+
p.name AS project_name, a.display_name AS agent_name
|
|
92
|
+
FROM sessions s
|
|
93
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
94
|
+
LEFT JOIN agents a ON a.id = s.agent_id
|
|
95
|
+
${where}
|
|
96
|
+
ORDER BY COALESCE(s.ended_at, s.started_at) DESC
|
|
97
|
+
LIMIT 100`).all(...params).map(r => ({
|
|
98
|
+
...r,
|
|
99
|
+
files_touched: parseJson(r.files_touched, []),
|
|
100
|
+
commits_made: parseJson(r.commits_made, []),
|
|
101
|
+
}));
|
|
102
|
+
const handoffs = db.prepare(`
|
|
103
|
+
SELECT m.id, m.project_id, m.name, m.description, m.body, m.status,
|
|
104
|
+
m.updated_at, m.origin_session, m.created_by_agent,
|
|
105
|
+
a.display_name AS agent_name, p.name AS project_name
|
|
106
|
+
FROM memory_nodes m
|
|
107
|
+
LEFT JOIN agents a ON a.id = m.created_by_agent
|
|
108
|
+
LEFT JOIN projects p ON p.id = m.project_id
|
|
109
|
+
WHERE m.type = 'handoff' ${projectId ? 'AND m.project_id = ?' : ''}
|
|
110
|
+
ORDER BY m.updated_at DESC`).all(...params);
|
|
111
|
+
res.json({ sessions, handoffs });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
app.get('/api/overview', (_req, res) => {
|
|
115
|
+
const count = (t) => db.prepare(`SELECT COUNT(*) c FROM ${t}`).get().c;
|
|
116
|
+
res.json({
|
|
117
|
+
db_path: DB_PATH,
|
|
118
|
+
counts: {
|
|
119
|
+
projects: count('projects'),
|
|
120
|
+
tasks: count('tasks'),
|
|
121
|
+
memory: count('memory_nodes'),
|
|
122
|
+
sessions: count('sessions'),
|
|
123
|
+
agents: count('agents'),
|
|
124
|
+
experiments: count('experiments'),
|
|
125
|
+
},
|
|
126
|
+
tasks_by_status: db.prepare(
|
|
127
|
+
`SELECT status, COUNT(*) c FROM tasks GROUP BY status ORDER BY c DESC`,
|
|
128
|
+
).all(),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
app.get('/api/projects', (_req, res) => {
|
|
133
|
+
res.json(db.prepare(
|
|
134
|
+
`SELECT id, name, root_path, stack, status, description, created_at, updated_at
|
|
135
|
+
FROM projects ORDER BY updated_at DESC, name`,
|
|
136
|
+
).all());
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
app.get('/api/tasks', (req, res) => {
|
|
140
|
+
const where = [];
|
|
141
|
+
const params = [];
|
|
142
|
+
if (req.query.project_id) { where.push('t.project_id = ?'); params.push(req.query.project_id); }
|
|
143
|
+
if (req.query.status) { where.push('t.status = ?'); params.push(req.query.status); }
|
|
144
|
+
const sql = `
|
|
145
|
+
SELECT t.id, t.project_id, t.title, t.description, t.status, t.priority,
|
|
146
|
+
t.created_by, t.assigned_to, t.assigned_agent_id, t.claimed_by_agent,
|
|
147
|
+
t.claimed_at, t.blocker, t.progress, t.required_capabilities,
|
|
148
|
+
t.created_at, t.updated_at, t.completed_at,
|
|
149
|
+
p.name AS project_name, a.display_name AS claimed_agent_name
|
|
150
|
+
FROM tasks t
|
|
151
|
+
LEFT JOIN projects p ON p.id = t.project_id
|
|
152
|
+
LEFT JOIN agents a ON a.id = t.claimed_by_agent
|
|
153
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
154
|
+
ORDER BY t.priority DESC, t.created_at DESC`;
|
|
155
|
+
const rows = db.prepare(sql).all(...params).map(r => ({
|
|
156
|
+
...r,
|
|
157
|
+
required_capabilities: parseJson(r.required_capabilities, []),
|
|
158
|
+
}));
|
|
159
|
+
res.json(rows);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app.get('/api/memory', (req, res) => {
|
|
163
|
+
const where = [];
|
|
164
|
+
const params = [];
|
|
165
|
+
if (req.query.project_id) { where.push('project_id = ?'); params.push(req.query.project_id); }
|
|
166
|
+
if (req.query.type) { where.push('type = ?'); params.push(req.query.type); }
|
|
167
|
+
if (req.query.status) { where.push('status = ?'); params.push(req.query.status); }
|
|
168
|
+
if (req.query.q) {
|
|
169
|
+
where.push('(name LIKE ? OR description LIKE ? OR body LIKE ? OR tags LIKE ?)');
|
|
170
|
+
const like = `%${req.query.q}%`;
|
|
171
|
+
params.push(like, like, like, like);
|
|
172
|
+
}
|
|
173
|
+
const sql = `
|
|
174
|
+
SELECT id, project_id, name, description, type, body, tags, status,
|
|
175
|
+
importance, confidence, surface, created_by_agent, created_at, updated_at
|
|
176
|
+
FROM memory_nodes
|
|
177
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
178
|
+
ORDER BY importance DESC, updated_at DESC`;
|
|
179
|
+
const rows = db.prepare(sql).all(...params).map(r => ({
|
|
180
|
+
...r,
|
|
181
|
+
tags: parseJson(r.tags, []),
|
|
182
|
+
}));
|
|
183
|
+
res.json(rows);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
app.get('/api/sessions', (req, res) => {
|
|
187
|
+
const where = [];
|
|
188
|
+
const params = [];
|
|
189
|
+
if (req.query.project_id) { where.push('s.project_id = ?'); params.push(req.query.project_id); }
|
|
190
|
+
const sql = `
|
|
191
|
+
SELECT s.id, s.project_id, s.surface, s.started_at, s.ended_at, s.summary,
|
|
192
|
+
s.files_touched, s.commits_made, s.outcome, s.agent_id,
|
|
193
|
+
s.provider, s.model, s.client,
|
|
194
|
+
p.name AS project_name, a.display_name AS agent_name
|
|
195
|
+
FROM sessions s
|
|
196
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
197
|
+
LEFT JOIN agents a ON a.id = s.agent_id
|
|
198
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
199
|
+
ORDER BY s.started_at DESC`;
|
|
200
|
+
const rows = db.prepare(sql).all(...params).map(r => ({
|
|
201
|
+
...r,
|
|
202
|
+
files_touched: parseJson(r.files_touched, []),
|
|
203
|
+
commits_made: parseJson(r.commits_made, []),
|
|
204
|
+
}));
|
|
205
|
+
res.json(rows);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
app.get('/api/agents', (_req, res) => {
|
|
209
|
+
const rows = db.prepare(
|
|
210
|
+
`SELECT id, display_name, provider, model, client, client_version,
|
|
211
|
+
capabilities, status, created_at, updated_at
|
|
212
|
+
FROM agents ORDER BY updated_at DESC`,
|
|
213
|
+
).all().map(r => ({ ...r, capabilities: parseJson(r.capabilities, []) }));
|
|
214
|
+
res.json(rows);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.get('/api/experiments', (_req, res) => {
|
|
218
|
+
res.json(db.prepare(
|
|
219
|
+
`SELECT e.id, e.project_id, e.name, e.description, e.scenario, e.status,
|
|
220
|
+
e.target_runs, e.created_at, e.updated_at, p.name AS project_name
|
|
221
|
+
FROM experiments e
|
|
222
|
+
LEFT JOIN projects p ON p.id = e.project_id
|
|
223
|
+
ORDER BY e.created_at DESC`,
|
|
224
|
+
).all());
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ---- Optional Russian translation (opt-in, cached) -------------------------
|
|
228
|
+
// Translates English hub content on demand via the user's existing DeepSeek
|
|
229
|
+
// key, reusing the same resolution dsc uses (env → ~/.deepseek-code/settings.json).
|
|
230
|
+
// The key stays server-side; every unique string is translated once and cached
|
|
231
|
+
// to disk, so repeat views cost nothing. Off unless the UI requests it.
|
|
232
|
+
|
|
233
|
+
const CACHE_DIR = path.join(__dirname, '.cache');
|
|
234
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'translations.json');
|
|
235
|
+
let trCache = {};
|
|
236
|
+
try { trCache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); } catch { trCache = {}; }
|
|
237
|
+
let cacheDirty = false;
|
|
238
|
+
function persistCache() {
|
|
239
|
+
if (!cacheDirty) return;
|
|
240
|
+
try { fs.mkdirSync(CACHE_DIR, { recursive: true }); fs.writeFileSync(CACHE_FILE, JSON.stringify(trCache)); cacheDirty = false; } catch { /* non-fatal */ }
|
|
241
|
+
}
|
|
242
|
+
setInterval(persistCache, 5000).unref();
|
|
243
|
+
|
|
244
|
+
function translateConfig() {
|
|
245
|
+
let settings = {};
|
|
246
|
+
try { settings = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.deepseek-code', 'settings.json'), 'utf8')); } catch { /* none */ }
|
|
247
|
+
const apiKey = (process.env.DEEPSEEK_API_KEY || settings.apiKey || '').trim();
|
|
248
|
+
const baseUrl = (process.env.DEEPSEEK_BASE_URL || settings.baseUrl || 'https://api.deepseek.com').replace(/\/$/, '');
|
|
249
|
+
const model = process.env.TRANSLATE_MODEL || 'deepseek-v4-flash';
|
|
250
|
+
return { apiKey, baseUrl, model };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const hash = (s) => crypto.createHash('sha1').update(s).digest('hex');
|
|
254
|
+
|
|
255
|
+
async function translateBatch(texts, cfg) {
|
|
256
|
+
const prompt = 'Translate each string in the following JSON array to Russian. '
|
|
257
|
+
+ 'Return ONLY a JSON array of strings, same length and order. Translate prose only; '
|
|
258
|
+
+ 'keep code, identifiers, slugs, file paths, URLs, and IDs unchanged.\n\n'
|
|
259
|
+
+ JSON.stringify(texts);
|
|
260
|
+
const resp = await fetch(`${cfg.baseUrl}/chat/completions`, {
|
|
261
|
+
method: 'POST',
|
|
262
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg.apiKey}` },
|
|
263
|
+
body: JSON.stringify({
|
|
264
|
+
model: cfg.model,
|
|
265
|
+
temperature: 0,
|
|
266
|
+
messages: [
|
|
267
|
+
{ role: 'system', content: 'You are a precise translator. Output strictly valid JSON, nothing else.' },
|
|
268
|
+
{ role: 'user', content: prompt },
|
|
269
|
+
],
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
if (!resp.ok) throw new Error(`translate API ${resp.status}`);
|
|
273
|
+
const data = await resp.json();
|
|
274
|
+
let content = data.choices?.[0]?.message?.content ?? '';
|
|
275
|
+
content = content.replace(/^```(?:json)?\s*|\s*```$/g, '').trim();
|
|
276
|
+
const arr = JSON.parse(content);
|
|
277
|
+
if (!Array.isArray(arr) || arr.length !== texts.length) throw new Error('translation shape mismatch');
|
|
278
|
+
return arr.map(String);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
app.use(express.json({ limit: '4mb' }));
|
|
282
|
+
|
|
283
|
+
app.post('/api/translate', async (req, res) => {
|
|
284
|
+
const texts = Array.isArray(req.body?.texts) ? req.body.texts.filter(t => typeof t === 'string') : [];
|
|
285
|
+
const cfg = translateConfig();
|
|
286
|
+
const out = {};
|
|
287
|
+
const missing = [];
|
|
288
|
+
for (const t of texts) {
|
|
289
|
+
const k = hash(t);
|
|
290
|
+
if (trCache[k] != null) out[t] = trCache[k];
|
|
291
|
+
else if (!missing.includes(t)) missing.push(t);
|
|
292
|
+
}
|
|
293
|
+
if (missing.length === 0) { process.stdout.write(`[translate] ${texts.length} texts, all cached\n`); return res.json({ translations: out }); }
|
|
294
|
+
if (!cfg.apiKey) { process.stdout.write('[translate] NO API KEY (set DEEPSEEK_API_KEY or apiKey in ~/.deepseek-code/settings.json)\n'); return res.json({ translations: out, error: 'no_key' }); }
|
|
295
|
+
try {
|
|
296
|
+
process.stdout.write(`[translate] calling ${cfg.baseUrl} model=${cfg.model} for ${missing.length} new texts…\n`);
|
|
297
|
+
const translated = await translateBatch(missing, cfg);
|
|
298
|
+
missing.forEach((src, i) => { trCache[hash(src)] = translated[i]; out[src] = translated[i]; });
|
|
299
|
+
cacheDirty = true; persistCache();
|
|
300
|
+
process.stdout.write(`[translate] ok (${missing.length} translated, ${Object.keys(trCache).length} cached total)\n`);
|
|
301
|
+
res.json({ translations: out });
|
|
302
|
+
} catch (e) {
|
|
303
|
+
process.stdout.write(`[translate] ERROR: ${String(e.message || e)}\n`);
|
|
304
|
+
res.json({ translations: out, error: String(e.message || e) });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Admin write endpoints (403 unless WAYMARK_ADMIN=1). Statuses are validated
|
|
309
|
+
// by the hub tools' own schemas.
|
|
310
|
+
app.post('/api/admin/task-status', (req, res) => {
|
|
311
|
+
const { id, status } = req.body ?? {};
|
|
312
|
+
if (!id || !status) return res.status(400).json({ error: 'id and status are required' });
|
|
313
|
+
adminTool(res, 'task_update', { id, status });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
app.post('/api/admin/memory-status', (req, res) => {
|
|
317
|
+
const { id, status } = req.body ?? {};
|
|
318
|
+
if (!id || !status) return res.status(400).json({ error: 'id and status are required' });
|
|
319
|
+
adminTool(res, 'memory_set_status', { id, status });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
app.use(express.static(__dirname));
|
|
323
|
+
|
|
324
|
+
if (ADMIN) {
|
|
325
|
+
adminClient = await initAdmin();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const server = app.listen(PORT, HOST, () => {
|
|
329
|
+
process.stdout.write(`Waymark dashboard (${ADMIN ? 'ADMIN mode' : 'read-only'}) → http://${HOST}:${PORT}\n`);
|
|
330
|
+
process.stdout.write(`Reading: ${DB_PATH}\n`);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
server.on('error', (err) => {
|
|
334
|
+
if (err.code === 'EADDRINUSE') {
|
|
335
|
+
process.stderr.write(
|
|
336
|
+
`\n[!] Порт ${PORT} уже занят другим процессом (вероятно, старый дашборд ещё работает).\n` +
|
|
337
|
+
` Останови его или запусти на другом порту: $env:DASH_PORT='4748'; npm run dashboard\n`,
|
|
338
|
+
);
|
|
339
|
+
} else {
|
|
340
|
+
process.stderr.write(`\n[!] Не удалось запустить дашборд: ${err.message}\n`);
|
|
341
|
+
}
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|