moflo 4.10.7 → 4.10.9
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/.claude/guidance/shipped/moflo-cli-reference.md +1 -1
- package/.claude/guidance/shipped/moflo-memory-strategy.md +1 -1
- package/.claude/guidance/shipped/moflo-yaml-reference.md +4 -4
- package/.claude/skills/memory-optimization/SKILL.md +1 -1
- package/.claude/skills/memory-patterns/SKILL.md +3 -3
- package/.claude/skills/vector-search/SKILL.md +2 -2
- package/README.md +5 -5
- package/bin/hooks.mjs +3 -2
- package/bin/index-all.mjs +3 -2
- package/bin/index-guidance.mjs +4 -4
- package/bin/lib/daemon-port.mjs +66 -0
- package/bin/lib/process-manager.mjs +3 -3
- package/dist/src/cli/commands/daemon.js +31 -10
- package/dist/src/cli/commands/doctor-checks-config.js +182 -10
- package/dist/src/cli/commands/doctor-fixes.js +208 -3
- package/dist/src/cli/commands/doctor-registry.js +16 -1
- package/dist/src/cli/commands/memory.js +8 -8
- package/dist/src/cli/commands/neural.js +8 -6
- package/dist/src/cli/config/moflo-config.js +68 -3
- package/dist/src/cli/index.js +18 -19
- package/dist/src/cli/init/moflo-yaml-template.js +1 -1
- package/dist/src/cli/mcp-server.js +59 -10
- package/dist/src/cli/mcp-tools/memory-tools.js +46 -27
- package/dist/src/cli/memory/auto-memory-bridge.js +1 -1
- package/dist/src/cli/memory/controllers/attestation-log.js +1 -1
- package/dist/src/cli/memory/controllers/causal-graph.js +1 -1
- package/dist/src/cli/memory/daemon-write-client.js +178 -49
- package/dist/src/cli/memory/database-provider.js +58 -3
- package/dist/src/cli/memory/intelligence.js +54 -26
- package/dist/src/cli/memory/memory-initializer.js +21 -11
- package/dist/src/cli/movector/model-router.js +1 -1
- package/dist/src/cli/movector/q-learning-router.js +2 -2
- package/dist/src/cli/services/daemon-dashboard.js +94 -25
- package/dist/src/cli/services/daemon-lock.js +390 -3
- package/dist/src/cli/services/daemon-port.js +252 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/dist/src/cli/config-adapter.js +0 -182
|
@@ -10,31 +10,30 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module v3/cli/intelligence
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
-
import { homedir } from 'node:os';
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
14
|
import { join } from 'node:path';
|
|
16
15
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
16
|
+
import { findProjectRoot } from '../services/project-root.js';
|
|
17
|
+
import { MOFLO_DIR, mofloHomeDir } from '../services/moflo-paths.js';
|
|
17
18
|
// ============================================================================
|
|
18
19
|
// Persistence Configuration
|
|
19
20
|
// ============================================================================
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const NEURAL_SUBDIR = 'neural';
|
|
22
|
+
const PATTERNS_FILE = 'patterns.json';
|
|
23
|
+
const STATS_FILE = 'stats.json';
|
|
24
|
+
// #1152: pre-fix builds wrote neural patterns to `~/.moflo/neural/` whenever
|
|
25
|
+
// cwd lacked `.moflo`. The bleed was silent — every moflo-using project on
|
|
26
|
+
// the machine shared one ReasoningBank. Resolver now anchors on
|
|
27
|
+
// findProjectRoot() like neural-tools.ts (#829); legacy home-dir files are
|
|
28
|
+
// copied (not moved) into the active project on first load so users do not
|
|
29
|
+
// lose history when older co-installed projects still point at the home-dir
|
|
30
|
+
// copy.
|
|
25
31
|
function getDataDir() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (existsSync(join(cwd, '.moflo'))) {
|
|
31
|
-
return localDir;
|
|
32
|
-
}
|
|
33
|
-
return homeDir;
|
|
32
|
+
return join(findProjectRoot(), MOFLO_DIR, NEURAL_SUBDIR);
|
|
33
|
+
}
|
|
34
|
+
function getLegacyDataDir() {
|
|
35
|
+
return join(mofloHomeDir(), NEURAL_SUBDIR);
|
|
34
36
|
}
|
|
35
|
-
/**
|
|
36
|
-
* Ensure the data directory exists
|
|
37
|
-
*/
|
|
38
37
|
function ensureDataDir() {
|
|
39
38
|
const dir = getDataDir();
|
|
40
39
|
if (!existsSync(dir)) {
|
|
@@ -42,17 +41,36 @@ function ensureDataDir() {
|
|
|
42
41
|
}
|
|
43
42
|
return dir;
|
|
44
43
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Get the patterns file path
|
|
47
|
-
*/
|
|
48
44
|
function getPatternsPath() {
|
|
49
|
-
return join(getDataDir(),
|
|
45
|
+
return join(getDataDir(), PATTERNS_FILE);
|
|
50
46
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Get the stats file path
|
|
53
|
-
*/
|
|
54
47
|
function getStatsPath() {
|
|
55
|
-
return join(getDataDir(),
|
|
48
|
+
return join(getDataDir(), STATS_FILE);
|
|
49
|
+
}
|
|
50
|
+
// Latch is intentionally not async-safe — all I/O in this module is
|
|
51
|
+
// synchronous so re-entry only happens via clearIntelligence() in tests.
|
|
52
|
+
let legacyMigrationAttempted = false;
|
|
53
|
+
function migrateLegacyIfNeeded() {
|
|
54
|
+
if (legacyMigrationAttempted)
|
|
55
|
+
return;
|
|
56
|
+
legacyMigrationAttempted = true;
|
|
57
|
+
const legacyDir = getLegacyDataDir();
|
|
58
|
+
const localDir = getDataDir();
|
|
59
|
+
if (legacyDir === localDir)
|
|
60
|
+
return;
|
|
61
|
+
for (const file of [PATTERNS_FILE, STATS_FILE]) {
|
|
62
|
+
const legacy = join(legacyDir, file);
|
|
63
|
+
const local = join(localDir, file);
|
|
64
|
+
if (existsSync(legacy) && !existsSync(local)) {
|
|
65
|
+
try {
|
|
66
|
+
mkdirSync(localDir, { recursive: true });
|
|
67
|
+
copyFileSync(legacy, local);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Best-effort migration; failures fall through to a fresh local store.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
56
74
|
}
|
|
57
75
|
// ============================================================================
|
|
58
76
|
// Default Configuration
|
|
@@ -173,6 +191,7 @@ class LocalReasoningBank {
|
|
|
173
191
|
*/
|
|
174
192
|
loadFromDisk() {
|
|
175
193
|
try {
|
|
194
|
+
migrateLegacyIfNeeded();
|
|
176
195
|
const path = getPatternsPath();
|
|
177
196
|
if (existsSync(path)) {
|
|
178
197
|
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
@@ -207,6 +226,13 @@ class LocalReasoningBank {
|
|
|
207
226
|
* Immediately flush patterns to disk
|
|
208
227
|
*/
|
|
209
228
|
flushToDisk() {
|
|
229
|
+
// Cancel any pending debounced save — we are writing right now and the
|
|
230
|
+
// deferred handler would otherwise fire post-teardown on short-lived
|
|
231
|
+
// processes (test runners hit this as a Windows EPERM during cleanup).
|
|
232
|
+
if (this.saveTimeout) {
|
|
233
|
+
clearTimeout(this.saveTimeout);
|
|
234
|
+
this.saveTimeout = null;
|
|
235
|
+
}
|
|
210
236
|
if (!this.persistenceEnabled || !this.dirty)
|
|
211
237
|
return;
|
|
212
238
|
try {
|
|
@@ -368,6 +394,7 @@ let globalStats = {
|
|
|
368
394
|
*/
|
|
369
395
|
function loadPersistedStats() {
|
|
370
396
|
try {
|
|
397
|
+
migrateLegacyIfNeeded();
|
|
371
398
|
const path = getStatsPath();
|
|
372
399
|
if (existsSync(path)) {
|
|
373
400
|
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
@@ -605,6 +632,7 @@ export function clearIntelligence() {
|
|
|
605
632
|
sonaCoordinator = null;
|
|
606
633
|
reasoningBank = null;
|
|
607
634
|
intelligenceInitialized = false;
|
|
635
|
+
legacyMigrationAttempted = false;
|
|
608
636
|
globalStats = {
|
|
609
637
|
trajectoriesRecorded: 0,
|
|
610
638
|
lastAdaptation: null
|
|
@@ -2220,32 +2220,42 @@ export async function deleteEntry(options) {
|
|
|
2220
2220
|
}
|
|
2221
2221
|
}
|
|
2222
2222
|
/**
|
|
2223
|
-
* Get
|
|
2224
|
-
*
|
|
2223
|
+
* Get memory stats via a single GROUP BY query — namespace counts plus the
|
|
2224
|
+
* number of rows that carry a non-null embedding. One trip to disk; the
|
|
2225
|
+
* server-side aggregation replaces a pre-#1149 client iteration that
|
|
2226
|
+
* fetched 100 000 rows just to count them.
|
|
2227
|
+
*
|
|
2228
|
+
* Throws on DB read errors. Returns a zero shape ONLY when the DB file
|
|
2229
|
+
* doesn't exist yet (the real "empty project" signal) — never swallows a
|
|
2230
|
+
* locked/corrupt-DB error into a fake zero, since that's the exact silent
|
|
2231
|
+
* wrong-answer this fix is for.
|
|
2225
2232
|
*/
|
|
2226
2233
|
export async function getNamespaceCounts(dbPath) {
|
|
2227
2234
|
const resolvedPath = dbPath || memoryDbPath(process.cwd());
|
|
2235
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
2236
|
+
return { namespaces: {}, total: 0, withEmbeddings: 0 };
|
|
2237
|
+
}
|
|
2238
|
+
const db = openDaemonDatabase(resolvedPath);
|
|
2228
2239
|
try {
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
}
|
|
2232
|
-
const db = openDaemonDatabase(resolvedPath);
|
|
2233
|
-
const result = db.exec("SELECT namespace, COUNT(*) as cnt FROM memory_entries WHERE status = 'active' GROUP BY namespace ORDER BY cnt DESC");
|
|
2234
|
-
db.close();
|
|
2240
|
+
const result = db.exec("SELECT namespace, COUNT(*) AS cnt, SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS emb_cnt " +
|
|
2241
|
+
"FROM memory_entries WHERE status = 'active' GROUP BY namespace ORDER BY cnt DESC");
|
|
2235
2242
|
const namespaces = {};
|
|
2236
2243
|
let total = 0;
|
|
2244
|
+
let withEmbeddings = 0;
|
|
2237
2245
|
if (result[0]?.values) {
|
|
2238
2246
|
for (const row of result[0].values) {
|
|
2239
2247
|
const ns = String(row[0]);
|
|
2240
2248
|
const count = Number(row[1]);
|
|
2249
|
+
const embCount = Number(row[2] ?? 0);
|
|
2241
2250
|
namespaces[ns] = count;
|
|
2242
2251
|
total += count;
|
|
2252
|
+
withEmbeddings += embCount;
|
|
2243
2253
|
}
|
|
2244
2254
|
}
|
|
2245
|
-
return { namespaces, total };
|
|
2255
|
+
return { namespaces, total, withEmbeddings };
|
|
2246
2256
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2257
|
+
finally {
|
|
2258
|
+
db.close();
|
|
2249
2259
|
}
|
|
2250
2260
|
}
|
|
2251
2261
|
export default {
|
|
@@ -80,7 +80,7 @@ const DEFAULT_CONFIG = {
|
|
|
80
80
|
maxUncertainty: 0.15,
|
|
81
81
|
enableCircuitBreaker: true,
|
|
82
82
|
circuitBreakerThreshold: 5,
|
|
83
|
-
statePath: '.
|
|
83
|
+
statePath: '.moflo/movector/model-router-state.json',
|
|
84
84
|
autoSaveInterval: 1, // Save after every decision for CLI persistence
|
|
85
85
|
enableCostOptimization: true,
|
|
86
86
|
preferSpeed: true,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Optimized state space with feature hashing
|
|
10
10
|
* - Epsilon decay with exponential annealing
|
|
11
11
|
* - Experience replay buffer for stable learning
|
|
12
|
-
* - Model persistence to .
|
|
12
|
+
* - Model persistence to .moflo/movector/q-learning-model.json
|
|
13
13
|
*
|
|
14
14
|
* @module q-learning-router
|
|
15
15
|
*/
|
|
@@ -32,7 +32,7 @@ const DEFAULT_CONFIG = {
|
|
|
32
32
|
enableReplay: true,
|
|
33
33
|
cacheSize: 256,
|
|
34
34
|
cacheTTL: 300000,
|
|
35
|
-
modelPath: '.
|
|
35
|
+
modelPath: '.moflo/movector/q-learning-model.json',
|
|
36
36
|
autoSaveInterval: 100,
|
|
37
37
|
stateSpaceDim: 64,
|
|
38
38
|
};
|
|
@@ -16,7 +16,18 @@ import { createServer } from 'node:http';
|
|
|
16
16
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
17
17
|
import { handleMemoryStore, handleMemoryDelete, handleMemoryBatch, handleMemoryGet, handleMemorySearch, handleMemoryList, matchMemoryRpcRoute, } from './daemon-memory-rpc.js';
|
|
18
18
|
import { aggregateClaudeStats, emptyClaudeStatsShape } from './claude-stats.js';
|
|
19
|
-
|
|
19
|
+
import { serverPortCandidates, LEGACY_DEFAULT_PORT } from './daemon-port.js';
|
|
20
|
+
import { writeLockPort } from './daemon-lock.js';
|
|
21
|
+
import { findProjectRoot } from './project-root.js';
|
|
22
|
+
import { readOwnMofloVersion } from './daemon-lock.js';
|
|
23
|
+
/**
|
|
24
|
+
* Legacy default port retained as a re-export of {@link LEGACY_DEFAULT_PORT}
|
|
25
|
+
* for backward compat with existing importers (`commands/daemon.ts`,
|
|
26
|
+
* `__tests__/daemon-dashboard.test.ts`). The actual port a daemon binds is
|
|
27
|
+
* now resolved deterministically per project via `serverPortCandidates()` —
|
|
28
|
+
* see `daemon-port.ts` and `docs/internal/1145-daemon-port-collision-analysis.md`.
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_DASHBOARD_PORT = LEGACY_DEFAULT_PORT;
|
|
20
31
|
/**
|
|
21
32
|
* Process-wide promise for the shared MemoryAccessor. Memoized as a *promise*
|
|
22
33
|
* (not the resolved value) so concurrent first-callers share a single init
|
|
@@ -129,6 +140,27 @@ function tryParseSafe(s) {
|
|
|
129
140
|
return s;
|
|
130
141
|
}
|
|
131
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Build the `/api/health` response (#1145).
|
|
145
|
+
*
|
|
146
|
+
* Identity payload — clients compare `projectRoot` against their own
|
|
147
|
+
* `findProjectRoot()` and refuse to route to this daemon on mismatch.
|
|
148
|
+
* Also surfaces `pid`, `version`, and `uptimeMs` for healer-class
|
|
149
|
+
* diagnostics and orphan-daemon detection.
|
|
150
|
+
*
|
|
151
|
+
* Read-only, no-auth, localhost-only (the dashboard binds 127.0.0.1).
|
|
152
|
+
*/
|
|
153
|
+
function handleHealth(daemon, opts) {
|
|
154
|
+
const status = daemon.getStatus();
|
|
155
|
+
const startedAt = status.startedAt instanceof Date ? status.startedAt : null;
|
|
156
|
+
return {
|
|
157
|
+
status: 'ok',
|
|
158
|
+
projectRoot: opts.projectRoot ?? findProjectRoot(),
|
|
159
|
+
pid: status.pid ?? process.pid,
|
|
160
|
+
version: readOwnMofloVersion() ?? null,
|
|
161
|
+
uptimeMs: startedAt ? Date.now() - startedAt.getTime() : 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
132
164
|
function handleStatus(daemon) {
|
|
133
165
|
const status = daemon.getStatus();
|
|
134
166
|
// Index config rows by worker type so the row renderer can show a
|
|
@@ -244,15 +276,18 @@ function tryParse(s) {
|
|
|
244
276
|
}
|
|
245
277
|
}
|
|
246
278
|
async function handleMemoryStats() {
|
|
247
|
-
// Single GROUP BY query — no hardcoded namespace list, no row fetching
|
|
248
|
-
try
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
279
|
+
// Single GROUP BY query — no hardcoded namespace list, no row fetching.
|
|
280
|
+
// Errors propagate to the request handler's outer try/catch → 500, so
|
|
281
|
+
// MCP clients see a real failure instead of a silent `totalEntries: 0`.
|
|
282
|
+
const { getNamespaceCounts } = await import('../memory/memory-initializer.js');
|
|
283
|
+
const { namespaces, total, withEmbeddings } = await getNamespaceCounts();
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
namespaces,
|
|
287
|
+
totalEntries: total,
|
|
288
|
+
withEmbeddings,
|
|
289
|
+
available: total > 0 || Object.keys(namespaces).length > 0,
|
|
290
|
+
};
|
|
256
291
|
}
|
|
257
292
|
/**
|
|
258
293
|
* Build the `/api/claude-stats` response (#1044).
|
|
@@ -433,6 +468,11 @@ async function handleRequest(req, res, daemon, opts) {
|
|
|
433
468
|
if (url === '/') {
|
|
434
469
|
sendHtml(res, DASHBOARD_HTML);
|
|
435
470
|
}
|
|
471
|
+
else if (url === '/api/health') {
|
|
472
|
+
// #1145 — identity probe. Clients use this to confirm they're talking
|
|
473
|
+
// to the daemon for their OWN project before routing memory ops here.
|
|
474
|
+
sendJson(res, 200, handleHealth(daemon, opts));
|
|
475
|
+
}
|
|
436
476
|
else if (url === '/api/status') {
|
|
437
477
|
sendJson(res, 200, handleStatus(daemon));
|
|
438
478
|
}
|
|
@@ -588,33 +628,62 @@ const MAX_PORT_ATTEMPTS = 10;
|
|
|
588
628
|
/**
|
|
589
629
|
* Start the dashboard HTTP server.
|
|
590
630
|
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
631
|
+
* Port selection (#1145):
|
|
632
|
+
* 1. `opts.port`, if explicitly set (CLI `--dashboard-port` flag).
|
|
633
|
+
* 2. Otherwise `serverPortCandidates(projectRoot)` — deterministic per-
|
|
634
|
+
* project port + collision-fallback range.
|
|
635
|
+
* Both honor `MOFLO_DAEMON_PORT` (collapses the candidate list to one).
|
|
636
|
+
*
|
|
637
|
+
* On successful bind the bound port is stamped into `.moflo/daemon.lock`
|
|
638
|
+
* via `writeLockPort()` so clients can discover it without guessing.
|
|
594
639
|
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
640
|
+
* On bind exhaustion (every candidate in use) the server throws — the
|
|
641
|
+
* caller is expected to surface the failure rather than stay half-alive
|
|
642
|
+
* (the silent-trap pattern that produced #1145).
|
|
643
|
+
*
|
|
644
|
+
* @returns handle whose `.port` field reflects the actually bound port
|
|
598
645
|
*/
|
|
599
646
|
export async function startDashboard(daemon, opts) {
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
647
|
+
const projectRoot = opts.projectRoot ?? findProjectRoot();
|
|
648
|
+
const candidates = buildBindCandidates(opts.port, projectRoot, MAX_PORT_ATTEMPTS);
|
|
649
|
+
let lastErr = null;
|
|
650
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
651
|
+
const port = candidates[i];
|
|
603
652
|
try {
|
|
604
|
-
const handle = await tryListenOnPort(daemon, opts, port);
|
|
653
|
+
const handle = await tryListenOnPort(daemon, { ...opts, projectRoot }, port);
|
|
654
|
+
// Stamp the bound port into the lock so clients discover us reliably.
|
|
655
|
+
// Best-effort: a missing/locked-by-another-pid lock means stamping
|
|
656
|
+
// is a no-op — the deterministic fallback still works.
|
|
657
|
+
try {
|
|
658
|
+
writeLockPort(projectRoot, handle.port);
|
|
659
|
+
}
|
|
660
|
+
catch { /* ignore */ }
|
|
605
661
|
return handle;
|
|
606
662
|
}
|
|
607
663
|
catch (err) {
|
|
664
|
+
lastErr = err;
|
|
608
665
|
const code = err && typeof err === 'object' && 'code' in err ? err.code : '';
|
|
609
|
-
if (code === 'EADDRINUSE' &&
|
|
610
|
-
// Port taken — try the next one
|
|
666
|
+
if (code === 'EADDRINUSE' && i < candidates.length - 1)
|
|
611
667
|
continue;
|
|
612
|
-
}
|
|
613
668
|
throw err;
|
|
614
669
|
}
|
|
615
670
|
}
|
|
616
|
-
//
|
|
617
|
-
throw new Error(`All dashboard ports ${
|
|
671
|
+
// Bind exhaustion — surface so the daemon can hard-fail (#1145 §9.4).
|
|
672
|
+
throw lastErr ?? new Error(`All dashboard ports (${candidates[0]}…${candidates[candidates.length - 1]}) are in use`);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Build the ordered list of ports to try.
|
|
676
|
+
*
|
|
677
|
+
* When the caller pinned a port (CLI flag), respect it without any
|
|
678
|
+
* fallback — the consumer pinned it on purpose. When they didn't, use
|
|
679
|
+
* the deterministic per-project candidates so two projects never collide
|
|
680
|
+
* silently on a fixed default.
|
|
681
|
+
*/
|
|
682
|
+
function buildBindCandidates(explicitPort, projectRoot, maxAttempts) {
|
|
683
|
+
if (typeof explicitPort === 'number' && explicitPort > 0 && explicitPort < 65536) {
|
|
684
|
+
return [explicitPort];
|
|
685
|
+
}
|
|
686
|
+
return serverPortCandidates(projectRoot, maxAttempts);
|
|
618
687
|
}
|
|
619
688
|
/**
|
|
620
689
|
* Attempt to bind the dashboard server to a specific port.
|