clay-server 2.27.0-beta.9 → 2.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/lib/daemon-projects.js +164 -0
- package/lib/daemon.js +13 -126
- package/lib/mates-identity.js +132 -0
- package/lib/mates-knowledge.js +113 -0
- package/lib/mates-prompts.js +398 -0
- package/lib/mates.js +40 -599
- package/lib/project-connection.js +2 -0
- package/lib/project-http.js +4 -2
- package/lib/project-loop.js +110 -48
- package/lib/project-mate-interaction.js +4 -0
- package/lib/project-notifications.js +210 -0
- package/lib/project-sessions.js +5 -2
- package/lib/project-user-message.js +2 -1
- package/lib/project.js +26 -2
- package/lib/public/app.js +1193 -8517
- package/lib/public/css/command-palette.css +14 -0
- package/lib/public/css/loop.css +301 -0
- package/lib/public/css/notifications-center.css +190 -0
- package/lib/public/css/rewind.css +6 -0
- package/lib/public/index.html +89 -35
- package/lib/public/modules/app-connection.js +160 -0
- package/lib/public/modules/app-cursors.js +473 -0
- package/lib/public/modules/app-debate-ui.js +389 -0
- package/lib/public/modules/app-dm.js +627 -0
- package/lib/public/modules/app-favicon.js +212 -0
- package/lib/public/modules/app-header.js +229 -0
- package/lib/public/modules/app-home-hub.js +600 -0
- package/lib/public/modules/app-loop-ui.js +589 -0
- package/lib/public/modules/app-loop-wizard.js +439 -0
- package/lib/public/modules/app-messages.js +1560 -0
- package/lib/public/modules/app-misc.js +299 -0
- package/lib/public/modules/app-notifications.js +372 -0
- package/lib/public/modules/app-panels.js +888 -0
- package/lib/public/modules/app-projects.js +798 -0
- package/lib/public/modules/app-rate-limit.js +451 -0
- package/lib/public/modules/app-rendering.js +597 -0
- package/lib/public/modules/app-skills-install.js +234 -0
- package/lib/public/modules/command-palette.js +27 -4
- package/lib/public/modules/input.js +31 -20
- package/lib/public/modules/scheduler-config.js +1532 -0
- package/lib/public/modules/scheduler-history.js +79 -0
- package/lib/public/modules/scheduler.js +33 -1554
- package/lib/public/modules/session-search.js +13 -1
- package/lib/public/modules/sidebar-mates.js +812 -0
- package/lib/public/modules/sidebar-mobile.js +1269 -0
- package/lib/public/modules/sidebar-projects.js +1449 -0
- package/lib/public/modules/sidebar-sessions.js +986 -0
- package/lib/public/modules/sidebar.js +232 -4591
- package/lib/public/modules/store.js +27 -0
- package/lib/public/modules/ws-ref.js +7 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +96 -717
- package/lib/sdk-message-processor.js +587 -0
- package/lib/sdk-message-queue.js +42 -0
- package/lib/sdk-skill-discovery.js +131 -0
- package/lib/server-admin.js +712 -0
- package/lib/server-auth.js +737 -0
- package/lib/server-dm.js +221 -0
- package/lib/server-mates.js +281 -0
- package/lib/server-palette.js +110 -0
- package/lib/server-settings.js +479 -0
- package/lib/server-skills.js +280 -0
- package/lib/server.js +246 -2755
- package/lib/sessions.js +11 -4
- package/lib/users-auth.js +146 -0
- package/lib/users-permissions.js +118 -0
- package/lib/users-preferences.js +210 -0
- package/lib/users.js +48 -398
- package/lib/ws-schema.js +498 -0
- package/package.json +1 -1
package/lib/mates.js
CHANGED
|
@@ -4,6 +4,9 @@ var crypto = require("crypto");
|
|
|
4
4
|
var config = require("./config");
|
|
5
5
|
|
|
6
6
|
var crisisSafety = require("./crisis-safety");
|
|
7
|
+
var matesPrompts = require("./mates-prompts");
|
|
8
|
+
var matesIdentity = require("./mates-identity");
|
|
9
|
+
var { attachKnowledge } = require("./mates-knowledge");
|
|
7
10
|
|
|
8
11
|
// --- Path resolution ---
|
|
9
12
|
|
|
@@ -20,6 +23,42 @@ function resolveMatesRoot(ctx) {
|
|
|
20
23
|
return path.join(config.CONFIG_DIR, "mates");
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
// --- Wiring: knowledge module needs resolveMatesRoot ---
|
|
27
|
+
var knowledge = attachKnowledge(resolveMatesRoot);
|
|
28
|
+
var loadCommonKnowledge = knowledge.loadCommonKnowledge;
|
|
29
|
+
var saveCommonKnowledge = knowledge.saveCommonKnowledge;
|
|
30
|
+
var promoteKnowledge = knowledge.promoteKnowledge;
|
|
31
|
+
var depromoteKnowledge = knowledge.depromoteKnowledge;
|
|
32
|
+
var getCommonKnowledgeForMate = knowledge.getCommonKnowledgeForMate;
|
|
33
|
+
var readCommonKnowledgeFile = knowledge.readCommonKnowledgeFile;
|
|
34
|
+
var isPromoted = knowledge.isPromoted;
|
|
35
|
+
|
|
36
|
+
// --- Aliases from extracted modules ---
|
|
37
|
+
var TEAM_MARKER = matesPrompts.TEAM_MARKER;
|
|
38
|
+
var TEAM_SECTION = matesPrompts.TEAM_SECTION;
|
|
39
|
+
var SESSION_MEMORY_MARKER = matesPrompts.SESSION_MEMORY_MARKER;
|
|
40
|
+
var SESSION_MEMORY_SECTION = matesPrompts.SESSION_MEMORY_SECTION;
|
|
41
|
+
var STICKY_NOTES_MARKER = matesPrompts.STICKY_NOTES_MARKER;
|
|
42
|
+
var STICKY_NOTES_SECTION = matesPrompts.STICKY_NOTES_SECTION;
|
|
43
|
+
var PROJECT_REGISTRY_MARKER = matesPrompts.PROJECT_REGISTRY_MARKER;
|
|
44
|
+
var DEBATE_AWARENESS_MARKER = matesPrompts.DEBATE_AWARENESS_MARKER;
|
|
45
|
+
var DEBATE_AWARENESS_SECTION = matesPrompts.DEBATE_AWARENESS_SECTION;
|
|
46
|
+
var ALL_SYSTEM_MARKERS = matesPrompts.ALL_SYSTEM_MARKERS;
|
|
47
|
+
var buildTeamSection = matesPrompts.buildTeamSection;
|
|
48
|
+
var enforceTeamAwareness = matesPrompts.enforceTeamAwareness;
|
|
49
|
+
var enforceProjectRegistry = matesPrompts.enforceProjectRegistry;
|
|
50
|
+
var buildProjectRegistrySection = matesPrompts.buildProjectRegistrySection;
|
|
51
|
+
var enforceSessionMemory = matesPrompts.enforceSessionMemory;
|
|
52
|
+
var enforceStickyNotes = matesPrompts.enforceStickyNotes;
|
|
53
|
+
var enforceDebateAwareness = matesPrompts.enforceDebateAwareness;
|
|
54
|
+
var PRIMARY_CAPABILITIES_MARKER = matesIdentity.PRIMARY_CAPABILITIES_MARKER;
|
|
55
|
+
var IDENTITY_MIN_LENGTH = matesIdentity.IDENTITY_MIN_LENGTH;
|
|
56
|
+
var buildPrimaryCapabilitiesSection = matesIdentity.buildPrimaryCapabilitiesSection;
|
|
57
|
+
var backupIdentity = matesIdentity.backupIdentity;
|
|
58
|
+
var loadIdentityBackup = matesIdentity.loadIdentityBackup;
|
|
59
|
+
var logIdentityChange = matesIdentity.logIdentityChange;
|
|
60
|
+
var extractIdentity = function (content) { return matesIdentity.extractIdentity(content, ALL_SYSTEM_MARKERS); };
|
|
61
|
+
|
|
23
62
|
function buildMateCtx(userId) {
|
|
24
63
|
if (!userId) return { userId: null, multiUser: false, linuxUser: null };
|
|
25
64
|
// Lazy require to avoid circular dependency
|
|
@@ -288,469 +327,6 @@ function migrateLegacyMates() {
|
|
|
288
327
|
}
|
|
289
328
|
}
|
|
290
329
|
|
|
291
|
-
// --- Team awareness ---
|
|
292
|
-
|
|
293
|
-
var TEAM_MARKER = "<!-- TEAM_AWARENESS_MANAGED_BY_SYSTEM -->";
|
|
294
|
-
|
|
295
|
-
// Static fallback when ctx is unavailable
|
|
296
|
-
var TEAM_SECTION =
|
|
297
|
-
"\n\n" + TEAM_MARKER + "\n" +
|
|
298
|
-
"## Your Team\n\n" +
|
|
299
|
-
"**This section is managed by the system and cannot be removed.**\n\n" +
|
|
300
|
-
"You are one of several AI Mates in this workspace. Your teammates and their profiles are listed in `../mates.json`. " +
|
|
301
|
-
"Each teammate's identity and working style is described in their own directory:\n\n" +
|
|
302
|
-
"- `../{mate_id}/CLAUDE.md` — their identity, personality, and working style\n" +
|
|
303
|
-
"- `../{mate_id}/mate.yaml` — their metadata (name, role, status, activities)\n" +
|
|
304
|
-
"- `../common-knowledge.json` — shared knowledge registry; files listed here are readable by all mates\n\n" +
|
|
305
|
-
"Check the team registry when it would be relevant to know who else is available or what they do. " +
|
|
306
|
-
"You cannot message other Mates directly yet, but knowing your team helps you work with the user more effectively.\n";
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Build a dynamic team section with current mate roster.
|
|
310
|
-
* Lists each teammate by stable ID with their current display name, role, and bio.
|
|
311
|
-
* @param {object} ctx - user context for loading mates
|
|
312
|
-
* @param {string} currentMateId - this mate's ID (excluded from the roster)
|
|
313
|
-
* @returns {string} Team section string, or static TEAM_SECTION as fallback
|
|
314
|
-
*/
|
|
315
|
-
function buildTeamSection(ctx, currentMateId) {
|
|
316
|
-
var data;
|
|
317
|
-
try { data = loadMates(ctx); } catch (e) { return TEAM_SECTION; }
|
|
318
|
-
if (!data || !data.mates || data.mates.length < 2) return TEAM_SECTION;
|
|
319
|
-
|
|
320
|
-
var mates = data.mates.filter(function (m) {
|
|
321
|
-
return m.id !== currentMateId && m.status === "ready";
|
|
322
|
-
});
|
|
323
|
-
if (mates.length === 0) return TEAM_SECTION;
|
|
324
|
-
|
|
325
|
-
var section = "\n\n" + TEAM_MARKER + "\n" +
|
|
326
|
-
"## Your Team\n\n" +
|
|
327
|
-
"**This section is managed by the system and updated automatically.**\n\n" +
|
|
328
|
-
"You are one of " + (mates.length + 1) + " AI Mates in this workspace. " +
|
|
329
|
-
"Here is your current team roster:\n\n" +
|
|
330
|
-
"| Name | ID | Bio |\n" +
|
|
331
|
-
"|------|-----|-----|\n";
|
|
332
|
-
|
|
333
|
-
for (var i = 0; i < mates.length; i++) {
|
|
334
|
-
var m = mates[i];
|
|
335
|
-
var name = (m.profile && m.profile.displayName) || m.name || "Unnamed";
|
|
336
|
-
var bio = (m.bio || "").replace(/\|/g, "/").replace(/\n/g, " ");
|
|
337
|
-
if (bio.length > 120) bio = bio.substring(0, 117) + "...";
|
|
338
|
-
section += "| " + name + " | `" + m.id + "` | " + bio + " |\n";
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
section += "\n" +
|
|
342
|
-
"Each teammate's full identity is in their own directory:\n\n" +
|
|
343
|
-
"- `../{mate_id}/CLAUDE.md` -- identity, personality, working style\n" +
|
|
344
|
-
"- `../{mate_id}/mate.yaml` -- metadata (name, role, status, activities)\n" +
|
|
345
|
-
"- `../common-knowledge.json` -- shared knowledge readable by all mates\n\n" +
|
|
346
|
-
"Use the **ID** (not the name) when referencing teammates in structured data. " +
|
|
347
|
-
"Names can change, IDs are permanent.\n";
|
|
348
|
-
|
|
349
|
-
return section;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// --- Project registry ---
|
|
353
|
-
|
|
354
|
-
var PROJECT_REGISTRY_MARKER = "<!-- PROJECT_REGISTRY_MANAGED_BY_SYSTEM -->";
|
|
355
|
-
|
|
356
|
-
function buildProjectRegistrySection(projects) {
|
|
357
|
-
if (!projects || projects.length === 0) return "";
|
|
358
|
-
var section = "\n\n" + PROJECT_REGISTRY_MARKER + "\n" +
|
|
359
|
-
"## Available Projects\n\n" +
|
|
360
|
-
"**This section is managed by the system and cannot be removed.**\n\n" +
|
|
361
|
-
"The following projects are registered in this workspace. " +
|
|
362
|
-
"Use this information when the user references a project by name, " +
|
|
363
|
-
"so you do not need to ask for the path.\n\n" +
|
|
364
|
-
"| Project | Path |\n" +
|
|
365
|
-
"|---------|------|\n";
|
|
366
|
-
for (var i = 0; i < projects.length; i++) {
|
|
367
|
-
var p = projects[i];
|
|
368
|
-
var name = (p.icon ? p.icon + " " : "") + (p.title || p.slug || path.basename(p.path));
|
|
369
|
-
section += "| " + name + " | `" + p.path + "` |\n";
|
|
370
|
-
}
|
|
371
|
-
return section;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Enforce the project registry section on a mate's CLAUDE.md.
|
|
376
|
-
* Inserts after team awareness section and before session memory section.
|
|
377
|
-
* Pass the current project list; if empty, removes the section.
|
|
378
|
-
* Returns true if the file was modified.
|
|
379
|
-
*/
|
|
380
|
-
function enforceProjectRegistry(filePath, projects) {
|
|
381
|
-
if (!fs.existsSync(filePath)) return false;
|
|
382
|
-
|
|
383
|
-
var content = fs.readFileSync(filePath, "utf8");
|
|
384
|
-
var newSection = buildProjectRegistrySection(projects);
|
|
385
|
-
|
|
386
|
-
// Strip existing section if present
|
|
387
|
-
var markerIdx = content.indexOf(PROJECT_REGISTRY_MARKER);
|
|
388
|
-
if (markerIdx !== -1) {
|
|
389
|
-
var afterMarker = content.substring(markerIdx);
|
|
390
|
-
// Find next system marker
|
|
391
|
-
var nextIdx = -1;
|
|
392
|
-
var candidates = [SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
|
|
393
|
-
for (var c = 0; c < candidates.length; c++) {
|
|
394
|
-
var ci = afterMarker.indexOf(candidates[c]);
|
|
395
|
-
if (ci !== -1 && (nextIdx === -1 || ci < nextIdx)) nextIdx = ci;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (nextIdx !== -1) {
|
|
399
|
-
var existing = afterMarker.substring(0, nextIdx).trimEnd();
|
|
400
|
-
if (existing === newSection.trimStart().trimEnd()) return false; // already correct
|
|
401
|
-
content = content.substring(0, markerIdx).trimEnd() + content.substring(markerIdx + nextIdx);
|
|
402
|
-
} else {
|
|
403
|
-
var existing = afterMarker.trimEnd();
|
|
404
|
-
if (existing === newSection.trimStart().trimEnd()) return false;
|
|
405
|
-
content = content.substring(0, markerIdx).trimEnd();
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// If no projects, just remove the section (already done above)
|
|
410
|
-
if (!newSection) {
|
|
411
|
-
if (markerIdx !== -1) {
|
|
412
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
413
|
-
return true;
|
|
414
|
-
}
|
|
415
|
-
return false;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Insert before session memory, sticky notes, debate, or crisis safety
|
|
419
|
-
var insertBefore = -1;
|
|
420
|
-
var insertCandidates = [SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
|
|
421
|
-
for (var ic = 0; ic < insertCandidates.length; ic++) {
|
|
422
|
-
var pos = content.indexOf(insertCandidates[ic]);
|
|
423
|
-
if (pos !== -1) { insertBefore = pos; break; }
|
|
424
|
-
}
|
|
425
|
-
if (insertBefore !== -1) {
|
|
426
|
-
content = content.substring(0, insertBefore).trimEnd() + newSection + "\n\n" + content.substring(insertBefore);
|
|
427
|
-
} else {
|
|
428
|
-
content = content.trimEnd() + newSection;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
432
|
-
return true;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function hasTeamSection(content) {
|
|
436
|
-
return content.indexOf(TEAM_MARKER) !== -1;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Enforce the team awareness section on a mate's CLAUDE.md.
|
|
441
|
-
* Inserts before the crisis safety section (if present), or appends at the end.
|
|
442
|
-
* Returns true if the file was modified.
|
|
443
|
-
*/
|
|
444
|
-
function enforceTeamAwareness(filePath) {
|
|
445
|
-
if (!fs.existsSync(filePath)) return false;
|
|
446
|
-
|
|
447
|
-
var content = fs.readFileSync(filePath, "utf8");
|
|
448
|
-
|
|
449
|
-
// Check if already present and correct
|
|
450
|
-
var teamIdx = content.indexOf(TEAM_MARKER);
|
|
451
|
-
if (teamIdx !== -1) {
|
|
452
|
-
// Extract existing team section (up to next system marker or end)
|
|
453
|
-
var afterTeam = content.substring(teamIdx);
|
|
454
|
-
// Find the nearest following system marker (project registry, session memory, or crisis safety)
|
|
455
|
-
var nextMarkerIdx = -1;
|
|
456
|
-
var projIdx = afterTeam.indexOf(PROJECT_REGISTRY_MARKER);
|
|
457
|
-
var memIdx = afterTeam.indexOf(SESSION_MEMORY_MARKER);
|
|
458
|
-
var crisisIdx = afterTeam.indexOf(crisisSafety.MARKER);
|
|
459
|
-
var teamNextCandidates = [projIdx, memIdx, crisisIdx];
|
|
460
|
-
for (var tn = 0; tn < teamNextCandidates.length; tn++) {
|
|
461
|
-
if (teamNextCandidates[tn] !== -1 && (nextMarkerIdx === -1 || teamNextCandidates[tn] < nextMarkerIdx)) {
|
|
462
|
-
nextMarkerIdx = teamNextCandidates[tn];
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
var existing;
|
|
466
|
-
if (nextMarkerIdx !== -1) {
|
|
467
|
-
existing = afterTeam.substring(0, nextMarkerIdx).trimEnd();
|
|
468
|
-
} else {
|
|
469
|
-
existing = afterTeam.trimEnd();
|
|
470
|
-
}
|
|
471
|
-
if (existing === TEAM_SECTION.trimStart().trimEnd()) return false; // already correct
|
|
472
|
-
|
|
473
|
-
// Strip the existing team section
|
|
474
|
-
var endOfTeam = nextMarkerIdx !== -1 ? teamIdx + nextMarkerIdx : content.length;
|
|
475
|
-
content = content.substring(0, teamIdx).trimEnd() + content.substring(endOfTeam);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Insert before the first subsequent system section (in order)
|
|
479
|
-
var insertBefore = -1;
|
|
480
|
-
var teamInsertCandidates = [PROJECT_REGISTRY_MARKER, SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
|
|
481
|
-
for (var ti = 0; ti < teamInsertCandidates.length; ti++) {
|
|
482
|
-
var tip = content.indexOf(teamInsertCandidates[ti]);
|
|
483
|
-
if (tip !== -1) { insertBefore = tip; break; }
|
|
484
|
-
}
|
|
485
|
-
if (insertBefore !== -1) {
|
|
486
|
-
content = content.substring(0, insertBefore).trimEnd() + TEAM_SECTION + "\n\n" + content.substring(insertBefore);
|
|
487
|
-
} else {
|
|
488
|
-
content = content.trimEnd() + TEAM_SECTION;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
492
|
-
return true;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// --- Session memory ---
|
|
496
|
-
|
|
497
|
-
var SESSION_MEMORY_MARKER = "<!-- SESSION_MEMORY_MANAGED_BY_SYSTEM -->";
|
|
498
|
-
|
|
499
|
-
var SESSION_MEMORY_SECTION =
|
|
500
|
-
"\n\n" + SESSION_MEMORY_MARKER + "\n" +
|
|
501
|
-
"## Session Memory\n\n" +
|
|
502
|
-
"**This section is managed by the system and cannot be removed.**\n\n" +
|
|
503
|
-
"Your `knowledge/memory-summary.md` file contains your compressed long-term memory, " +
|
|
504
|
-
"automatically maintained across sessions. Refer to it for context about past " +
|
|
505
|
-
"interactions, decisions, and patterns.\n\n" +
|
|
506
|
-
"Your `knowledge/session-digests.jsonl` file contains raw session logs as an archive. " +
|
|
507
|
-
"You do not need to read it routinely. Only access it when you need to look up " +
|
|
508
|
-
"specific details from a past session that are not in the summary.\n";
|
|
509
|
-
|
|
510
|
-
function hasSessionMemory(content) {
|
|
511
|
-
return content.indexOf(SESSION_MEMORY_MARKER) !== -1;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Enforce the session memory section on a mate's CLAUDE.md.
|
|
516
|
-
* Inserts after team awareness section and before crisis safety section.
|
|
517
|
-
* Returns true if the file was modified.
|
|
518
|
-
*/
|
|
519
|
-
function enforceSessionMemory(filePath) {
|
|
520
|
-
if (!fs.existsSync(filePath)) return false;
|
|
521
|
-
|
|
522
|
-
var content = fs.readFileSync(filePath, "utf8");
|
|
523
|
-
|
|
524
|
-
// Check if already present and correct
|
|
525
|
-
var memIdx = content.indexOf(SESSION_MEMORY_MARKER);
|
|
526
|
-
if (memIdx !== -1) {
|
|
527
|
-
// Extract existing section (up to next system marker or end)
|
|
528
|
-
var afterMem = content.substring(memIdx);
|
|
529
|
-
var nextMemIdx = -1;
|
|
530
|
-
var memNextCandidates = [STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
|
|
531
|
-
for (var mn = 0; mn < memNextCandidates.length; mn++) {
|
|
532
|
-
var mni = afterMem.indexOf(memNextCandidates[mn]);
|
|
533
|
-
if (mni !== -1 && (nextMemIdx === -1 || mni < nextMemIdx)) nextMemIdx = mni;
|
|
534
|
-
}
|
|
535
|
-
var existing;
|
|
536
|
-
if (nextMemIdx !== -1) {
|
|
537
|
-
existing = afterMem.substring(0, nextMemIdx).trimEnd();
|
|
538
|
-
} else {
|
|
539
|
-
existing = afterMem.trimEnd();
|
|
540
|
-
}
|
|
541
|
-
if (existing === SESSION_MEMORY_SECTION.trimStart().trimEnd()) return false; // already correct
|
|
542
|
-
|
|
543
|
-
// Strip the existing session memory section
|
|
544
|
-
var endOfMem = nextMemIdx !== -1 ? memIdx + nextMemIdx : content.length;
|
|
545
|
-
content = content.substring(0, memIdx).trimEnd() + content.substring(endOfMem);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Insert before sticky notes, debate, or crisis safety section if present, otherwise append
|
|
549
|
-
var memInsertBefore = -1;
|
|
550
|
-
var memInsertCandidates = [STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
|
|
551
|
-
for (var mi = 0; mi < memInsertCandidates.length; mi++) {
|
|
552
|
-
var mip = content.indexOf(memInsertCandidates[mi]);
|
|
553
|
-
if (mip !== -1) { memInsertBefore = mip; break; }
|
|
554
|
-
}
|
|
555
|
-
if (memInsertBefore !== -1) {
|
|
556
|
-
content = content.substring(0, memInsertBefore).trimEnd() + SESSION_MEMORY_SECTION + "\n\n" + content.substring(memInsertBefore);
|
|
557
|
-
} else {
|
|
558
|
-
content = content.trimEnd() + SESSION_MEMORY_SECTION;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
562
|
-
return true;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// --- Sticky notes pointer in CLAUDE.md ---
|
|
566
|
-
|
|
567
|
-
var STICKY_NOTES_MARKER = "<!-- STICKY_NOTES_MANAGED_BY_SYSTEM -->";
|
|
568
|
-
|
|
569
|
-
var STICKY_NOTES_SECTION =
|
|
570
|
-
"\n\n" + STICKY_NOTES_MARKER + "\n" +
|
|
571
|
-
"## Sticky Notes\n\n" +
|
|
572
|
-
"**This section is managed by the system and cannot be removed.**\n\n" +
|
|
573
|
-
"Your `knowledge/sticky-notes.md` file contains sticky notes left by the user. " +
|
|
574
|
-
"Read this file when starting a conversation for important context. " +
|
|
575
|
-
"These notes are read-only. You cannot create, update, or delete them.\n";
|
|
576
|
-
|
|
577
|
-
function hasStickyNotesSection(content) {
|
|
578
|
-
return content.indexOf(STICKY_NOTES_MARKER) !== -1;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Enforce the sticky notes pointer section in a mate's CLAUDE.md.
|
|
583
|
-
* Inserts after session memory and before crisis safety.
|
|
584
|
-
* Returns true if the file was modified.
|
|
585
|
-
*/
|
|
586
|
-
function enforceStickyNotes(filePath) {
|
|
587
|
-
if (!fs.existsSync(filePath)) return false;
|
|
588
|
-
|
|
589
|
-
var content = fs.readFileSync(filePath, "utf8");
|
|
590
|
-
|
|
591
|
-
var markerIdx = content.indexOf(STICKY_NOTES_MARKER);
|
|
592
|
-
if (markerIdx !== -1) {
|
|
593
|
-
var afterMarker = content.substring(markerIdx);
|
|
594
|
-
var stickyNextIdx = -1;
|
|
595
|
-
var stickyNextCandidates = [DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
|
|
596
|
-
for (var sn = 0; sn < stickyNextCandidates.length; sn++) {
|
|
597
|
-
var sni = afterMarker.indexOf(stickyNextCandidates[sn]);
|
|
598
|
-
if (sni !== -1 && (stickyNextIdx === -1 || sni < stickyNextIdx)) stickyNextIdx = sni;
|
|
599
|
-
}
|
|
600
|
-
var existing;
|
|
601
|
-
if (stickyNextIdx !== -1) {
|
|
602
|
-
existing = afterMarker.substring(0, stickyNextIdx).trimEnd();
|
|
603
|
-
} else {
|
|
604
|
-
existing = afterMarker.trimEnd();
|
|
605
|
-
}
|
|
606
|
-
if (existing === STICKY_NOTES_SECTION.trimStart().trimEnd()) return false;
|
|
607
|
-
|
|
608
|
-
var endOfSection = stickyNextIdx !== -1 ? markerIdx + stickyNextIdx : content.length;
|
|
609
|
-
content = content.substring(0, markerIdx).trimEnd() + content.substring(endOfSection);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
var stickyInsertBefore = -1;
|
|
613
|
-
var stickyInsertCandidates = [DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
|
|
614
|
-
for (var si = 0; si < stickyInsertCandidates.length; si++) {
|
|
615
|
-
var sip = content.indexOf(stickyInsertCandidates[si]);
|
|
616
|
-
if (sip !== -1) { stickyInsertBefore = sip; break; }
|
|
617
|
-
}
|
|
618
|
-
if (stickyInsertBefore !== -1) {
|
|
619
|
-
content = content.substring(0, stickyInsertBefore).trimEnd() + STICKY_NOTES_SECTION + "\n\n" + content.substring(stickyInsertBefore);
|
|
620
|
-
} else {
|
|
621
|
-
content = content.trimEnd() + STICKY_NOTES_SECTION;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
625
|
-
return true;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// --- Debate awareness ---
|
|
629
|
-
|
|
630
|
-
var DEBATE_AWARENESS_MARKER = "<!-- DEBATE_AWARENESS_MANAGED_BY_SYSTEM -->";
|
|
631
|
-
|
|
632
|
-
var DEBATE_AWARENESS_SECTION =
|
|
633
|
-
"\n\n" + DEBATE_AWARENESS_MARKER + "\n" +
|
|
634
|
-
"## Proposing Debates\n\n" +
|
|
635
|
-
"**This section is managed by the system and cannot be removed.**\n\n" +
|
|
636
|
-
"When the user suggests a debate, you MUST use the `propose_debate` tool. " +
|
|
637
|
-
"NEVER write debate files to disk. NEVER mkdir for debates. NEVER use Write/Bash for debate setup. " +
|
|
638
|
-
"The ONLY way to propose a debate is the `propose_debate` tool.\n\n" +
|
|
639
|
-
"**How to propose a debate:**\n" +
|
|
640
|
-
"Call the `propose_debate` tool with these parameters:\n" +
|
|
641
|
-
"- `topic` (required): The refined debate topic\n" +
|
|
642
|
-
"- `format`: Debate format, default \"free_discussion\"\n" +
|
|
643
|
-
"- `context`: Key context from the conversation that panelists should know\n" +
|
|
644
|
-
"- `specialRequests`: Any special instructions\n" +
|
|
645
|
-
"- `panelists` (required): A JSON string array of panelist objects:\n" +
|
|
646
|
-
" `[{\"mateId\": \"<mate UUID from team roster>\", \"role\": \"perspective\", \"brief\": \"guidance\"}]`\n\n" +
|
|
647
|
-
"The user will see an inline approval card. The tool blocks until they approve or cancel.\n\n" +
|
|
648
|
-
"**Rules:**\n" +
|
|
649
|
-
"- Choose 2-4 panelists from the team roster. Pick mates whose expertise fits the topic.\n" +
|
|
650
|
-
"- Do NOT include yourself as a panelist. You will moderate the debate.\n" +
|
|
651
|
-
"- Only propose a debate when the user explicitly asks for one.\n" +
|
|
652
|
-
"- Do NOT write files to disk for debate proposals. Always use the propose_debate tool.\n";
|
|
653
|
-
|
|
654
|
-
function enforceDebateAwareness(filePath) {
|
|
655
|
-
if (!fs.existsSync(filePath)) return false;
|
|
656
|
-
|
|
657
|
-
var content = fs.readFileSync(filePath, "utf8");
|
|
658
|
-
|
|
659
|
-
var markerIdx = content.indexOf(DEBATE_AWARENESS_MARKER);
|
|
660
|
-
if (markerIdx !== -1) {
|
|
661
|
-
var afterMarker = content.substring(markerIdx);
|
|
662
|
-
var crisisIdx = afterMarker.indexOf(crisisSafety.MARKER);
|
|
663
|
-
var existing;
|
|
664
|
-
if (crisisIdx !== -1) {
|
|
665
|
-
existing = afterMarker.substring(0, crisisIdx).trimEnd();
|
|
666
|
-
} else {
|
|
667
|
-
existing = afterMarker.trimEnd();
|
|
668
|
-
}
|
|
669
|
-
if (existing === DEBATE_AWARENESS_SECTION.trimStart().trimEnd()) return false;
|
|
670
|
-
|
|
671
|
-
var endOfSection = crisisIdx !== -1 ? markerIdx + crisisIdx : content.length;
|
|
672
|
-
content = content.substring(0, markerIdx).trimEnd() + content.substring(endOfSection);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
var crisisPos = content.indexOf(crisisSafety.MARKER);
|
|
676
|
-
if (crisisPos !== -1) {
|
|
677
|
-
content = content.substring(0, crisisPos).trimEnd() + DEBATE_AWARENESS_SECTION + "\n\n" + content.substring(crisisPos);
|
|
678
|
-
} else {
|
|
679
|
-
content = content.trimEnd() + DEBATE_AWARENESS_SECTION;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
683
|
-
return true;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// --- Primary mate capabilities (dynamically injected, never stored in identity) ---
|
|
687
|
-
|
|
688
|
-
var PRIMARY_CAPABILITIES_MARKER = "<!-- PRIMARY_CAPABILITIES_MANAGED_BY_SYSTEM -->";
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Build the capabilities section for a primary mate.
|
|
692
|
-
* This is injected as a system section so it auto-updates with code changes
|
|
693
|
-
* without touching the mate's identity in CLAUDE.md.
|
|
694
|
-
*/
|
|
695
|
-
function buildPrimaryCapabilitiesSection(mate) {
|
|
696
|
-
if (!mate || !mate.primary) return "";
|
|
697
|
-
|
|
698
|
-
var parts = [
|
|
699
|
-
"\n\n" + PRIMARY_CAPABILITIES_MARKER,
|
|
700
|
-
"## System Capabilities",
|
|
701
|
-
"",
|
|
702
|
-
"**This section is managed by the system and updated automatically with each release.**",
|
|
703
|
-
""
|
|
704
|
-
];
|
|
705
|
-
|
|
706
|
-
if (mate.globalSearch) {
|
|
707
|
-
parts.push("### Cross-Mate Awareness");
|
|
708
|
-
parts.push("");
|
|
709
|
-
parts.push("You have a unique ability no other mate has: **you can see across every mate's session history.**");
|
|
710
|
-
parts.push("When the user asks you a question, the system automatically searches all teammates' past sessions");
|
|
711
|
-
parts.push("and surfaces relevant context to you. Results from other mates are tagged with their name (e.g. @Arch).");
|
|
712
|
-
parts.push("");
|
|
713
|
-
parts.push("Use this to:");
|
|
714
|
-
parts.push("- Answer questions like \"What did Arch decide about the API?\" or \"What was Buzz's take on the launch plan?\"");
|
|
715
|
-
parts.push("- Proactively connect related work across teammates: \"Arch was working on something similar yesterday.\"");
|
|
716
|
-
parts.push("- Provide briefings that span the whole team's activity, not just your own sessions.");
|
|
717
|
-
parts.push("");
|
|
718
|
-
parts.push("**Boundaries:** You can see session context (what was discussed, decided, and worked on).");
|
|
719
|
-
parts.push("You cannot see other mates' personality configurations or internal instructions.");
|
|
720
|
-
parts.push("");
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return parts.join("\n");
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// --- Atomic enforce: single read/write for all system sections ---
|
|
727
|
-
|
|
728
|
-
var ALL_SYSTEM_MARKERS = [TEAM_MARKER, PROJECT_REGISTRY_MARKER, PRIMARY_CAPABILITIES_MARKER, SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
|
|
729
|
-
|
|
730
|
-
// Minimum identity length (chars) to consider it "real" content
|
|
731
|
-
var IDENTITY_MIN_LENGTH = 50;
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Extract identity content from a CLAUDE.md string.
|
|
735
|
-
* Identity is everything before the first system marker.
|
|
736
|
-
*/
|
|
737
|
-
function extractIdentity(content) {
|
|
738
|
-
var earliest = -1;
|
|
739
|
-
for (var i = 0; i < ALL_SYSTEM_MARKERS.length; i++) {
|
|
740
|
-
var idx = content.indexOf(ALL_SYSTEM_MARKERS[i]);
|
|
741
|
-
if (idx !== -1 && (earliest === -1 || idx < earliest)) {
|
|
742
|
-
earliest = idx;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
// Also check for bare "## Crisis Safety" heading as fallback
|
|
746
|
-
var crisisHeading = content.indexOf("\n## Crisis Safety");
|
|
747
|
-
if (crisisHeading !== -1 && (earliest === -1 || crisisHeading < earliest)) {
|
|
748
|
-
earliest = crisisHeading;
|
|
749
|
-
}
|
|
750
|
-
if (earliest === -1) return content.trimEnd();
|
|
751
|
-
return content.substring(0, earliest).trimEnd();
|
|
752
|
-
}
|
|
753
|
-
|
|
754
330
|
/**
|
|
755
331
|
* Strip all system sections from CLAUDE.md content, returning only identity.
|
|
756
332
|
*/
|
|
@@ -758,53 +334,6 @@ function stripAllSystemSections(content) {
|
|
|
758
334
|
return extractIdentity(content);
|
|
759
335
|
}
|
|
760
336
|
|
|
761
|
-
/**
|
|
762
|
-
* Save an identity backup to knowledge/identity-backup.md.
|
|
763
|
-
* Only overwrites if the new identity is substantive.
|
|
764
|
-
*/
|
|
765
|
-
function backupIdentity(mateDir, identity) {
|
|
766
|
-
if (!identity || identity.length < IDENTITY_MIN_LENGTH) return false;
|
|
767
|
-
var knDir = path.join(mateDir, "knowledge");
|
|
768
|
-
try { fs.mkdirSync(knDir, { recursive: true }); } catch (e) {}
|
|
769
|
-
var backupPath = path.join(knDir, "identity-backup.md");
|
|
770
|
-
fs.writeFileSync(backupPath, identity, "utf8");
|
|
771
|
-
return true;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* Load identity backup from knowledge/identity-backup.md.
|
|
776
|
-
* Returns null if no backup exists or backup is empty.
|
|
777
|
-
*/
|
|
778
|
-
function loadIdentityBackup(mateDir) {
|
|
779
|
-
var backupPath = path.join(mateDir, "knowledge", "identity-backup.md");
|
|
780
|
-
try {
|
|
781
|
-
var content = fs.readFileSync(backupPath, "utf8");
|
|
782
|
-
if (content && content.length >= IDENTITY_MIN_LENGTH) return content;
|
|
783
|
-
} catch (e) {}
|
|
784
|
-
return null;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/**
|
|
788
|
-
* Log an identity change to knowledge/identity-history.jsonl.
|
|
789
|
-
*/
|
|
790
|
-
function logIdentityChange(mateDir, action, identity, prevIdentity) {
|
|
791
|
-
var knDir = path.join(mateDir, "knowledge");
|
|
792
|
-
try { fs.mkdirSync(knDir, { recursive: true }); } catch (e) {}
|
|
793
|
-
var historyPath = path.join(knDir, "identity-history.jsonl");
|
|
794
|
-
var entry = {
|
|
795
|
-
ts: Date.now(),
|
|
796
|
-
date: new Date().toISOString(),
|
|
797
|
-
action: action,
|
|
798
|
-
lengthChars: identity ? identity.length : 0,
|
|
799
|
-
prevLengthChars: prevIdentity ? prevIdentity.length : 0,
|
|
800
|
-
hash: crypto.createHash("sha256").update(identity || "").digest("hex").substring(0, 16),
|
|
801
|
-
preview: (identity || "").substring(0, 200)
|
|
802
|
-
};
|
|
803
|
-
try {
|
|
804
|
-
fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf8");
|
|
805
|
-
} catch (e) {}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
337
|
// Tracks paths we've already warned about missing identity (avoids log spam)
|
|
809
338
|
var _warnedPaths = {};
|
|
810
339
|
|
|
@@ -842,7 +371,7 @@ function enforceAllSections(filePath, opts) {
|
|
|
842
371
|
|
|
843
372
|
// 4. Rebuild the full file: identity + all system sections in order
|
|
844
373
|
// Use dynamic team section when ctx is available, static fallback otherwise
|
|
845
|
-
var teamSection = (opts.ctx && opts.mateId) ? buildTeamSection(opts.ctx, opts.mateId) : TEAM_SECTION;
|
|
374
|
+
var teamSection = (opts.ctx && opts.mateId) ? buildTeamSection(opts.ctx, opts.mateId, loadMates) : TEAM_SECTION;
|
|
846
375
|
|
|
847
376
|
// Primary mate capabilities (dynamically injected from code)
|
|
848
377
|
var capSection = "";
|
|
@@ -887,94 +416,6 @@ function enforceAllSections(filePath, opts) {
|
|
|
887
416
|
return true;
|
|
888
417
|
}
|
|
889
418
|
|
|
890
|
-
// --- Common knowledge registry ---
|
|
891
|
-
|
|
892
|
-
function commonKnowledgePath(ctx) {
|
|
893
|
-
return path.join(resolveMatesRoot(ctx), "common-knowledge.json");
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
function loadCommonKnowledge(ctx) {
|
|
897
|
-
try {
|
|
898
|
-
var raw = fs.readFileSync(commonKnowledgePath(ctx), "utf8");
|
|
899
|
-
return JSON.parse(raw);
|
|
900
|
-
} catch (e) {
|
|
901
|
-
return [];
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
function saveCommonKnowledge(ctx, entries) {
|
|
906
|
-
var filePath = commonKnowledgePath(ctx);
|
|
907
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
908
|
-
var tmpPath = filePath + ".tmp";
|
|
909
|
-
fs.writeFileSync(tmpPath, JSON.stringify(entries, null, 2));
|
|
910
|
-
fs.renameSync(tmpPath, filePath);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function promoteKnowledge(ctx, mateId, mateName, fileName) {
|
|
914
|
-
var entries = loadCommonKnowledge(ctx);
|
|
915
|
-
// Check if already promoted (same mateId + name)
|
|
916
|
-
for (var i = 0; i < entries.length; i++) {
|
|
917
|
-
if (entries[i].mateId === mateId && entries[i].name === fileName) {
|
|
918
|
-
return entries; // already promoted
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
entries.push({
|
|
922
|
-
name: fileName,
|
|
923
|
-
mateId: mateId,
|
|
924
|
-
mateName: mateName || null,
|
|
925
|
-
promotedAt: Date.now()
|
|
926
|
-
});
|
|
927
|
-
saveCommonKnowledge(ctx, entries);
|
|
928
|
-
return entries;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
function depromoteKnowledge(ctx, mateId, fileName) {
|
|
932
|
-
var entries = loadCommonKnowledge(ctx);
|
|
933
|
-
entries = entries.filter(function (e) {
|
|
934
|
-
return !(e.mateId === mateId && e.name === fileName);
|
|
935
|
-
});
|
|
936
|
-
saveCommonKnowledge(ctx, entries);
|
|
937
|
-
return entries;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
function getCommonKnowledgeForMate(ctx, mateId) {
|
|
941
|
-
var entries = loadCommonKnowledge(ctx);
|
|
942
|
-
var root = resolveMatesRoot(ctx);
|
|
943
|
-
var result = [];
|
|
944
|
-
for (var i = 0; i < entries.length; i++) {
|
|
945
|
-
var e = entries[i];
|
|
946
|
-
var filePath = path.join(root, e.mateId, "knowledge", e.name);
|
|
947
|
-
try {
|
|
948
|
-
var stat = fs.statSync(filePath);
|
|
949
|
-
result.push({
|
|
950
|
-
name: e.name,
|
|
951
|
-
size: stat.size,
|
|
952
|
-
mtime: stat.mtimeMs,
|
|
953
|
-
common: true,
|
|
954
|
-
ownMateId: e.mateId,
|
|
955
|
-
ownerName: e.mateName
|
|
956
|
-
});
|
|
957
|
-
} catch (err) {
|
|
958
|
-
// Source file deleted, skip (could clean up registry but not critical)
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
return result;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function readCommonKnowledgeFile(ctx, mateId, fileName) {
|
|
965
|
-
var root = resolveMatesRoot(ctx);
|
|
966
|
-
var filePath = path.join(root, mateId, "knowledge", path.basename(fileName));
|
|
967
|
-
return fs.readFileSync(filePath, "utf8");
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function isPromoted(ctx, mateId, fileName) {
|
|
971
|
-
var entries = loadCommonKnowledge(ctx);
|
|
972
|
-
for (var i = 0; i < entries.length; i++) {
|
|
973
|
-
if (entries[i].mateId === mateId && entries[i].name === fileName) return true;
|
|
974
|
-
}
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
419
|
// Format seed data as a human-readable context string
|
|
979
420
|
function formatSeedContext(seedData) {
|
|
980
421
|
if (!seedData) return "";
|
|
@@ -34,6 +34,7 @@ function attachConnection(ctx) {
|
|
|
34
34
|
var sendTo = ctx.sendTo;
|
|
35
35
|
var opts = ctx.opts;
|
|
36
36
|
var _loop = ctx._loop;
|
|
37
|
+
var _notifications = ctx._notifications;
|
|
37
38
|
var hydrateImageRefs = ctx.hydrateImageRefs;
|
|
38
39
|
var broadcastClientCount = ctx.broadcastClientCount;
|
|
39
40
|
var broadcastPresence = ctx.broadcastPresence;
|
|
@@ -97,6 +98,7 @@ function attachConnection(ctx) {
|
|
|
97
98
|
sendTo(ws, { type: "notes_list", notes: nm.list() });
|
|
98
99
|
sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
|
|
99
100
|
_loop.sendConnectionState(ws);
|
|
101
|
+
if (_notifications) _notifications.sendConnectionState(ws, sendTo);
|
|
100
102
|
|
|
101
103
|
// Session list (filtered for access control)
|
|
102
104
|
var allSessions = [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; });
|