@runcore-sh/runcore 0.3.1 → 0.3.2

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.
Files changed (41) hide show
  1. package/dictionary.json +2 -2
  2. package/dist/agents/runtime/driver.d.ts.map +1 -1
  3. package/dist/agents/runtime/driver.js +1 -2
  4. package/dist/agents/runtime/driver.js.map +1 -1
  5. package/dist/agents/runtime/manager.d.ts.map +1 -1
  6. package/dist/agents/runtime/manager.js +1 -0
  7. package/dist/agents/runtime/manager.js.map +1 -1
  8. package/dist/agents/runtime/types.d.ts +2 -0
  9. package/dist/agents/runtime/types.d.ts.map +1 -1
  10. package/dist/auth/middleware.d.ts.map +1 -1
  11. package/dist/auth/middleware.js +2 -0
  12. package/dist/auth/middleware.js.map +1 -1
  13. package/dist/cli.d.ts +1 -1
  14. package/dist/cli.js +1 -1
  15. package/dist/llm/ollama.d.ts +5 -0
  16. package/dist/llm/ollama.d.ts.map +1 -1
  17. package/dist/llm/ollama.js +22 -1
  18. package/dist/llm/ollama.js.map +1 -1
  19. package/dist/llm/providers/ollama.d.ts +2 -2
  20. package/dist/llm/providers/ollama.d.ts.map +1 -1
  21. package/dist/llm/providers/ollama.js +63 -17
  22. package/dist/llm/providers/ollama.js.map +1 -1
  23. package/dist/server.d.ts.map +1 -1
  24. package/dist/server.js +85 -29
  25. package/dist/server.js.map +1 -1
  26. package/dist/ui-sync.d.ts +34 -0
  27. package/dist/ui-sync.d.ts.map +1 -0
  28. package/dist/ui-sync.js +108 -0
  29. package/dist/ui-sync.js.map +1 -0
  30. package/package.json +5 -5
  31. package/public/avatar/Hey-Dash_en_windows_v4_0_0.zip +0 -0
  32. package/public/avatar/README.md +43 -0
  33. package/public/avatar/dash_headhshot_v1.png +0 -0
  34. package/public/avatar/idle.mp4 +0 -0
  35. package/public/avatar/photo.png +0 -0
  36. package/public/index.html +114 -64
  37. package/public/nerve/icon-192.svg +6 -0
  38. package/public/nerve/icon-512.svg +6 -0
  39. package/public/nerve/index.html +698 -0
  40. package/public/nerve/manifest.json +24 -0
  41. package/public/nerve/sw.js +84 -0
package/dist/server.js CHANGED
@@ -9,12 +9,15 @@ import { serveStatic } from "@hono/node-server/serve-static";
9
9
  import { join, dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { readFile, writeFile, mkdir } from "node:fs/promises";
12
- // Package root (where public/ lives) — works whether run from CWD or npx
12
+ // Package root — works whether run from CWD or npx
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = dirname(__filename);
15
15
  const PKG_ROOT = join(__dirname, "..");
16
+ // UI directory — resolved at startup. Prefers CDN-synced, falls back to bundled.
17
+ let UI_DIR = getUiPublicDir(PKG_ROOT);
16
18
  import { writeFileSync } from "node:fs";
17
19
  import { initInstanceName, getInstanceName, setInstanceName, getInstanceNameLower, resolveEnv, getAlertEmailFrom } from "./instance.js";
20
+ import { syncUi, getUiPublicDir } from "./ui-sync.js";
18
21
  import { readBrainFile, writeBrainFile, appendBrainLine } from "./lib/brain-io.js";
19
22
  import { runWithAuditContext } from "./lib/audit.js";
20
23
  import { Brain } from "./brain.js";
@@ -186,6 +189,13 @@ async function getOrCreateChatSession(sessionId, name) {
186
189
  let cs = chatSessions.get(sessionId);
187
190
  if (cs)
188
191
  return cs;
192
+ // Single-user system: reuse existing chat session from any prior session ID.
193
+ // This ensures all tabs/devices see the same conversation history.
194
+ if (chatSessions.size > 0) {
195
+ const [existingId, existingCs] = chatSessions.entries().next().value;
196
+ chatSessions.set(sessionId, existingCs);
197
+ return existingCs;
198
+ }
189
199
  // Read custom personality instructions (empty string if file doesn't exist)
190
200
  let personality = "";
191
201
  try {
@@ -207,18 +217,17 @@ async function getOrCreateChatSession(sessionId, name) {
207
217
  await ltm.init();
208
218
  const brain = new Brain({
209
219
  systemPrompt: [
210
- `You are ${getInstanceName()}, a personal AI agent. You just came into existence and paired with your human partner, ${name}.`,
220
+ `IDENTITY:`,
221
+ `- Your name is ${getInstanceName()}.`,
222
+ `- The human you are talking to is named ${name}. When they say "my name" they mean "${name}".`,
223
+ `- You are ${name}'s personal AI agent, running locally on their machine. This conversation is private.`,
211
224
  ``,
212
- `CRITICAL RULES:`,
213
- `- NEVER invent information. You have no knowledge of reports, accounts, schedules, or tasks unless they appear in the context below.`,
214
- `- If context is provided below, reference ONLY that. If no context is provided, you know nothing yet — and that's okay.`,
215
- `- This is a new relationship. You and ${name} are just getting to know each other. Be curious. Ask real questions.`,
225
+ `RULES:`,
216
226
  `- Be warm, honest, and direct. Have personality. Don't be a corporate assistant.`,
217
- `- If you don't know something, say so plainly. Never fabricate details to seem helpful.`,
218
- `- NEVER reference board items, tasks, backlog items, or project work unless they appear verbatim in injected context below. If no "board issues" section is present, you know NOTHING about the board — do not guess, summarize from memory, or invent items.`,
219
- `- NEVER claim you searched the web unless "Web search results" appear in your context. If no search results are present, you did NOT search.`,
220
- ``,
221
- `You are running locally on ${name}'s machine. This conversation is private.`,
227
+ `- If you don't know something, say so. Never invent information.`,
228
+ `- Only reference data that appears in the context below. If nothing is provided, you know nothing yet.`,
229
+ `- NEVER reference board items, tasks, or project work unless they appear verbatim below.`,
230
+ `- NEVER claim you searched the web unless search results appear in your context.`,
222
231
  ...(personality ? [``, `--- Custom personality ---`, personality, `--- End custom personality ---`] : []),
223
232
  isSearchAvailable()
224
233
  ? `You have web search capability. When search results appear in your context, use them to answer. You don't control when searches happen — the system handles that automatically.`
@@ -354,8 +363,8 @@ app.onError((err, c) => {
354
363
  log.error("Unhandled route error", { error: msg, stack, path: c.req.path, method: c.req.method });
355
364
  return c.json({ error: msg }, 500);
356
365
  });
357
- // Serve static files from public/ (relative to package, not CWD)
358
- app.use("/public/*", serveStatic({ root: PKG_ROOT }));
366
+ // Serve static files from UI directory (CDN-synced or bundled fallback)
367
+ app.use("/public/*", serveStatic({ root: join(UI_DIR, ".."), rewriteRequestPath: (p) => p }));
359
368
  // --- HTML template cache (replaces {{INSTANCE_NAME}} with configured name) ---
360
369
  const htmlCache = new Map();
361
370
  async function serveHtmlTemplate(filePath) {
@@ -373,12 +382,12 @@ async function serveHtmlTemplate(filePath) {
373
382
  app.use("/api/*", requireSession());
374
383
  // Serve index.html at root
375
384
  app.get("/", async (c) => {
376
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "index.html"));
385
+ const html = await serveHtmlTemplate(join(UI_DIR, "index.html"));
377
386
  return c.html(html);
378
387
  });
379
388
  // --- Nerve endpoint (PWA) ---
380
389
  app.get("/nerve", async (c) => {
381
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "nerve", "index.html"));
390
+ const html = await serveHtmlTemplate(join(UI_DIR, "nerve", "index.html"));
382
391
  return c.html(html);
383
392
  });
384
393
  // --- Audit context middleware ---
@@ -1267,6 +1276,25 @@ app.put("/api/prompt", async (c) => {
1267
1276
  await writeBrainFile(PERSONALITY_PATH, prompt ?? "");
1268
1277
  return c.json({ ok: true });
1269
1278
  });
1279
+ // --- Model discovery ---
1280
+ app.get("/api/models", async (c) => {
1281
+ const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
1282
+ try {
1283
+ const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });
1284
+ if (!res.ok)
1285
+ return c.json({ models: [], error: "Ollama not responding" });
1286
+ const data = await res.json();
1287
+ const models = (data.models ?? []).map((m) => ({
1288
+ name: m.name,
1289
+ size: m.size,
1290
+ modified: m.modified_at,
1291
+ }));
1292
+ return c.json({ models });
1293
+ }
1294
+ catch {
1295
+ return c.json({ models: [], error: "Ollama not reachable" });
1296
+ }
1297
+ });
1270
1298
  // --- Settings routes ---
1271
1299
  app.get("/api/settings", async (c) => {
1272
1300
  const settings = getSettings();
@@ -1391,7 +1419,7 @@ app.get("/api/avatar/video/:hash", async (c) => {
1391
1419
  if (!/^[a-f0-9]+\.mp4$/.test(hash)) {
1392
1420
  return c.json({ error: "Invalid hash" }, 400);
1393
1421
  }
1394
- const filePath = join(PKG_ROOT, "public", "avatar", "cache", hash);
1422
+ const filePath = join(UI_DIR, "avatar", "cache", hash);
1395
1423
  try {
1396
1424
  const mp4 = await readFile(filePath);
1397
1425
  return new Response(mp4, {
@@ -1421,7 +1449,7 @@ app.post("/api/avatar/photo", async (c) => {
1421
1449
  return c.json({ error: "Photo body required" }, 400);
1422
1450
  const avatarConfig = getAvatarConfig();
1423
1451
  const photoPath = join(process.cwd(), avatarConfig.photoPath);
1424
- await mkdir(join(PKG_ROOT, "public", "avatar"), { recursive: true });
1452
+ await mkdir(join(UI_DIR, "avatar"), { recursive: true });
1425
1453
  await writeFile(photoPath, Buffer.from(body));
1426
1454
  const ok = await preparePhoto(photoPath);
1427
1455
  if (ok) {
@@ -1480,6 +1508,22 @@ app.get("/api/history", async (c) => {
1480
1508
  .map((m) => ({ role: m.role, content: m.content }));
1481
1509
  return c.json({ messages });
1482
1510
  });
1511
+ // Persist intro message so it appears in all tabs/devices
1512
+ app.post("/api/history/intro", async (c) => {
1513
+ const body = await c.req.json();
1514
+ const { sessionId, message } = body;
1515
+ if (!sessionId || !message)
1516
+ return c.json({ error: "sessionId and message required" }, 400);
1517
+ const session = validateSession(sessionId);
1518
+ if (!session)
1519
+ return c.json({ error: "Invalid session" }, 401);
1520
+ const cs = await getOrCreateChatSession(sessionId, session.name);
1521
+ // Only add if history is empty (first run)
1522
+ if (cs.history.length === 0) {
1523
+ cs.history.push({ role: "assistant", content: message });
1524
+ }
1525
+ return c.json({ ok: true });
1526
+ });
1483
1527
  // Activity log: poll for background actions
1484
1528
  app.get("/api/activity", async (c) => {
1485
1529
  const sessionId = c.req.query("sessionId");
@@ -1553,7 +1597,7 @@ app.post("/api/branch", async (c) => {
1553
1597
  data: JSON.stringify({
1554
1598
  meta: {
1555
1599
  provider: activeProvider,
1556
- model: activeChatModel ?? (activeProvider === "ollama" ? "llama3.1:8b" : "claude-sonnet-4"),
1600
+ model: activeChatModel ?? (activeProvider === "ollama" ? "llama3.2:3b" : "claude-sonnet-4"),
1557
1601
  traceId: branchTraceId,
1558
1602
  backref: primaryBackref,
1559
1603
  },
@@ -3233,7 +3277,7 @@ app.get("/api/cache", (c) => {
3233
3277
  });
3234
3278
  // --- Help page routes (no auth — knowledge exchange for other AIs/humans) ---
3235
3279
  app.get("/help", async (c) => {
3236
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "help.html"));
3280
+ const html = await serveHtmlTemplate(join(UI_DIR, "help.html"));
3237
3281
  return c.html(html);
3238
3282
  });
3239
3283
  app.get("/api/help/context", async (c) => {
@@ -3276,33 +3320,33 @@ app.get("/api/help/context", async (c) => {
3276
3320
  // --- Ops dashboard routes (posture-gated: board level) ---
3277
3321
  // Board-level pages — only assembled when user has shown intent for full visibility
3278
3322
  app.get("/observatory", requireSurface("pages"), async (c) => {
3279
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "observatory.html"));
3323
+ const html = await serveHtmlTemplate(join(UI_DIR, "observatory.html"));
3280
3324
  return c.html(html);
3281
3325
  });
3282
3326
  app.get("/ops", requireSurface("pages"), async (c) => {
3283
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "ops.html"));
3327
+ const html = await serveHtmlTemplate(join(UI_DIR, "ops.html"));
3284
3328
  return c.html(html);
3285
3329
  });
3286
3330
  app.get("/board", requireSurface("pages"), async (c) => {
3287
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "board.html"));
3331
+ const html = await serveHtmlTemplate(join(UI_DIR, "board.html"));
3288
3332
  return c.html(html);
3289
3333
  });
3290
3334
  app.get("/library", requireSurface("pages"), async (c) => {
3291
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "library.html"));
3335
+ const html = await serveHtmlTemplate(join(UI_DIR, "library.html"));
3292
3336
  return c.html(html);
3293
3337
  });
3294
3338
  app.get("/browser", requireSurface("pages"), async (c) => {
3295
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "browser.html"));
3339
+ const html = await serveHtmlTemplate(join(UI_DIR, "browser.html"));
3296
3340
  return c.html(html);
3297
3341
  });
3298
3342
  // Registry is always available — it's the entry point
3299
3343
  app.get("/registry", async (c) => {
3300
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "registry.html"));
3344
+ const html = await serveHtmlTemplate(join(UI_DIR, "registry.html"));
3301
3345
  return c.html(html);
3302
3346
  });
3303
3347
  // Serve roadmap.html (strategic roadmap & rearview)
3304
3348
  app.get("/roadmap", async (c) => {
3305
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "roadmap.html"));
3349
+ const html = await serveHtmlTemplate(join(UI_DIR, "roadmap.html"));
3306
3350
  return c.html(html);
3307
3351
  });
3308
3352
  // Roadmap API — parse brain/operations/roadmap.yaml and return as JSON
@@ -3364,7 +3408,7 @@ app.get("/api/roadmap/recent", async (c) => {
3364
3408
  // Serve personal.html (placeholder — instances populate this)
3365
3409
  app.get("/personal", async (c) => {
3366
3410
  try {
3367
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "personal.html"));
3411
+ const html = await serveHtmlTemplate(join(UI_DIR, "personal.html"));
3368
3412
  return c.html(html);
3369
3413
  }
3370
3414
  catch {
@@ -3374,7 +3418,7 @@ app.get("/personal", async (c) => {
3374
3418
  // Serve life.html (placeholder — instances populate this)
3375
3419
  app.get("/life", async (c) => {
3376
3420
  try {
3377
- const html = await serveHtmlTemplate(join(PKG_ROOT, "public", "life.html"));
3421
+ const html = await serveHtmlTemplate(join(UI_DIR, "life.html"));
3378
3422
  return c.html(html);
3379
3423
  }
3380
3424
  catch {
@@ -4545,7 +4589,7 @@ app.post("/api/chat", async (c) => {
4545
4589
  const reqSignal = c.req.raw.signal;
4546
4590
  return streamSSE(c, async (stream) => {
4547
4591
  // Send metadata first so UI can show which model is responding
4548
- await stream.writeSSE({ data: JSON.stringify({ meta: { provider: activeProvider, model: activeChatModel ?? (activeProvider === "ollama" ? "llama3.1:8b" : "claude-sonnet-4") } }) });
4592
+ await stream.writeSSE({ data: JSON.stringify({ meta: { provider: activeProvider, model: activeChatModel ?? (activeProvider === "ollama" ? "llama3.2:3b" : "claude-sonnet-4") } }) });
4549
4593
  let fullResponse = "";
4550
4594
  const savePartial = () => {
4551
4595
  if (fullResponse) {
@@ -5294,9 +5338,21 @@ async function start(opts) {
5294
5338
  const taskCount = await queueProvider.getStore().count();
5295
5339
  log.info(`Board: ${board.name} (local, ${taskCount} tasks)`);
5296
5340
  }
5341
+ // Sync UI from CDN (non-blocking — falls back to bundled if offline)
5342
+ syncUi().then(({ source, revision }) => {
5343
+ if (source === "cdn") {
5344
+ UI_DIR = getUiPublicDir(PKG_ROOT);
5345
+ log.info(`UI synced from CDN: revision ${revision}`);
5346
+ }
5347
+ else {
5348
+ log.info(`UI source: ${source}${revision ? ` (revision ${revision})` : ""}`);
5349
+ }
5350
+ }).catch(() => { });
5297
5351
  // Generate startup token for zero-friction local auth
5298
5352
  const { randomBytes: rng } = await import("node:crypto");
5299
5353
  startupToken = rng(32).toString("hex");
5354
+ // Warm up local model in background (non-blocking)
5355
+ import("./llm/ollama.js").then(({ warmupOllama }) => warmupOllama()).catch(() => { });
5300
5356
  if (code) {
5301
5357
  log.info(`First launch detected. Pairing code: ${code}`);
5302
5358
  }