@runcore-sh/runcore 0.1.8 → 0.1.10
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/dist/access/manifest.d.ts +59 -0
- package/dist/access/manifest.d.ts.map +1 -0
- package/dist/access/manifest.js +251 -0
- package/dist/access/manifest.js.map +1 -0
- package/dist/activity/log.d.ts +1 -1
- package/dist/activity/log.d.ts.map +1 -1
- package/dist/agents/autonomous.d.ts.map +1 -1
- package/dist/agents/autonomous.js +38 -0
- package/dist/agents/autonomous.js.map +1 -1
- package/dist/agents/governance.d.ts +70 -0
- package/dist/agents/governance.d.ts.map +1 -0
- package/dist/agents/governance.js +220 -0
- package/dist/agents/governance.js.map +1 -0
- package/dist/agents/governed-spawn.d.ts +83 -0
- package/dist/agents/governed-spawn.d.ts.map +1 -0
- package/dist/agents/governed-spawn.js +186 -0
- package/dist/agents/governed-spawn.js.map +1 -0
- package/dist/agents/heartbeat.d.ts +91 -0
- package/dist/agents/heartbeat.d.ts.map +1 -0
- package/dist/agents/heartbeat.js +323 -0
- package/dist/agents/heartbeat.js.map +1 -0
- package/dist/agents/index.d.ts +4 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +6 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/spawn-policy.d.ts +45 -0
- package/dist/agents/spawn-policy.d.ts.map +1 -0
- package/dist/agents/spawn-policy.js +202 -0
- package/dist/agents/spawn-policy.js.map +1 -0
- package/dist/alert.d.ts +16 -0
- package/dist/alert.d.ts.map +1 -0
- package/dist/alert.js +70 -0
- package/dist/alert.js.map +1 -0
- package/dist/cli.js +261 -32
- package/dist/cli.js.map +1 -1
- package/dist/credentials/store.d.ts +1 -1
- package/dist/credentials/store.d.ts.map +1 -1
- package/dist/credentials/store.js +14 -3
- package/dist/credentials/store.js.map +1 -1
- package/dist/crystallizer.d.ts +56 -0
- package/dist/crystallizer.d.ts.map +1 -0
- package/dist/crystallizer.js +159 -0
- package/dist/crystallizer.js.map +1 -0
- package/dist/distiller.d.ts +48 -0
- package/dist/distiller.d.ts.map +1 -0
- package/dist/distiller.js +140 -0
- package/dist/distiller.js.map +1 -0
- package/dist/files/deep-index.d.ts +59 -0
- package/dist/files/deep-index.d.ts.map +1 -0
- package/dist/files/deep-index.js +337 -0
- package/dist/files/deep-index.js.map +1 -0
- package/dist/files/import.d.ts +44 -0
- package/dist/files/import.d.ts.map +1 -0
- package/dist/files/import.js +213 -0
- package/dist/files/import.js.map +1 -0
- package/dist/files/index-local.d.ts +37 -0
- package/dist/files/index-local.d.ts.map +1 -0
- package/dist/files/index-local.js +198 -0
- package/dist/files/index-local.js.map +1 -0
- package/dist/google/auth.d.ts +2 -0
- package/dist/google/auth.d.ts.map +1 -1
- package/dist/google/auth.js +2 -0
- package/dist/google/auth.js.map +1 -1
- package/dist/integrations/gate.d.ts +40 -0
- package/dist/integrations/gate.d.ts.map +1 -0
- package/dist/integrations/gate.js +100 -0
- package/dist/integrations/gate.js.map +1 -0
- package/dist/lib/audit.d.ts +43 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/audit.js +120 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/brain-io.d.ts.map +1 -1
- package/dist/lib/brain-io.js +52 -0
- package/dist/lib/brain-io.js.map +1 -1
- package/dist/lib/dpapi.d.ts +14 -0
- package/dist/lib/dpapi.d.ts.map +1 -0
- package/dist/lib/dpapi.js +104 -0
- package/dist/lib/dpapi.js.map +1 -0
- package/dist/lib/glob-match.d.ts +22 -0
- package/dist/lib/glob-match.d.ts.map +1 -0
- package/dist/lib/glob-match.js +64 -0
- package/dist/lib/glob-match.js.map +1 -0
- package/dist/lib/locked.d.ts +40 -0
- package/dist/lib/locked.d.ts.map +1 -0
- package/dist/lib/locked.js +130 -0
- package/dist/lib/locked.js.map +1 -0
- package/dist/llm/complete.d.ts.map +1 -1
- package/dist/llm/complete.js +5 -2
- package/dist/llm/complete.js.map +1 -1
- package/dist/llm/fetch-guard.d.ts +16 -0
- package/dist/llm/fetch-guard.d.ts.map +1 -0
- package/dist/llm/fetch-guard.js +61 -0
- package/dist/llm/fetch-guard.js.map +1 -0
- package/dist/llm/guard.d.ts +40 -0
- package/dist/llm/guard.d.ts.map +1 -0
- package/dist/llm/guard.js +88 -0
- package/dist/llm/guard.js.map +1 -0
- package/dist/llm/membrane.d.ts +46 -0
- package/dist/llm/membrane.d.ts.map +1 -0
- package/dist/llm/membrane.js +123 -0
- package/dist/llm/membrane.js.map +1 -0
- package/dist/llm/providers/index.d.ts +5 -1
- package/dist/llm/providers/index.d.ts.map +1 -1
- package/dist/llm/providers/index.js +8 -1
- package/dist/llm/providers/index.js.map +1 -1
- package/dist/llm/redact.d.ts +39 -0
- package/dist/llm/redact.d.ts.map +1 -0
- package/dist/llm/redact.js +155 -0
- package/dist/llm/redact.js.map +1 -0
- package/dist/llm/sensitive-registry.d.ts +33 -0
- package/dist/llm/sensitive-registry.d.ts.map +1 -0
- package/dist/llm/sensitive-registry.js +106 -0
- package/dist/llm/sensitive-registry.js.map +1 -0
- package/dist/mcp-server.d.ts +11 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +520 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mdns.d.ts +17 -0
- package/dist/mdns.d.ts.map +1 -0
- package/dist/mdns.js +110 -0
- package/dist/mdns.js.map +1 -0
- package/dist/nerve/push.d.ts +26 -0
- package/dist/nerve/push.d.ts.map +1 -0
- package/dist/nerve/push.js +170 -0
- package/dist/nerve/push.js.map +1 -0
- package/dist/nerve/state.d.ts +35 -0
- package/dist/nerve/state.d.ts.map +1 -0
- package/dist/nerve/state.js +257 -0
- package/dist/nerve/state.js.map +1 -0
- package/dist/posture/engine.d.ts +41 -0
- package/dist/posture/engine.d.ts.map +1 -0
- package/dist/posture/engine.js +217 -0
- package/dist/posture/engine.js.map +1 -0
- package/dist/posture/index.d.ts +11 -0
- package/dist/posture/index.d.ts.map +1 -0
- package/dist/posture/index.js +10 -0
- package/dist/posture/index.js.map +1 -0
- package/dist/posture/middleware.d.ts +30 -0
- package/dist/posture/middleware.d.ts.map +1 -0
- package/dist/posture/middleware.js +92 -0
- package/dist/posture/middleware.js.map +1 -0
- package/dist/posture/types.d.ts +61 -0
- package/dist/posture/types.d.ts.map +1 -0
- package/dist/posture/types.js +48 -0
- package/dist/posture/types.js.map +1 -0
- package/dist/resend/inbox.d.ts +23 -0
- package/dist/resend/inbox.d.ts.map +1 -0
- package/dist/resend/inbox.js +198 -0
- package/dist/resend/inbox.js.map +1 -0
- package/dist/resend/webhooks.d.ts +30 -0
- package/dist/resend/webhooks.d.ts.map +1 -0
- package/dist/resend/webhooks.js +244 -0
- package/dist/resend/webhooks.js.map +1 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +773 -58
- package/dist/server.js.map +1 -1
- package/dist/settings.d.ts +14 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +32 -1
- package/dist/settings.js.map +1 -1
- package/dist/tier/bond.d.ts +51 -0
- package/dist/tier/bond.d.ts.map +1 -0
- package/dist/tier/bond.js +154 -0
- package/dist/tier/bond.js.map +1 -0
- package/dist/tier/freeze.d.ts +21 -0
- package/dist/tier/freeze.d.ts.map +1 -0
- package/dist/tier/freeze.js +73 -0
- package/dist/tier/freeze.js.map +1 -0
- package/dist/tier/gate.d.ts +11 -0
- package/dist/tier/gate.d.ts.map +1 -0
- package/dist/tier/gate.js +25 -0
- package/dist/tier/gate.js.map +1 -0
- package/dist/tier/heartbeat.d.ts +22 -0
- package/dist/tier/heartbeat.d.ts.map +1 -0
- package/dist/tier/heartbeat.js +128 -0
- package/dist/tier/heartbeat.js.map +1 -0
- package/dist/tier/token.d.ts +22 -0
- package/dist/tier/token.d.ts.map +1 -0
- package/dist/tier/token.js +100 -0
- package/dist/tier/token.js.map +1 -0
- package/dist/tier/types.d.ts +44 -0
- package/dist/tier/types.d.ts.map +1 -0
- package/dist/tier/types.js +61 -0
- package/dist/tier/types.js.map +1 -0
- package/dist/updater.d.ts +32 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +145 -0
- package/dist/updater.js.map +1 -0
- package/dist/vault/policy.d.ts +42 -0
- package/dist/vault/policy.d.ts.map +1 -0
- package/dist/vault/policy.js +159 -0
- package/dist/vault/policy.js.map +1 -0
- package/dist/vault/store.d.ts +6 -0
- package/dist/vault/store.d.ts.map +1 -1
- package/dist/vault/store.js +15 -5
- package/dist/vault/store.js.map +1 -1
- package/dist/vault/transfer.d.ts +33 -0
- package/dist/vault/transfer.d.ts.map +1 -0
- package/dist/vault/transfer.js +187 -0
- package/dist/vault/transfer.js.map +1 -0
- package/dist/voucher.d.ts +39 -0
- package/dist/voucher.d.ts.map +1 -0
- package/dist/voucher.js +105 -0
- package/dist/voucher.js.map +1 -0
- package/dist/webhooks/handlers.d.ts +10 -0
- package/dist/webhooks/handlers.d.ts.map +1 -1
- package/dist/webhooks/handlers.js +53 -0
- package/dist/webhooks/handlers.js.map +1 -1
- package/dist/webhooks/index.d.ts +2 -2
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +2 -2
- package/dist/webhooks/index.js.map +1 -1
- package/dist/webhooks/verify.d.ts +8 -0
- package/dist/webhooks/verify.d.ts.map +1 -1
- package/dist/webhooks/verify.js +56 -0
- package/dist/webhooks/verify.js.map +1 -1
- package/package.json +8 -2
- package/public/board.html +8 -3
- package/public/browser.html +8 -3
- package/public/library.html +8 -3
- package/public/observatory.html +8 -3
- package/public/ops.html +8 -3
- package/public/registry.html +627 -0
- package/public/roadmap.html +975 -0
package/dist/server.js
CHANGED
|
@@ -16,6 +16,7 @@ const PKG_ROOT = join(__dirname, "..");
|
|
|
16
16
|
import { writeFileSync } from "node:fs";
|
|
17
17
|
import { initInstanceName, getInstanceName, getInstanceNameLower, resolveEnv, getAlertEmailFrom } from "./instance.js";
|
|
18
18
|
import { readBrainFile, writeBrainFile, appendBrainLine } from "./lib/brain-io.js";
|
|
19
|
+
import { runWithAuditContext } from "./lib/audit.js";
|
|
19
20
|
import { Brain } from "./brain.js";
|
|
20
21
|
import { FileSystemLongTermMemory } from "./memory/file-backed.js";
|
|
21
22
|
import { createLogger } from "./utils/logger.js";
|
|
@@ -25,7 +26,13 @@ import { ensurePairingCode, getStatus, pair, authenticate, getRecoveryQuestion,
|
|
|
25
26
|
import { getProvider } from "./llm/providers/index.js";
|
|
26
27
|
import { withStreamRetry } from "./llm/retry.js";
|
|
27
28
|
import { LLMError } from "./llm/errors.js";
|
|
28
|
-
import {
|
|
29
|
+
import { PrivateModeError, isPrivateMode, checkOllamaHealth } from "./llm/guard.js";
|
|
30
|
+
import { installFetchGuard } from "./llm/fetch-guard.js";
|
|
31
|
+
import { SensitiveRegistry } from "./llm/sensitive-registry.js";
|
|
32
|
+
import { PrivacyMembrane } from "./llm/membrane.js";
|
|
33
|
+
import { setActiveMembrane, rehydrateResponse } from "./llm/redact.js";
|
|
34
|
+
import { loadSettings, getSettings, updateSettings, resolveProvider, resolveChatModel, resolveUtilityModel, getPulseSettings, getMeshConfig, } from "./settings.js";
|
|
35
|
+
import { startMdns, stopMdns } from "./mdns.js";
|
|
29
36
|
import { ingestDirectory } from "./files/ingest.js";
|
|
30
37
|
import { processIngestFolder } from "./files/ingest-folder.js";
|
|
31
38
|
import { saveSession, loadSession } from "./sessions/store.js";
|
|
@@ -42,7 +49,9 @@ import { isSttAvailable, transcribe } from "./stt/client.js";
|
|
|
42
49
|
import { startAvatarSidecar, stopAvatarSidecar, isAvatarAvailable } from "./avatar/sidecar.js";
|
|
43
50
|
import { preparePhoto, generateVideo, getCachedVideo, cacheVideo, clearVideoCache } from "./avatar/client.js";
|
|
44
51
|
import { getTtsConfig, getSttConfig, getAvatarConfig } from "./settings.js";
|
|
45
|
-
import { loadVault, listVaultKeys, setVaultKey, deleteVaultKey, getDashReadableVault, getVaultEntries } from "./vault/store.js";
|
|
52
|
+
import { loadVault, listVaultKeys, setVaultKey, deleteVaultKey, getDashReadableVault, getVaultEntries, hydrateEnv as rehydrateVaultEnv } from "./vault/store.js";
|
|
53
|
+
import { exportVault, importVault, verifyExport } from "./vault/transfer.js";
|
|
54
|
+
import { getIntegrationStatus, isIntegrationEnabled } from "./integrations/gate.js";
|
|
46
55
|
import { makeCall } from "./twilio/call.js";
|
|
47
56
|
import { isGoogleConfigured, isGoogleAuthenticated, getAuthUrl, exchangeCode, } from "./google/auth.js";
|
|
48
57
|
import { isCalendarAvailable, getTodaySchedule, getUpcomingEvents, listEvents, searchEvents, getFreeBusy, createEvent, updateEvent, deleteEvent } from "./google/calendar.js";
|
|
@@ -100,6 +109,7 @@ import { listChannels, getChannelInfo, joinChannel, getChannelHistory } from "./
|
|
|
100
109
|
// Slack types no longer needed — providers handle their own type mapping
|
|
101
110
|
import { getClient as getWhatsAppClient, isWhatsAppConfigured } from "./channels/whatsapp.js";
|
|
102
111
|
import { parseFormBody, processIncomingMessage, emptyTwimlResponse, replyTwiml, twilioProvider } from "./webhooks/twilio.js";
|
|
112
|
+
import { resendProvider } from "./resend/webhooks.js";
|
|
103
113
|
import { handleWhatsAppMessage } from "./services/whatsapp.js";
|
|
104
114
|
import { initLLMCache, shutdownLLMCache, getCacheDiagnostics } from "./cache/llm-cache.js";
|
|
105
115
|
import { mountWebhookAdmin, createWebhookRoute, verifyWebhookRequest } from "./webhooks/mount.js";
|
|
@@ -122,7 +132,8 @@ function pickStreamFn() {
|
|
|
122
132
|
return withStreamRetry((options) => provider.streamChat(options), { maxRetries: 3, baseDelayMs: 1_000, maxDelayMs: 30_000 });
|
|
123
133
|
}
|
|
124
134
|
// --- Config ---
|
|
125
|
-
const PORT = parseInt(process.env.CORE_PORT ?? resolveEnv("PORT") ?? "
|
|
135
|
+
const PORT = parseInt(process.env.CORE_PORT ?? resolveEnv("PORT") ?? "0", 10);
|
|
136
|
+
let actualPort = PORT;
|
|
126
137
|
const SIDECAR_PORT = resolveEnv("SEARCH_PORT") ?? "3578";
|
|
127
138
|
const BRAIN_DIR = join(process.cwd(), "brain");
|
|
128
139
|
const SKILLS_DIR = join(process.cwd(), "skills");
|
|
@@ -177,7 +188,7 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
177
188
|
// Read custom personality instructions (empty string if file doesn't exist)
|
|
178
189
|
let personality = "";
|
|
179
190
|
try {
|
|
180
|
-
personality = (await readBrainFile(PERSONALITY_PATH)).trim();
|
|
191
|
+
personality = (await runWithAuditContext({ caller: "http:init:personality", channel: "http" }, () => readBrainFile(PERSONALITY_PATH))).trim();
|
|
181
192
|
}
|
|
182
193
|
catch { }
|
|
183
194
|
// Fetch current projects for system prompt injection
|
|
@@ -362,6 +373,17 @@ app.get("/", async (c) => {
|
|
|
362
373
|
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "index.html"));
|
|
363
374
|
return c.html(html);
|
|
364
375
|
});
|
|
376
|
+
// --- Nerve endpoint (PWA) ---
|
|
377
|
+
app.get("/nerve", async (c) => {
|
|
378
|
+
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "nerve", "index.html"));
|
|
379
|
+
return c.html(html);
|
|
380
|
+
});
|
|
381
|
+
// --- Audit context middleware ---
|
|
382
|
+
// Tags all brain file reads within HTTP handlers with the route info.
|
|
383
|
+
app.use("/api/*", async (c, next) => {
|
|
384
|
+
const caller = `http:${c.req.method} ${c.req.path}`;
|
|
385
|
+
return runWithAuditContext({ caller, channel: "http" }, () => next());
|
|
386
|
+
});
|
|
365
387
|
// --- Tracing middleware ---
|
|
366
388
|
app.use("/api/*", tracingMiddleware());
|
|
367
389
|
// --- Metrics middleware ---
|
|
@@ -384,12 +406,17 @@ app.use("/api/*", async (c, next) => {
|
|
|
384
406
|
}
|
|
385
407
|
return generalLimiter(c, next);
|
|
386
408
|
});
|
|
409
|
+
// --- Posture middleware (intent accumulation + surface gating) ---
|
|
410
|
+
// Track all API interactions for posture engine
|
|
411
|
+
app.use("/api/*", postureTracker());
|
|
412
|
+
// Attach posture surface header to all responses
|
|
413
|
+
app.use("/api/*", postureHeader());
|
|
387
414
|
// --- Webhook initialization (batch registration + config + admin routes) ---
|
|
388
415
|
const webhookInitStart = performance.now();
|
|
389
416
|
// Phase 1: Batch-register all webhook providers (deferred from module imports to avoid
|
|
390
417
|
// 5 individual logActivity calls during startup — now a single batch call).
|
|
391
418
|
const registerStart = performance.now();
|
|
392
|
-
registerProviders([githubProvider, slackEventsProvider, slackCommandsProvider, slackInteractionsProvider, twilioProvider]);
|
|
419
|
+
registerProviders([githubProvider, slackEventsProvider, slackCommandsProvider, slackInteractionsProvider, twilioProvider, resendProvider]);
|
|
393
420
|
const registerMs = performance.now() - registerStart;
|
|
394
421
|
// Phase 2: Configure webhook providers (secrets resolved from env vars)
|
|
395
422
|
const configStart = performance.now();
|
|
@@ -399,6 +426,7 @@ setProviderConfigs([
|
|
|
399
426
|
{ name: "slack-interactions", secret: "SLACK_SIGNING_SECRET", signatureHeader: "x-slack-signature", algorithm: "slack-v0", path: "/api/slack/interactions" },
|
|
400
427
|
{ name: "twilio", secret: "TWILIO_AUTH_TOKEN", signatureHeader: "x-twilio-signature", algorithm: "twilio", path: "/api/twilio/whatsapp" },
|
|
401
428
|
{ name: "github", secret: "GITHUB_WEBHOOK_SECRET", signatureHeader: "x-hub-signature-256", algorithm: "hmac-sha256-hex", path: "/api/github/webhooks" },
|
|
429
|
+
{ name: "resend", secret: "RESEND_WEBHOOK_SECRET", signatureHeader: "svix-signature", algorithm: "custom", path: "/api/resend/webhooks" },
|
|
402
430
|
]);
|
|
403
431
|
const configMs = performance.now() - configStart;
|
|
404
432
|
// Phase 3: Mount admin routes
|
|
@@ -422,6 +450,7 @@ app.get("/api/status", async (c) => {
|
|
|
422
450
|
...status,
|
|
423
451
|
provider: resolveProvider(),
|
|
424
452
|
airplaneMode: settings.airplaneMode,
|
|
453
|
+
privateMode: settings.privateMode,
|
|
425
454
|
safeWordMode: settings.safeWordMode,
|
|
426
455
|
search: isSearchAvailable(),
|
|
427
456
|
tts: isTtsAvailable(),
|
|
@@ -579,6 +608,70 @@ app.delete("/api/vault/:name", async (c) => {
|
|
|
579
608
|
await deleteVaultKey(name, key);
|
|
580
609
|
return c.json({ ok: true });
|
|
581
610
|
});
|
|
611
|
+
// Export vault to portable passphrase-encrypted file
|
|
612
|
+
app.post("/api/vault/export", async (c) => {
|
|
613
|
+
const sessionId = c.req.query("sessionId");
|
|
614
|
+
if (!sessionId)
|
|
615
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
616
|
+
const session = validateSession(sessionId);
|
|
617
|
+
if (!session)
|
|
618
|
+
return c.json({ error: "Invalid or expired session" }, 401);
|
|
619
|
+
const body = await c.req.json();
|
|
620
|
+
if (!body.passphrase || body.passphrase.length < 8) {
|
|
621
|
+
return c.json({ error: "Passphrase required (min 8 characters)" }, 400);
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
const result = await exportVault(body.passphrase);
|
|
625
|
+
return c.json({ ok: true, filePath: result.filePath, stats: result.stats });
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
return c.json({ error: e.message }, 500);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
// Import vault from portable passphrase-encrypted file
|
|
632
|
+
app.post("/api/vault/import", async (c) => {
|
|
633
|
+
const sessionId = c.req.query("sessionId");
|
|
634
|
+
if (!sessionId)
|
|
635
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
636
|
+
const session = validateSession(sessionId);
|
|
637
|
+
if (!session)
|
|
638
|
+
return c.json({ error: "Invalid or expired session" }, 401);
|
|
639
|
+
const key = sessionKeys.get(sessionId);
|
|
640
|
+
if (!key)
|
|
641
|
+
return c.json({ error: "Session key not found" }, 401);
|
|
642
|
+
const body = await c.req.json();
|
|
643
|
+
if (!body.filePath || !body.passphrase) {
|
|
644
|
+
return c.json({ error: "filePath and passphrase required" }, 400);
|
|
645
|
+
}
|
|
646
|
+
const strategy = body.strategy ?? "skip";
|
|
647
|
+
try {
|
|
648
|
+
const result = await importVault(body.filePath, body.passphrase, strategy, key);
|
|
649
|
+
return c.json({ ok: true, stats: result.stats });
|
|
650
|
+
}
|
|
651
|
+
catch (e) {
|
|
652
|
+
return c.json({ error: e.message }, 400);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
// Verify a vault export file without importing
|
|
656
|
+
app.post("/api/vault/verify-export", async (c) => {
|
|
657
|
+
const sessionId = c.req.query("sessionId");
|
|
658
|
+
if (!sessionId)
|
|
659
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
660
|
+
const session = validateSession(sessionId);
|
|
661
|
+
if (!session)
|
|
662
|
+
return c.json({ error: "Invalid or expired session" }, 401);
|
|
663
|
+
const body = await c.req.json();
|
|
664
|
+
if (!body.filePath || !body.passphrase) {
|
|
665
|
+
return c.json({ error: "filePath and passphrase required" }, 400);
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
const result = await verifyExport(body.filePath, body.passphrase);
|
|
669
|
+
return c.json({ ok: true, message: result.message, stats: result.stats });
|
|
670
|
+
}
|
|
671
|
+
catch (e) {
|
|
672
|
+
return c.json({ error: e.message }, 400);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
582
675
|
// --- Google OAuth2 routes ---
|
|
583
676
|
// Initiate Google OAuth flow — redirects to Google consent screen
|
|
584
677
|
// No session required: this just redirects to Google, no sensitive data returned
|
|
@@ -1178,6 +1271,43 @@ app.put("/api/settings", async (c) => {
|
|
|
1178
1271
|
},
|
|
1179
1272
|
});
|
|
1180
1273
|
});
|
|
1274
|
+
// --- Integration admin routes ---
|
|
1275
|
+
app.get("/api/admin/integrations", async (c) => {
|
|
1276
|
+
const settings = getSettings();
|
|
1277
|
+
const integrations = settings.integrations ?? { enabled: true };
|
|
1278
|
+
const status = getIntegrationStatus();
|
|
1279
|
+
return c.json({
|
|
1280
|
+
enabled: integrations.enabled ?? true,
|
|
1281
|
+
services: status,
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
app.post("/api/admin/integrations", async (c) => {
|
|
1285
|
+
const body = await c.req.json();
|
|
1286
|
+
const patch = {
|
|
1287
|
+
integrations: {},
|
|
1288
|
+
};
|
|
1289
|
+
if (typeof body.enabled === "boolean") {
|
|
1290
|
+
patch.integrations.enabled = body.enabled;
|
|
1291
|
+
}
|
|
1292
|
+
if (body.services && typeof body.services === "object") {
|
|
1293
|
+
patch.integrations.services = body.services;
|
|
1294
|
+
}
|
|
1295
|
+
const updated = await updateSettings(patch);
|
|
1296
|
+
// Re-hydrate env with new gates — clear integration vars first, then re-hydrate
|
|
1297
|
+
const status = getIntegrationStatus();
|
|
1298
|
+
rehydrateVaultEnv();
|
|
1299
|
+
const credStore = getCredentialStore();
|
|
1300
|
+
if (credStore)
|
|
1301
|
+
await credStore.hydrate();
|
|
1302
|
+
return c.json({
|
|
1303
|
+
enabled: updated.integrations?.enabled ?? true,
|
|
1304
|
+
services: status.map((s) => ({
|
|
1305
|
+
...s,
|
|
1306
|
+
// Re-check after settings update
|
|
1307
|
+
enabled: isIntegrationEnabled(s.service),
|
|
1308
|
+
})),
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1181
1311
|
// --- Voice routes ---
|
|
1182
1312
|
// Voice status: which voice features are available?
|
|
1183
1313
|
app.get("/api/voice-status", async (c) => {
|
|
@@ -1386,7 +1516,16 @@ app.post("/api/branch", async (c) => {
|
|
|
1386
1516
|
];
|
|
1387
1517
|
const activeProvider = resolveProvider();
|
|
1388
1518
|
const activeChatModel = resolveChatModel();
|
|
1389
|
-
|
|
1519
|
+
let stream_fn;
|
|
1520
|
+
try {
|
|
1521
|
+
stream_fn = pickStreamFn();
|
|
1522
|
+
}
|
|
1523
|
+
catch (err) {
|
|
1524
|
+
if (err instanceof PrivateModeError) {
|
|
1525
|
+
return c.json({ error: err.message }, 503);
|
|
1526
|
+
}
|
|
1527
|
+
throw err;
|
|
1528
|
+
}
|
|
1390
1529
|
const reqSignal = c.req.raw.signal;
|
|
1391
1530
|
return streamSSE(c, async (stream) => {
|
|
1392
1531
|
// Send branch trace metadata so the UI can track lineage
|
|
@@ -1407,21 +1546,42 @@ app.post("/api/branch", async (c) => {
|
|
|
1407
1546
|
}
|
|
1408
1547
|
const onAbort = () => resolve();
|
|
1409
1548
|
reqSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1549
|
+
// Token buffer for split-placeholder rehydration
|
|
1550
|
+
let tokenBuf = "";
|
|
1551
|
+
const flushBuf = () => {
|
|
1552
|
+
if (!tokenBuf)
|
|
1553
|
+
return;
|
|
1554
|
+
const rehydrated = rehydrateResponse(tokenBuf);
|
|
1555
|
+
tokenBuf = "";
|
|
1556
|
+
stream.writeSSE({ data: JSON.stringify({ token: rehydrated }) }).catch(() => { });
|
|
1557
|
+
};
|
|
1410
1558
|
stream_fn({
|
|
1411
1559
|
messages,
|
|
1412
1560
|
model: activeChatModel,
|
|
1413
1561
|
signal: reqSignal,
|
|
1414
1562
|
onToken: (token) => {
|
|
1415
|
-
|
|
1563
|
+
tokenBuf += token;
|
|
1564
|
+
// Hold if buffer ends with partial placeholder: << ... (no closing >>)
|
|
1565
|
+
const lastOpen = tokenBuf.lastIndexOf("<<");
|
|
1566
|
+
if (lastOpen !== -1 && tokenBuf.indexOf(">>", lastOpen) === -1)
|
|
1567
|
+
return;
|
|
1568
|
+
flushBuf();
|
|
1416
1569
|
},
|
|
1417
1570
|
onDone: () => {
|
|
1571
|
+
flushBuf(); // flush remainder
|
|
1418
1572
|
reqSignal?.removeEventListener("abort", onAbort);
|
|
1419
1573
|
stream.writeSSE({ data: JSON.stringify({ done: true }) }).catch(() => { });
|
|
1420
1574
|
resolve();
|
|
1421
1575
|
},
|
|
1422
|
-
onError: (err) => {
|
|
1576
|
+
onError: async (err) => {
|
|
1577
|
+
flushBuf();
|
|
1423
1578
|
reqSignal?.removeEventListener("abort", onAbort);
|
|
1424
|
-
|
|
1579
|
+
let errorMsg = err instanceof LLMError ? err.userMessage : (err.message || "Stream error");
|
|
1580
|
+
if (isPrivateMode() && /ECONNREFUSED|fetch failed|network|socket/i.test(errorMsg)) {
|
|
1581
|
+
const health = await checkOllamaHealth();
|
|
1582
|
+
if (!health.ok)
|
|
1583
|
+
errorMsg += " — Check that Ollama is running. " + health.message;
|
|
1584
|
+
}
|
|
1425
1585
|
stream.writeSSE({ data: JSON.stringify({ error: errorMsg }) }).catch(() => { });
|
|
1426
1586
|
resolve();
|
|
1427
1587
|
},
|
|
@@ -2076,6 +2236,22 @@ app.post("/api/slack/commands", createWebhookRoute({ provider: "slack-commands"
|
|
|
2076
2236
|
// Slack interactions: routed through the generic webhook system.
|
|
2077
2237
|
// The slack-interactions provider handles extracting JSON from the form "payload" field.
|
|
2078
2238
|
app.post("/api/slack/interactions", createWebhookRoute({ provider: "slack-interactions" }));
|
|
2239
|
+
// --- Resend inbound email ---
|
|
2240
|
+
// Resend webhook: receive inbound emails via Svix-signed webhooks (direct path).
|
|
2241
|
+
// Uses generic webhook route with Svix signature verification.
|
|
2242
|
+
app.post("/api/resend/webhooks", createWebhookRoute({ provider: "resend" }));
|
|
2243
|
+
// Resend inbox: manually trigger inbox check (pulls from Worker KV).
|
|
2244
|
+
app.post("/api/resend/check-inbox", async (c) => {
|
|
2245
|
+
const sessionId = c.req.query("sessionId");
|
|
2246
|
+
if (!sessionId)
|
|
2247
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
2248
|
+
const session = validateSession(sessionId);
|
|
2249
|
+
if (!session)
|
|
2250
|
+
return c.json({ error: "Invalid or expired session" }, 401);
|
|
2251
|
+
const { forceCheckResendInbox } = await import("./resend/inbox.js");
|
|
2252
|
+
const count = await forceCheckResendInbox();
|
|
2253
|
+
return c.json({ ok: true, processed: count });
|
|
2254
|
+
});
|
|
2079
2255
|
// --- WhatsApp routes (Twilio-backed) ---
|
|
2080
2256
|
// WhatsApp status: check if configured
|
|
2081
2257
|
app.get("/api/whatsapp/status", (c) => {
|
|
@@ -2147,6 +2323,59 @@ app.post("/api/relay/whatsapp", async (c) => {
|
|
|
2147
2323
|
const result = await handleWhatsAppMessage(from, body, payload.ProfileName);
|
|
2148
2324
|
return c.json({ ok: result.ok, reply: result.reply, error: result.error });
|
|
2149
2325
|
});
|
|
2326
|
+
// Resend relay: receive pre-verified inbound emails from the Cloudflare Worker.
|
|
2327
|
+
// The Worker has already verified Resend's Svix signature and fetched the full
|
|
2328
|
+
// email body — this endpoint verifies the relay's own HMAC-SHA256 signature.
|
|
2329
|
+
app.post("/api/relay/resend", async (c) => {
|
|
2330
|
+
const rawBody = await c.req.text();
|
|
2331
|
+
const headers = {};
|
|
2332
|
+
c.req.raw.headers.forEach((value, key) => {
|
|
2333
|
+
headers[key.toLowerCase()] = value;
|
|
2334
|
+
});
|
|
2335
|
+
const relaySecret = process.env.RELAY_SECRET ?? "";
|
|
2336
|
+
const verification = verifyRelaySignature(rawBody, headers, relaySecret);
|
|
2337
|
+
if (!verification.valid) {
|
|
2338
|
+
return c.json({ error: verification.error ?? "Invalid relay signature" }, 401);
|
|
2339
|
+
}
|
|
2340
|
+
let payload;
|
|
2341
|
+
try {
|
|
2342
|
+
payload = JSON.parse(rawBody);
|
|
2343
|
+
}
|
|
2344
|
+
catch {
|
|
2345
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
2346
|
+
}
|
|
2347
|
+
if (payload.type !== "email.received" || !payload.body?.trim()) {
|
|
2348
|
+
return c.json({ ok: true, message: "No actionable content" });
|
|
2349
|
+
}
|
|
2350
|
+
logActivity({
|
|
2351
|
+
source: "resend",
|
|
2352
|
+
summary: `Inbound email relayed from ${payload.from}: "${payload.subject}"`,
|
|
2353
|
+
});
|
|
2354
|
+
// Import processInboundEmail dynamically to avoid circular deps at module level
|
|
2355
|
+
const { processInboundEmail, sendResendReply } = await import("./resend/webhooks.js");
|
|
2356
|
+
const reply = await processInboundEmail({
|
|
2357
|
+
from: payload.from,
|
|
2358
|
+
subject: payload.subject,
|
|
2359
|
+
body: payload.body,
|
|
2360
|
+
date: payload.created_at || new Date().toISOString(),
|
|
2361
|
+
});
|
|
2362
|
+
if (!reply) {
|
|
2363
|
+
return c.json({ ok: true, message: "No reply generated" });
|
|
2364
|
+
}
|
|
2365
|
+
const sent = await sendResendReply({
|
|
2366
|
+
to: payload.from,
|
|
2367
|
+
subject: payload.subject,
|
|
2368
|
+
body: reply,
|
|
2369
|
+
inReplyTo: payload.message_id,
|
|
2370
|
+
});
|
|
2371
|
+
logActivity({
|
|
2372
|
+
source: "resend",
|
|
2373
|
+
summary: sent
|
|
2374
|
+
? `Replied to ${payload.from}: "${payload.subject}" (${reply.length} chars)`
|
|
2375
|
+
: `Failed to reply to ${payload.from}: "${payload.subject}"`,
|
|
2376
|
+
});
|
|
2377
|
+
return c.json({ ok: true, sent, replyLength: reply.length });
|
|
2378
|
+
});
|
|
2150
2379
|
// WhatsApp send: send a message (requires sessionId)
|
|
2151
2380
|
app.post("/api/whatsapp/send", async (c) => {
|
|
2152
2381
|
const sessionId = c.req.query("sessionId");
|
|
@@ -2214,6 +2443,10 @@ function issuesToCardPayload(issues) {
|
|
|
2214
2443
|
};
|
|
2215
2444
|
});
|
|
2216
2445
|
}
|
|
2446
|
+
// Board API — gated to board posture
|
|
2447
|
+
app.use("/api/board/*", requireSurface("pages"));
|
|
2448
|
+
app.use("/api/ops/*", requireSurface("pages"));
|
|
2449
|
+
app.use("/api/agents/*", requireSurface("agents"));
|
|
2217
2450
|
// Board status: is a provider configured, and who is the user?
|
|
2218
2451
|
app.get("/api/board/status", async (c) => {
|
|
2219
2452
|
const board = getBoardProvider();
|
|
@@ -3020,32 +3253,114 @@ app.get("/api/help/context", async (c) => {
|
|
|
3020
3253
|
changelog,
|
|
3021
3254
|
});
|
|
3022
3255
|
});
|
|
3023
|
-
// --- Ops dashboard routes (
|
|
3024
|
-
//
|
|
3025
|
-
app.get("/observatory", async (c) => {
|
|
3256
|
+
// --- Ops dashboard routes (posture-gated: board level) ---
|
|
3257
|
+
// Board-level pages — only assembled when user has shown intent for full visibility
|
|
3258
|
+
app.get("/observatory", requireSurface("pages"), async (c) => {
|
|
3026
3259
|
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "observatory.html"));
|
|
3027
3260
|
return c.html(html);
|
|
3028
3261
|
});
|
|
3029
|
-
|
|
3030
|
-
app.get("/ops", async (c) => {
|
|
3262
|
+
app.get("/ops", requireSurface("pages"), async (c) => {
|
|
3031
3263
|
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "ops.html"));
|
|
3032
3264
|
return c.html(html);
|
|
3033
3265
|
});
|
|
3034
|
-
|
|
3035
|
-
app.get("/board", async (c) => {
|
|
3266
|
+
app.get("/board", requireSurface("pages"), async (c) => {
|
|
3036
3267
|
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "board.html"));
|
|
3037
3268
|
return c.html(html);
|
|
3038
3269
|
});
|
|
3039
|
-
|
|
3040
|
-
app.get("/library", async (c) => {
|
|
3270
|
+
app.get("/library", requireSurface("pages"), async (c) => {
|
|
3041
3271
|
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "library.html"));
|
|
3042
3272
|
return c.html(html);
|
|
3043
3273
|
});
|
|
3044
|
-
|
|
3045
|
-
app.get("/browser", async (c) => {
|
|
3274
|
+
app.get("/browser", requireSurface("pages"), async (c) => {
|
|
3046
3275
|
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "browser.html"));
|
|
3047
3276
|
return c.html(html);
|
|
3048
3277
|
});
|
|
3278
|
+
// Registry is always available — it's the entry point
|
|
3279
|
+
app.get("/registry", async (c) => {
|
|
3280
|
+
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "registry.html"));
|
|
3281
|
+
return c.html(html);
|
|
3282
|
+
});
|
|
3283
|
+
// Serve roadmap.html (strategic roadmap & rearview)
|
|
3284
|
+
app.get("/roadmap", async (c) => {
|
|
3285
|
+
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "roadmap.html"));
|
|
3286
|
+
return c.html(html);
|
|
3287
|
+
});
|
|
3288
|
+
// Roadmap API — parse brain/operations/roadmap.yaml and return as JSON
|
|
3289
|
+
app.get("/api/roadmap", async (c) => {
|
|
3290
|
+
try {
|
|
3291
|
+
const raw = await readBrainFile(join(process.cwd(), "brain", "operations", "roadmap.yaml"));
|
|
3292
|
+
const parsed = parseRoadmapYaml(raw);
|
|
3293
|
+
return c.json(parsed);
|
|
3294
|
+
}
|
|
3295
|
+
catch (err) {
|
|
3296
|
+
return c.json({ error: "Failed to load roadmap.yaml" }, 500);
|
|
3297
|
+
}
|
|
3298
|
+
});
|
|
3299
|
+
// Roadmap rearview API — recent git commits grouped by hour
|
|
3300
|
+
app.get("/api/roadmap/recent", async (c) => {
|
|
3301
|
+
const hours = parseInt(c.req.query("hours") || "24", 10);
|
|
3302
|
+
if (isNaN(hours) || hours < 1 || hours > 168) {
|
|
3303
|
+
return c.json({ error: "hours must be between 1 and 168" }, 400);
|
|
3304
|
+
}
|
|
3305
|
+
try {
|
|
3306
|
+
const { execSync } = await import("child_process");
|
|
3307
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
3308
|
+
const raw = execSync(`git log --after="${since}" --format="%H||%an||%ai||%s" --no-merges`, { cwd: process.cwd(), encoding: "utf-8", timeout: 10000 }).trim();
|
|
3309
|
+
if (!raw)
|
|
3310
|
+
return c.json({ commits: [], groups: [], hours, total: 0 });
|
|
3311
|
+
const commits = raw.split("\n").map((line) => {
|
|
3312
|
+
const [hash, author, date, ...msgParts] = line.split("||");
|
|
3313
|
+
const message = msgParts.join("||");
|
|
3314
|
+
const isAuto = /^\[(?:dash|agent)\]/i.test(message);
|
|
3315
|
+
return {
|
|
3316
|
+
hash: hash.slice(0, 8),
|
|
3317
|
+
fullHash: hash,
|
|
3318
|
+
author,
|
|
3319
|
+
date,
|
|
3320
|
+
message,
|
|
3321
|
+
autonomous: isAuto,
|
|
3322
|
+
tag: isAuto ? "dash" : "human",
|
|
3323
|
+
};
|
|
3324
|
+
});
|
|
3325
|
+
// Group by hour bucket
|
|
3326
|
+
const groups = {};
|
|
3327
|
+
for (const commit of commits) {
|
|
3328
|
+
const d = new Date(commit.date);
|
|
3329
|
+
const hourKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:00`;
|
|
3330
|
+
if (!groups[hourKey])
|
|
3331
|
+
groups[hourKey] = [];
|
|
3332
|
+
groups[hourKey].push(commit);
|
|
3333
|
+
}
|
|
3334
|
+
const grouped = Object.entries(groups)
|
|
3335
|
+
.map(([hour, items]) => ({ hour, commits: items, count: items.length }))
|
|
3336
|
+
.sort((a, b) => b.hour.localeCompare(a.hour));
|
|
3337
|
+
return c.json({ commits, groups: grouped, hours, total: commits.length });
|
|
3338
|
+
}
|
|
3339
|
+
catch (err) {
|
|
3340
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3341
|
+
return c.json({ error: "Failed to read git log: " + msg }, 500);
|
|
3342
|
+
}
|
|
3343
|
+
});
|
|
3344
|
+
// Serve personal.html (placeholder — instances populate this)
|
|
3345
|
+
app.get("/personal", async (c) => {
|
|
3346
|
+
try {
|
|
3347
|
+
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "personal.html"));
|
|
3348
|
+
return c.html(html);
|
|
3349
|
+
}
|
|
3350
|
+
catch {
|
|
3351
|
+
return c.text("Personal page not configured for this instance.", 404);
|
|
3352
|
+
}
|
|
3353
|
+
});
|
|
3354
|
+
// Serve life.html (placeholder — instances populate this)
|
|
3355
|
+
app.get("/life", async (c) => {
|
|
3356
|
+
try {
|
|
3357
|
+
const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "life.html"));
|
|
3358
|
+
return c.html(html);
|
|
3359
|
+
}
|
|
3360
|
+
catch {
|
|
3361
|
+
return c.text("Life page not configured for this instance.", 404);
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
3049
3364
|
// Browse API — fetch a URL and return what the agent sees (stripped text)
|
|
3050
3365
|
app.get("/api/browse", async (c) => {
|
|
3051
3366
|
const url = c.req.query("url");
|
|
@@ -3159,6 +3474,7 @@ app.get("/api/ops/health", async (c) => {
|
|
|
3159
3474
|
},
|
|
3160
3475
|
provider: resolveProvider(),
|
|
3161
3476
|
airplaneMode: settings.airplaneMode,
|
|
3477
|
+
privateMode: settings.privateMode,
|
|
3162
3478
|
models: settings.models,
|
|
3163
3479
|
sidecars: {
|
|
3164
3480
|
search: isSidecarAvailable(),
|
|
@@ -3378,6 +3694,30 @@ app.delete("/api/ops/projects/:id", async (c) => {
|
|
|
3378
3694
|
return c.json({ error: "Project not found" }, 404);
|
|
3379
3695
|
return c.json({ ok: true });
|
|
3380
3696
|
});
|
|
3697
|
+
// --- Posture API (UI surface assembly) ---
|
|
3698
|
+
app.get("/api/posture", (c) => {
|
|
3699
|
+
return c.json({
|
|
3700
|
+
posture: getPosture(),
|
|
3701
|
+
surface: getSurface(),
|
|
3702
|
+
state: getPostureState(),
|
|
3703
|
+
});
|
|
3704
|
+
});
|
|
3705
|
+
app.put("/api/posture", async (c) => {
|
|
3706
|
+
const body = await c.req.json();
|
|
3707
|
+
if (body.posture && ["silent", "pulse", "board"].includes(body.posture)) {
|
|
3708
|
+
if (body.pinned !== false) {
|
|
3709
|
+
pinPosture(body.posture);
|
|
3710
|
+
}
|
|
3711
|
+
else {
|
|
3712
|
+
pinPosture(body.posture);
|
|
3713
|
+
unpinPosture();
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
else if (body.pinned === false) {
|
|
3717
|
+
unpinPosture();
|
|
3718
|
+
}
|
|
3719
|
+
return c.json({ posture: getPosture(), surface: getSurface(), state: getPostureState() });
|
|
3720
|
+
});
|
|
3381
3721
|
// --- Pulse (nervous system) endpoint ---
|
|
3382
3722
|
app.get("/api/pulse/status", (c) => {
|
|
3383
3723
|
const integrator = getPressureIntegrator();
|
|
@@ -3393,6 +3733,85 @@ app.get("/api/pulse/history", (c) => {
|
|
|
3393
3733
|
}
|
|
3394
3734
|
return c.json(integrator.getVoltageHistory());
|
|
3395
3735
|
});
|
|
3736
|
+
// --- Nerve API (three-dot goo) ---
|
|
3737
|
+
import { getNerveState } from "./nerve/state.js";
|
|
3738
|
+
import { initPush, getVapidPublicKey, addSubscription, checkAndNotify, startPushMonitor, stopPushMonitor } from "./nerve/push.js";
|
|
3739
|
+
import { loadPosture, startDecayTimer, getPosture, getPostureState, getSurface, pinPosture, unpinPosture, } from "./posture/engine.js";
|
|
3740
|
+
import { postureTracker, requireSurface, postureHeader } from "./posture/middleware.js";
|
|
3741
|
+
// State endpoint — three dots
|
|
3742
|
+
app.get("/api/nerve/state", async (c) => {
|
|
3743
|
+
const state = await getNerveState();
|
|
3744
|
+
return c.json(state);
|
|
3745
|
+
});
|
|
3746
|
+
// VAPID public key for push subscription
|
|
3747
|
+
app.get("/api/nerve/vapid-key", (c) => {
|
|
3748
|
+
try {
|
|
3749
|
+
return c.json({ key: getVapidPublicKey() });
|
|
3750
|
+
}
|
|
3751
|
+
catch {
|
|
3752
|
+
return c.json({ error: "Push not initialized" }, 503);
|
|
3753
|
+
}
|
|
3754
|
+
});
|
|
3755
|
+
// Store push subscription from a nerve endpoint
|
|
3756
|
+
app.post("/api/nerve/subscribe", async (c) => {
|
|
3757
|
+
const body = await c.req.json();
|
|
3758
|
+
const { subscription, label } = body;
|
|
3759
|
+
if (!subscription?.endpoint || !subscription?.keys) {
|
|
3760
|
+
return c.json({ error: "Invalid subscription" }, 400);
|
|
3761
|
+
}
|
|
3762
|
+
const id = await addSubscription(subscription, label);
|
|
3763
|
+
return c.json({ id });
|
|
3764
|
+
});
|
|
3765
|
+
// SSE stream — real-time nerve state updates
|
|
3766
|
+
app.get("/api/nerve/stream", async (c) => {
|
|
3767
|
+
return streamSSE(c, async (stream) => {
|
|
3768
|
+
// Send initial state
|
|
3769
|
+
const initial = await getNerveState();
|
|
3770
|
+
await stream.writeSSE({ event: "state", data: JSON.stringify(initial) });
|
|
3771
|
+
// Poll every 5 seconds and send updates
|
|
3772
|
+
let lastJson = JSON.stringify(initial);
|
|
3773
|
+
const interval = setInterval(async () => {
|
|
3774
|
+
try {
|
|
3775
|
+
const state = await getNerveState();
|
|
3776
|
+
const json = JSON.stringify(state);
|
|
3777
|
+
if (json !== lastJson) {
|
|
3778
|
+
lastJson = json;
|
|
3779
|
+
await stream.writeSSE({ event: "state", data: json });
|
|
3780
|
+
// Check if push notifications should fire
|
|
3781
|
+
await checkAndNotify(state).catch(() => { });
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
catch { /* stream may be closed */ }
|
|
3785
|
+
}, 5000);
|
|
3786
|
+
// Keep alive
|
|
3787
|
+
const keepAlive = setInterval(async () => {
|
|
3788
|
+
try {
|
|
3789
|
+
await stream.writeSSE({ event: "ping", data: "" });
|
|
3790
|
+
}
|
|
3791
|
+
catch { /* ok */ }
|
|
3792
|
+
}, 30000);
|
|
3793
|
+
stream.onAbort(() => {
|
|
3794
|
+
clearInterval(interval);
|
|
3795
|
+
clearInterval(keepAlive);
|
|
3796
|
+
});
|
|
3797
|
+
// Hold the stream open
|
|
3798
|
+
await new Promise(() => { });
|
|
3799
|
+
});
|
|
3800
|
+
});
|
|
3801
|
+
// Update: accept a pending major update
|
|
3802
|
+
app.post("/api/nerve/accept-update", async (c) => {
|
|
3803
|
+
try {
|
|
3804
|
+
const { acceptMajorUpdate } = await import("./updater.js");
|
|
3805
|
+
const { clearPendingUpdate } = await import("./nerve/state.js");
|
|
3806
|
+
clearPendingUpdate();
|
|
3807
|
+
// This will restart the process after updating
|
|
3808
|
+
await acceptMajorUpdate();
|
|
3809
|
+
return c.json({ ok: true, message: "Updating and restarting..." });
|
|
3810
|
+
}
|
|
3811
|
+
catch (err) {
|
|
3812
|
+
return c.json({ error: err.message }, 500);
|
|
3813
|
+
}
|
|
3814
|
+
});
|
|
3396
3815
|
// Chat: streamed response (or learn command)
|
|
3397
3816
|
app.post("/api/chat", async (c) => {
|
|
3398
3817
|
const body = await c.req.json();
|
|
@@ -4091,7 +4510,16 @@ app.post("/api/chat", async (c) => {
|
|
|
4091
4510
|
ctx.messages[lastIdx] = { role: "user", content: userContent };
|
|
4092
4511
|
}
|
|
4093
4512
|
}
|
|
4094
|
-
|
|
4513
|
+
let stream_fn;
|
|
4514
|
+
try {
|
|
4515
|
+
stream_fn = pickStreamFn();
|
|
4516
|
+
}
|
|
4517
|
+
catch (err) {
|
|
4518
|
+
if (err instanceof PrivateModeError) {
|
|
4519
|
+
return c.json({ error: err.message }, 503);
|
|
4520
|
+
}
|
|
4521
|
+
throw err;
|
|
4522
|
+
}
|
|
4095
4523
|
const activeProvider = resolveProvider();
|
|
4096
4524
|
const activeChatModel = resolveChatModel();
|
|
4097
4525
|
const reqSignal = c.req.raw.signal;
|
|
@@ -4118,15 +4546,30 @@ app.post("/api/chat", async (c) => {
|
|
|
4118
4546
|
resolve();
|
|
4119
4547
|
};
|
|
4120
4548
|
reqSignal?.addEventListener("abort", onAbort, { once: true });
|
|
4549
|
+
// Token buffer for split-placeholder rehydration
|
|
4550
|
+
let tokenBuf2 = "";
|
|
4551
|
+
const flushBuf2 = () => {
|
|
4552
|
+
if (!tokenBuf2)
|
|
4553
|
+
return;
|
|
4554
|
+
const rehydrated = rehydrateResponse(tokenBuf2);
|
|
4555
|
+
tokenBuf2 = "";
|
|
4556
|
+
stream.writeSSE({ data: JSON.stringify({ token: rehydrated }) }).catch(() => { });
|
|
4557
|
+
};
|
|
4121
4558
|
stream_fn({
|
|
4122
4559
|
messages: ctx.messages,
|
|
4123
4560
|
model: activeChatModel,
|
|
4124
4561
|
signal: reqSignal,
|
|
4125
4562
|
onToken: (token) => {
|
|
4126
4563
|
fullResponse += token;
|
|
4127
|
-
|
|
4564
|
+
tokenBuf2 += token;
|
|
4565
|
+
// Hold if buffer ends with partial placeholder
|
|
4566
|
+
const lastOpen = tokenBuf2.lastIndexOf("<<");
|
|
4567
|
+
if (lastOpen !== -1 && tokenBuf2.indexOf(">>", lastOpen) === -1)
|
|
4568
|
+
return;
|
|
4569
|
+
flushBuf2();
|
|
4128
4570
|
},
|
|
4129
4571
|
onDone: () => {
|
|
4572
|
+
flushBuf2(); // flush remainder
|
|
4130
4573
|
reqSignal?.removeEventListener("abort", onAbort);
|
|
4131
4574
|
// Process action blocks BEFORE sending done — ensures SSE events reach client before stream closes.
|
|
4132
4575
|
// ALWAYS strip [AGENT_REQUEST] blocks from response, even if they can't be parsed.
|
|
@@ -4347,14 +4790,20 @@ app.post("/api/chat", async (c) => {
|
|
|
4347
4790
|
resolve();
|
|
4348
4791
|
}
|
|
4349
4792
|
},
|
|
4350
|
-
onError: (err) => {
|
|
4793
|
+
onError: async (err) => {
|
|
4794
|
+
flushBuf2();
|
|
4351
4795
|
reqSignal?.removeEventListener("abort", onAbort);
|
|
4352
4796
|
// If this error is from an abort, save partial and exit quietly
|
|
4353
4797
|
if (reqSignal?.aborted) {
|
|
4354
4798
|
savePartial();
|
|
4355
4799
|
}
|
|
4356
4800
|
else {
|
|
4357
|
-
|
|
4801
|
+
let errorMsg = err instanceof LLMError ? err.userMessage : (err.message || "Stream error");
|
|
4802
|
+
if (isPrivateMode() && /ECONNREFUSED|fetch failed|network|socket/i.test(errorMsg)) {
|
|
4803
|
+
const health = await checkOllamaHealth();
|
|
4804
|
+
if (!health.ok)
|
|
4805
|
+
errorMsg += " — Check that Ollama is running. " + health.message;
|
|
4806
|
+
}
|
|
4358
4807
|
stream.writeSSE({ data: JSON.stringify({ error: errorMsg }) }).catch(() => { });
|
|
4359
4808
|
}
|
|
4360
4809
|
resolve(); // Still resolve so stream closes
|
|
@@ -4363,8 +4812,111 @@ app.post("/api/chat", async (c) => {
|
|
|
4363
4812
|
});
|
|
4364
4813
|
});
|
|
4365
4814
|
});
|
|
4815
|
+
// --- Freeze endpoint (operator clicks freeze link) ---
|
|
4816
|
+
app.post("/api/freeze", async (c) => {
|
|
4817
|
+
const body = await c.req.json().catch(() => null);
|
|
4818
|
+
if (!body?.signal)
|
|
4819
|
+
return c.json({ error: "Missing freeze signal" }, 400);
|
|
4820
|
+
const { freeze, isFrozen } = await import("./tier/freeze.js");
|
|
4821
|
+
if (isFrozen())
|
|
4822
|
+
return c.json({ error: "Already frozen" }, 409);
|
|
4823
|
+
await freeze(body.signal, process.cwd());
|
|
4824
|
+
return c.json({ status: "frozen", message: "All agents dormant." });
|
|
4825
|
+
});
|
|
4826
|
+
app.post("/api/thaw", async (c) => {
|
|
4827
|
+
const { thaw } = await import("./tier/freeze.js");
|
|
4828
|
+
await thaw(process.cwd());
|
|
4829
|
+
return c.json({ status: "thawed", message: "Operations resuming." });
|
|
4830
|
+
});
|
|
4831
|
+
app.get("/api/freeze/status", async (c) => {
|
|
4832
|
+
const { isFrozen, getFreezeSignal } = await import("./tier/freeze.js");
|
|
4833
|
+
return c.json({ frozen: isFrozen(), signal: getFreezeSignal() });
|
|
4834
|
+
});
|
|
4835
|
+
// --- Brain import API ---
|
|
4836
|
+
app.post("/api/import/folder", async (c) => {
|
|
4837
|
+
const body = await c.req.json();
|
|
4838
|
+
if (!body.paths || !Array.isArray(body.paths) || body.paths.length === 0) {
|
|
4839
|
+
return c.json({ error: "paths[] required" }, 400);
|
|
4840
|
+
}
|
|
4841
|
+
const { resolve } = await import("node:path");
|
|
4842
|
+
const { importToBrain } = await import("./files/import.js");
|
|
4843
|
+
const result = await importToBrain({
|
|
4844
|
+
sources: body.paths.map(p => resolve(p)),
|
|
4845
|
+
brainRoot: process.cwd(),
|
|
4846
|
+
dryRun: body.dryRun ?? false,
|
|
4847
|
+
});
|
|
4848
|
+
// After import: fast pass → deep pass (both background, chained)
|
|
4849
|
+
if (!body.dryRun && result.imported > 0) {
|
|
4850
|
+
import("./files/index-local.js").then(({ indexImportedFiles }) => {
|
|
4851
|
+
indexImportedFiles({ localOnly: true })
|
|
4852
|
+
.then(() => import("./files/deep-index.js"))
|
|
4853
|
+
.then(({ runDeepIndex }) => runDeepIndex())
|
|
4854
|
+
.catch(() => { });
|
|
4855
|
+
});
|
|
4856
|
+
}
|
|
4857
|
+
return c.json(result);
|
|
4858
|
+
});
|
|
4859
|
+
app.post("/api/import/index", async (c) => {
|
|
4860
|
+
const { indexImportedFiles } = await import("./files/index-local.js");
|
|
4861
|
+
const result = await indexImportedFiles({ localOnly: true });
|
|
4862
|
+
// After fast pass, kick off deep index in background
|
|
4863
|
+
import("./files/deep-index.js").then(({ runDeepIndex }) => {
|
|
4864
|
+
runDeepIndex().catch(() => { });
|
|
4865
|
+
});
|
|
4866
|
+
return c.json(result);
|
|
4867
|
+
});
|
|
4868
|
+
app.post("/api/import/deep-index", async (c) => {
|
|
4869
|
+
const { runDeepIndex } = await import("./files/deep-index.js");
|
|
4870
|
+
const result = await runDeepIndex();
|
|
4871
|
+
return c.json(result);
|
|
4872
|
+
});
|
|
4873
|
+
app.get("/api/import/deep-index/progress", async (c) => {
|
|
4874
|
+
const { getDeepIndexProgress } = await import("./files/deep-index.js");
|
|
4875
|
+
return c.json(getDeepIndexProgress());
|
|
4876
|
+
});
|
|
4877
|
+
app.get("/api/import/deep-index/results", async (c) => {
|
|
4878
|
+
const { readFile: rf } = await import("node:fs/promises");
|
|
4879
|
+
const { join: jp } = await import("node:path");
|
|
4880
|
+
try {
|
|
4881
|
+
const raw = await rf(jp(process.cwd(), "brain", ".core", "deep-index.json"), "utf-8");
|
|
4882
|
+
return c.json(JSON.parse(raw));
|
|
4883
|
+
}
|
|
4884
|
+
catch {
|
|
4885
|
+
return c.json({ entities: [], themes: [], crossRefs: [], flags: [], deepIndexed: [] });
|
|
4886
|
+
}
|
|
4887
|
+
});
|
|
4888
|
+
app.post("/api/import/files", async (c) => {
|
|
4889
|
+
const formData = await c.req.formData();
|
|
4890
|
+
const files = formData.getAll("files");
|
|
4891
|
+
if (files.length === 0)
|
|
4892
|
+
return c.json({ error: "No files provided" }, 400);
|
|
4893
|
+
const { mkdir: mkdirFs, writeFile: writeFs } = await import("node:fs/promises");
|
|
4894
|
+
const { join: joinPath } = await import("node:path");
|
|
4895
|
+
const ingestDir = joinPath(process.cwd(), "ingest");
|
|
4896
|
+
await mkdirFs(ingestDir, { recursive: true });
|
|
4897
|
+
const saved = [];
|
|
4898
|
+
for (const file of files) {
|
|
4899
|
+
if (!file.name)
|
|
4900
|
+
continue;
|
|
4901
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
4902
|
+
const dest = joinPath(ingestDir, file.name);
|
|
4903
|
+
await writeFs(dest, buffer);
|
|
4904
|
+
saved.push(file.name);
|
|
4905
|
+
}
|
|
4906
|
+
// Process the ingest folder immediately
|
|
4907
|
+
const { processIngestFolder } = await import("./files/ingest-folder.js");
|
|
4908
|
+
const ingestedDir = joinPath(process.cwd(), "ingested");
|
|
4909
|
+
const result = await processIngestFolder(ingestDir, ingestedDir);
|
|
4910
|
+
return c.json({
|
|
4911
|
+
saved: saved.length,
|
|
4912
|
+
files: saved,
|
|
4913
|
+
ingested: result.newFiles,
|
|
4914
|
+
});
|
|
4915
|
+
});
|
|
4366
4916
|
// --- Startup ---
|
|
4367
|
-
async function start() {
|
|
4917
|
+
async function start(opts) {
|
|
4918
|
+
const tier = opts?.tier ?? "byok";
|
|
4919
|
+
const tierGate = await import("./tier/gate.js");
|
|
4368
4920
|
// Initialize instance name before anything else
|
|
4369
4921
|
initInstanceName();
|
|
4370
4922
|
// Initialize OpenTelemetry tracing (must be early, before instrumented code)
|
|
@@ -4375,6 +4927,16 @@ async function start() {
|
|
|
4375
4927
|
});
|
|
4376
4928
|
// Load settings (airplane mode, model selection)
|
|
4377
4929
|
const settings = await loadSettings();
|
|
4930
|
+
// Initialize posture system (UI surface assembly)
|
|
4931
|
+
await loadPosture();
|
|
4932
|
+
startDecayTimer();
|
|
4933
|
+
// Install fetch guard before any routes or outbound calls
|
|
4934
|
+
installFetchGuard();
|
|
4935
|
+
// Initialize PrivacyMembrane for reversible redaction
|
|
4936
|
+
const sensitiveRegistry = new SensitiveRegistry();
|
|
4937
|
+
await sensitiveRegistry.load(BRAIN_DIR);
|
|
4938
|
+
const membrane = new PrivacyMembrane(sensitiveRegistry);
|
|
4939
|
+
setActiveMembrane(membrane);
|
|
4378
4940
|
// Run independent initialization in parallel: LLM cache, auth, and sidecars
|
|
4379
4941
|
const [, pairingCode, sidecarResults] = await Promise.all([
|
|
4380
4942
|
// LLM response cache (file-backed in .core/cache/, 1-hour TTL)
|
|
@@ -4491,6 +5053,17 @@ async function start() {
|
|
|
4491
5053
|
log.info(`Boot tension: ${todoCount} todo(s) found — pulse should fire`);
|
|
4492
5054
|
}
|
|
4493
5055
|
}
|
|
5056
|
+
// Initialize nerve push notifications (VAPID keys + subscriptions)
|
|
5057
|
+
try {
|
|
5058
|
+
await initPush();
|
|
5059
|
+
startPushMonitor(getNerveState, 30_000);
|
|
5060
|
+
log.info("Nerve push notifications ready (monitoring every 30s)");
|
|
5061
|
+
}
|
|
5062
|
+
catch (err) {
|
|
5063
|
+
log.warn("Nerve push init failed — push notifications disabled", {
|
|
5064
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5065
|
+
});
|
|
5066
|
+
}
|
|
4494
5067
|
// Start weekly backlog review timer (runs every Friday — DASH-59)
|
|
4495
5068
|
startBacklogReviewTimer(queueProvider.getStore());
|
|
4496
5069
|
// Start daily morning briefing timer (DASH-44)
|
|
@@ -4527,17 +5100,24 @@ async function start() {
|
|
|
4527
5100
|
logActivity({ source: "agent", summary: `Continuation error: ${msg}` });
|
|
4528
5101
|
}
|
|
4529
5102
|
});
|
|
4530
|
-
// Initialize
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
5103
|
+
// Initialize agent spawning — tier >= spawn only
|
|
5104
|
+
if (tierGate.canSpawn(tier)) {
|
|
5105
|
+
// Initialize instance manager (GC, health checks, load balancing)
|
|
5106
|
+
instanceManager = new AgentInstanceManager(runtime);
|
|
5107
|
+
await instanceManager.init();
|
|
5108
|
+
// Initialize agent pool (circuit breakers, isolation, resource management)
|
|
5109
|
+
agentPool = AgentPool.fromExisting(runtime, instanceManager);
|
|
5110
|
+
setAgentPool(agentPool);
|
|
5111
|
+
// Initialize workflow engine for multi-agent coordination
|
|
5112
|
+
workflowEngine = new WorkflowEngine(agentPool);
|
|
5113
|
+
await workflowEngine.loadAllDefinitions().catch((err) => {
|
|
5114
|
+
log.warn("Failed to load workflow definitions", { error: err instanceof Error ? err.message : String(err) });
|
|
5115
|
+
});
|
|
5116
|
+
log.info(`Agent spawning enabled (tier: ${tier})`);
|
|
5117
|
+
}
|
|
5118
|
+
else {
|
|
5119
|
+
log.info(`Agent spawning disabled (tier: ${tier} — requires spawn tier)`);
|
|
5120
|
+
}
|
|
4541
5121
|
// --- Register component health checks (after all systems initialized) ---
|
|
4542
5122
|
// Queue store: can we read the JSONL file?
|
|
4543
5123
|
health.register("queue", queueStoreCheck(() => queueProvider.getStore()), { critical: false });
|
|
@@ -4561,25 +5141,27 @@ async function start() {
|
|
|
4561
5141
|
recovery.start();
|
|
4562
5142
|
// Start alert evaluation loop (evaluates every 30s)
|
|
4563
5143
|
alertManager.start();
|
|
4564
|
-
// Wire notification channels —
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
5144
|
+
// Wire notification channels — tier >= byok only (requires BYOK API keys)
|
|
5145
|
+
if (tierGate.canAlert(tier)) {
|
|
5146
|
+
const resendKey = process.env.RESEND_API_KEY;
|
|
5147
|
+
if (resendKey) {
|
|
5148
|
+
alertDispatcher.add(new EmailChannel({
|
|
5149
|
+
endpoint: "https://api.resend.com/emails",
|
|
5150
|
+
apiKey: resendKey,
|
|
5151
|
+
from: `${getInstanceName()} <${getAlertEmailFrom()}>`,
|
|
5152
|
+
to: [resolveEnv("ALERT_EMAIL_TO") ?? ""].filter(Boolean),
|
|
5153
|
+
}));
|
|
5154
|
+
log.info("Alert channel: email (Resend)");
|
|
5155
|
+
}
|
|
5156
|
+
if (process.env.TWILIO_ACCOUNT_SID) {
|
|
5157
|
+
alertDispatcher.add(new PhoneChannel());
|
|
5158
|
+
log.info("Alert channel: phone (Twilio voice)");
|
|
5159
|
+
}
|
|
5160
|
+
alertManager.updateNotifications([
|
|
5161
|
+
{ channel: "email", minSeverity: "warning" },
|
|
5162
|
+
{ channel: "phone", minSeverity: "critical" },
|
|
5163
|
+
]);
|
|
5164
|
+
}
|
|
4583
5165
|
// Start credit monitoring (checks every 5 min, configurable via CORE_CREDIT_CHECK_INTERVAL_MS)
|
|
4584
5166
|
startCreditMonitor(health, alertManager);
|
|
4585
5167
|
// Avatar sidecar launches in background — slow (model loading) but non-blocking
|
|
@@ -4601,7 +5183,21 @@ async function start() {
|
|
|
4601
5183
|
log.info(`${getInstanceName()} — Local Chat starting`);
|
|
4602
5184
|
// Show LLM provider based on settings
|
|
4603
5185
|
const provider = resolveProvider();
|
|
4604
|
-
if (settings.
|
|
5186
|
+
if (settings.privateMode) {
|
|
5187
|
+
log.info("Mode: PRIVATE (network-isolated), LLM: Ollama only — cloud providers blocked");
|
|
5188
|
+
// Fail loudly at startup if Ollama isn't available in private mode
|
|
5189
|
+
try {
|
|
5190
|
+
const { assertOllamaAvailable } = await import("./llm/guard.js");
|
|
5191
|
+
await assertOllamaAvailable();
|
|
5192
|
+
log.info("Ollama: reachable ✓");
|
|
5193
|
+
}
|
|
5194
|
+
catch (err) {
|
|
5195
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5196
|
+
log.error(msg);
|
|
5197
|
+
throw new Error("Cannot start in privateMode without Ollama. " + msg);
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
else if (settings.airplaneMode) {
|
|
4605
5201
|
log.info("Mode: Airplane (local only), LLM: Ollama");
|
|
4606
5202
|
}
|
|
4607
5203
|
else {
|
|
@@ -4767,10 +5363,46 @@ async function start() {
|
|
|
4767
5363
|
return reply.trim();
|
|
4768
5364
|
});
|
|
4769
5365
|
log.info(`${getInstanceName()} email handler registered — emails with '${getInstanceName()}' in subject will be auto-replied`);
|
|
4770
|
-
|
|
4771
|
-
|
|
5366
|
+
await new Promise((resolve) => {
|
|
5367
|
+
const server = serve({ fetch: app.fetch, port: PORT }, () => {
|
|
5368
|
+
const addr = server.address();
|
|
5369
|
+
if (typeof addr === "object" && addr) {
|
|
5370
|
+
actualPort = addr.port;
|
|
5371
|
+
}
|
|
5372
|
+
log.info(`Listening on http://localhost:${actualPort}`);
|
|
5373
|
+
// Show LAN IP for phone access
|
|
5374
|
+
try {
|
|
5375
|
+
import("node:os").then(({ networkInterfaces }) => {
|
|
5376
|
+
const nets = networkInterfaces();
|
|
5377
|
+
for (const name of Object.keys(nets)) {
|
|
5378
|
+
for (const net of nets[name] ?? []) {
|
|
5379
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
5380
|
+
log.info(`Nerve (phone): http://${net.address}:${actualPort}/nerve`);
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
});
|
|
5385
|
+
}
|
|
5386
|
+
catch { /* ok */ }
|
|
5387
|
+
// Announce on LAN if mesh.lanAnnounce is enabled AND tier >= byok
|
|
5388
|
+
if (tierGate.canMesh(tier) && getMeshConfig().lanAnnounce) {
|
|
5389
|
+
try {
|
|
5390
|
+
startMdns(actualPort);
|
|
5391
|
+
}
|
|
5392
|
+
catch (err) {
|
|
5393
|
+
log.warn("mDNS announcement failed — discovery disabled", {
|
|
5394
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
resolve();
|
|
5399
|
+
});
|
|
4772
5400
|
});
|
|
4773
5401
|
}
|
|
5402
|
+
/** Returns the port the server is actually listening on (resolves port 0). */
|
|
5403
|
+
export function getActualPort() {
|
|
5404
|
+
return actualPort;
|
|
5405
|
+
}
|
|
4774
5406
|
export { start };
|
|
4775
5407
|
const isDirectRun = process.argv[1]?.replace(/\\/g, "/").includes("server");
|
|
4776
5408
|
if (isDirectRun) {
|
|
@@ -4779,6 +5411,87 @@ if (isDirectRun) {
|
|
|
4779
5411
|
process.exit(1);
|
|
4780
5412
|
});
|
|
4781
5413
|
}
|
|
5414
|
+
function parseRoadmapYaml(raw) {
|
|
5415
|
+
const result = { phases: [], streams: [], milestones: [] };
|
|
5416
|
+
const lines = raw.split("\n");
|
|
5417
|
+
let section = null;
|
|
5418
|
+
let currentObj = null;
|
|
5419
|
+
function flush() {
|
|
5420
|
+
if (currentObj && section && section !== "layout") {
|
|
5421
|
+
result[section].push(currentObj);
|
|
5422
|
+
currentObj = null;
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
for (const line of lines) {
|
|
5426
|
+
const trimmed = line.trimEnd();
|
|
5427
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
5428
|
+
continue;
|
|
5429
|
+
// Top-level section headers
|
|
5430
|
+
if (/^layout:\s*$/.test(trimmed)) {
|
|
5431
|
+
flush();
|
|
5432
|
+
section = "layout";
|
|
5433
|
+
result.layout = {};
|
|
5434
|
+
continue;
|
|
5435
|
+
}
|
|
5436
|
+
if (/^phases:\s*$/.test(trimmed)) {
|
|
5437
|
+
flush();
|
|
5438
|
+
section = "phases";
|
|
5439
|
+
continue;
|
|
5440
|
+
}
|
|
5441
|
+
if (/^streams:\s*$/.test(trimmed)) {
|
|
5442
|
+
flush();
|
|
5443
|
+
section = "streams";
|
|
5444
|
+
continue;
|
|
5445
|
+
}
|
|
5446
|
+
if (/^milestones:\s*$/.test(trimmed)) {
|
|
5447
|
+
flush();
|
|
5448
|
+
section = "milestones";
|
|
5449
|
+
continue;
|
|
5450
|
+
}
|
|
5451
|
+
if (!section)
|
|
5452
|
+
continue;
|
|
5453
|
+
// Layout is a flat object, not an array
|
|
5454
|
+
if (section === "layout") {
|
|
5455
|
+
const kvMatch = trimmed.match(/^\s+(\w+)\s*:\s*(.*)/);
|
|
5456
|
+
if (kvMatch && result.layout) {
|
|
5457
|
+
result.layout[kvMatch[1]] = kvMatch[2].replace(/^["']|["']$/g, "").trim();
|
|
5458
|
+
}
|
|
5459
|
+
continue;
|
|
5460
|
+
}
|
|
5461
|
+
// New list item: " - key: value"
|
|
5462
|
+
const newItemMatch = trimmed.match(/^\s+-\s+(\w+)\s*:\s*(.*)/);
|
|
5463
|
+
if (newItemMatch) {
|
|
5464
|
+
flush();
|
|
5465
|
+
currentObj = {};
|
|
5466
|
+
const key = newItemMatch[1];
|
|
5467
|
+
const val = newItemMatch[2].replace(/^["']|["']$/g, "").trim();
|
|
5468
|
+
if (val.startsWith("[")) {
|
|
5469
|
+
// Inline array: [a, b, c]
|
|
5470
|
+
currentObj[key] = val.slice(1, -1).split(",").map(s => s.trim().replace(/^["']|["']$/g, ""));
|
|
5471
|
+
}
|
|
5472
|
+
else {
|
|
5473
|
+
currentObj[key] = val;
|
|
5474
|
+
}
|
|
5475
|
+
continue;
|
|
5476
|
+
}
|
|
5477
|
+
// Continuation key: " key: value"
|
|
5478
|
+
if (currentObj) {
|
|
5479
|
+
const kvMatch = trimmed.match(/^\s+(\w+)\s*:\s*(.*)/);
|
|
5480
|
+
if (kvMatch) {
|
|
5481
|
+
const key = kvMatch[1];
|
|
5482
|
+
const val = kvMatch[2].replace(/^["']|["']$/g, "").trim();
|
|
5483
|
+
if (val.startsWith("[")) {
|
|
5484
|
+
currentObj[key] = val.slice(1, -1).split(",").map(s => s.trim().replace(/^["']|["']$/g, ""));
|
|
5485
|
+
}
|
|
5486
|
+
else {
|
|
5487
|
+
currentObj[key] = val;
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
flush();
|
|
5493
|
+
return result;
|
|
5494
|
+
}
|
|
4782
5495
|
// Graceful shutdown: agent pool first (drains queue, terminates agents, cleans resources),
|
|
4783
5496
|
// then sidecars, timers, and monitors.
|
|
4784
5497
|
async function gracefulShutdown(signal) {
|
|
@@ -4812,6 +5525,8 @@ async function gracefulShutdown(signal) {
|
|
|
4812
5525
|
stopInsightsTimer();
|
|
4813
5526
|
stopOpenLoopScanner();
|
|
4814
5527
|
stopCreditMonitor();
|
|
5528
|
+
stopPushMonitor();
|
|
5529
|
+
stopMdns();
|
|
4815
5530
|
stopAvatarSidecar();
|
|
4816
5531
|
stopTtsSidecar();
|
|
4817
5532
|
stopSttSidecar();
|