clay-server 3.1.0-beta.1 → 3.1.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.
@@ -45,8 +45,9 @@ var STICKY_NOTES_SECTION =
45
45
  "\n\n" + STICKY_NOTES_MARKER + "\n" +
46
46
  "## Sticky Notes\n\n" +
47
47
  "**This section is managed by the system and cannot be removed.**\n\n" +
48
- "Your `knowledge/sticky-notes.md` file contains sticky notes left by the user. " +
49
- "Read this file when starting a conversation for important context. " +
48
+ "Your `knowledge/sticky-notes.md` file is shared project memory that persists across sessions. " +
49
+ "It may contain checklists, to-do items, work goals, handoffs, unfinished work, durable decisions and constraints, or project knowledge that future sessions should remember. " +
50
+ "Read this file when starting a conversation and use relevant notes to recover the current context and next actions. " +
50
51
  "These notes are read-only. You cannot create, update, or delete them.\n";
51
52
 
52
53
  var PROJECT_REGISTRY_MARKER = "<!-- PROJECT_REGISTRY_MANAGED_BY_SYSTEM -->";
@@ -1,12 +1,12 @@
1
1
  var sessionNotesMcp = require("./session-notes-mcp-server");
2
2
 
3
- var MAX_NOTE_TEXT_CHARS = 2000;
3
+ var MAX_NOTE_TEXT_CHARS = 20000;
4
4
  var MAX_ACTIVE_NOTES = 20;
5
- var MAX_PROMPT_CHARS = 2000;
6
- var MAX_NOTE_PROMPT_CHARS = 240;
7
- var NOTES_LABEL = "--- Project sticky notes (shared memory; manage via clay-notes tools) ---";
5
+ var MAX_PROMPT_CHARS = 4000;
6
+ var MAX_NOTE_PROMPT_CHARS = 800;
7
+ var NOTES_LABEL = "--- Project sticky notes (cross-session work memory; manage via clay-notes tools) ---";
8
8
  var NOTES_HINT = "- More notes are available; call list_notes for the rest.";
9
- var PROACTIVE_POLICY = "Use the board proactively: when the user states a decision, preference, correction, or durable project fact that future sessions will need, record it with write_note WITHOUT being asked, and say so in one short clause. The bar: a person skimming the board a week from now must still find the note useful, and it must stand on its own without this conversation. Never write announcement or narration notes ('leaving a note', 'did X just now'), routine progress, transient state, or anything the repo already records. Update or remove your stale notes instead of adding near-duplicates. A task should rarely add more than one or two notes.";
9
+ var PROACTIVE_POLICY = "The board persists across sessions and is shared with the user and every Clay agent working on this project. People may use it freely. As a Clay agent, use it specifically as cross-session work memory: checklists and to-do lists, work goals, handoffs, unfinished work, durable decisions and constraints, and knowledge that should remain available after this session ends. Create or update a note proactively when the user asks for a handoff, establishes something future sessions must remember, or when preserving the current goal and next actions would make continuation easier. Put a concise plain-text title on the first line. Add a detailed body with the relevant background, rationale, decisions, completed work, current state, validation, next steps, blockers, and references. If an injected preview is relevant or truncated, call list_notes and read the full note before acting. Do not use notes as a transcript, routine progress log, or self-announcement. Keep one coherent topic per note and consolidate or remove stale notes instead of creating duplicates.";
10
10
  var NOTE_COLORS = ["yellow", "blue", "green", "pink", "orange", "purple"];
11
11
 
12
12
  function toolResult(value) {
@@ -128,7 +128,7 @@ function attachSessionNotes(ctx) {
128
128
  function writeNote(args, caller) {
129
129
  if (!caller) return toolError("write_note requires a session-bound tool server");
130
130
  var rawText = typeof args.text === "string" ? args.text : "";
131
- if (rawText.length > MAX_NOTE_TEXT_CHARS) return toolError("text exceeds 2000 characters");
131
+ if (rawText.length > MAX_NOTE_TEXT_CHARS) return toolError("text exceeds " + MAX_NOTE_TEXT_CHARS + " characters");
132
132
  var text = rawText.trim();
133
133
  if (!text) return toolError("text is required");
134
134
  var notes = nm.list() || [];
@@ -335,15 +335,37 @@
335
335
  }
336
336
 
337
337
  .sticky-note-rendered .sn-check {
338
+ display: inline-flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ width: 14px;
342
+ height: 14px;
338
343
  font-size: 12px;
339
344
  margin-right: 4px;
340
345
  opacity: 0.6;
346
+ cursor: pointer;
347
+ user-select: none;
348
+ border-radius: 3px;
349
+ transition: opacity 0.15s, background 0.15s, transform 0.15s;
350
+ }
351
+
352
+ .sticky-note-rendered .sn-check:hover,
353
+ .sticky-note-rendered .sn-check:focus-visible {
354
+ opacity: 1;
355
+ background: rgba(0,0,0,0.07);
356
+ outline: none;
357
+ transform: scale(1.08);
341
358
  }
342
359
 
343
360
  .sticky-note-rendered .sn-check.checked {
344
361
  opacity: 0.9;
345
362
  }
346
363
 
364
+ .dark-theme .sticky-note-rendered .sn-check:hover,
365
+ .dark-theme .sticky-note-rendered .sn-check:focus-visible {
366
+ background: rgba(255,255,255,0.1);
367
+ }
368
+
347
369
  /* --- Resize handle (bottom-right corner) --- */
348
370
  .sticky-note-resize {
349
371
  position: absolute;
@@ -649,9 +671,12 @@
649
671
  font-weight: 400 !important;
650
672
  color: var(--text-dimmer) !important;
651
673
  margin-top: 6px !important;
674
+ max-width: 520px;
652
675
  display: flex;
653
676
  align-items: center;
677
+ justify-content: center;
654
678
  gap: 4px;
679
+ line-height: 1.55;
655
680
  }
656
681
 
657
682
  .notes-archive-empty-sub .lucide {
@@ -0,0 +1,89 @@
1
+ // Markdown helpers for sticky-note titles, handoff bodies, and checklists.
2
+
3
+ export function getTitle(text) {
4
+ if (!text) return "";
5
+ var idx = text.indexOf("\n");
6
+ return idx === -1 ? text : text.substring(0, idx);
7
+ }
8
+
9
+ function formatInline(text) {
10
+ var escaped = text
11
+ .replace(/&/g, "&amp;")
12
+ .replace(/</g, "&lt;")
13
+ .replace(/>/g, "&gt;");
14
+ return escaped
15
+ .replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
16
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
17
+ .replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<em>$1</em>")
18
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
19
+ .replace(/~~(.+?)~~/g, "<del>$1</del>")
20
+ .replace(/^(?:-\s*)?\[[xX]\]/gm, '<span class="sn-check checked" role="checkbox" aria-checked="true" tabindex="0" contenteditable="false">✓</span>')
21
+ .replace(/^(?:-\s*)?\[ \]/gm, '<span class="sn-check" role="checkbox" aria-checked="false" tabindex="0" contenteditable="false">☐</span>')
22
+ .replace(/\n/g, "<br>");
23
+ }
24
+
25
+ export function renderMiniMarkdown(text) {
26
+ if (!text) return "";
27
+ var lines = text.split("\n");
28
+ var title = lines[0];
29
+ var body = lines.slice(1).join("\n");
30
+ var html = '<div class="sn-title">' + formatInline(title) + "</div>";
31
+ if (body.trim()) html += formatInline(body);
32
+ return html;
33
+ }
34
+
35
+ function childrenToMarkdown(element) {
36
+ var result = "";
37
+ for (var i = 0; i < element.childNodes.length; i++) {
38
+ result += nodeToMarkdown(element.childNodes[i]);
39
+ }
40
+ return result;
41
+ }
42
+
43
+ function nodeToMarkdown(node) {
44
+ if (node.nodeType === 3) return node.textContent;
45
+ if (node.nodeType !== 1) return "";
46
+
47
+ var tag = node.tagName;
48
+ var inner = childrenToMarkdown(node);
49
+ switch (tag) {
50
+ case "STRONG": case "B": return "**" + inner + "**";
51
+ case "EM": case "I": return "*" + inner + "*";
52
+ case "DEL": case "S": case "STRIKE": return "~~" + inner + "~~";
53
+ case "CODE": return "`" + inner + "`";
54
+ case "BR": return "\n";
55
+ case "DIV":
56
+ if (node.classList.contains("sn-title")) return inner;
57
+ if (node.classList.contains("sn-placeholder")) return "";
58
+ return "\n" + inner;
59
+ case "P": return "\n" + inner;
60
+ case "A": return node.getAttribute("href") || inner;
61
+ case "SPAN":
62
+ if (node.classList.contains("sn-check")) {
63
+ return node.classList.contains("checked") ? "- [x]" : "- [ ]";
64
+ }
65
+ if (node.classList.contains("sn-placeholder")) return "";
66
+ return inner;
67
+ default: return inner;
68
+ }
69
+ }
70
+
71
+ export function extractMarkdown(rendered) {
72
+ var titleEl = rendered.querySelector(".sn-title");
73
+ if (titleEl) {
74
+ var titleMarkdown = childrenToMarkdown(titleEl);
75
+ var rest = "";
76
+ var afterTitle = false;
77
+ for (var i = 0; i < rendered.childNodes.length; i++) {
78
+ var child = rendered.childNodes[i];
79
+ if (child === titleEl) {
80
+ afterTitle = true;
81
+ continue;
82
+ }
83
+ if (afterTitle) rest += nodeToMarkdown(child);
84
+ }
85
+ if (rest && rest.charAt(0) === "\n") rest = rest.substring(1);
86
+ return titleMarkdown + (rest ? "\n" + rest : "");
87
+ }
88
+ return childrenToMarkdown(rendered).replace(/^\n+/, "");
89
+ }
@@ -1,5 +1,6 @@
1
1
  import { refreshIcons, iconHtml } from './icons.js';
2
2
  import { VENDOR_AVATARS } from './app-rendering.js';
3
+ import { extractMarkdown, getTitle, renderMiniMarkdown } from './sticky-note-markdown.js';
3
4
 
4
5
  var ctx;
5
6
  var notes = new Map(); // id -> { data, el }
@@ -136,106 +137,11 @@ function debouncedTextUpdate(id, text) {
136
137
  }, 500);
137
138
  }
138
139
 
139
- // --- Simple markdown ---
140
-
141
- function getTitle(text) {
142
- if (!text) return "";
143
- var idx = text.indexOf("\n");
144
- return idx === -1 ? text : text.substring(0, idx);
145
- }
146
-
147
- function renderMiniMarkdown(text) {
148
- if (!text) return "";
149
- var lines = text.split("\n");
150
- var title = lines[0];
151
- var body = lines.slice(1).join("\n");
152
-
153
- function fmt(s) {
154
- var escaped = s
155
- .replace(/&/g, "&amp;")
156
- .replace(/</g, "&lt;")
157
- .replace(/>/g, "&gt;");
158
- return escaped
159
- .replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
160
- .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
161
- .replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<em>$1</em>")
162
- .replace(/`([^`]+)`/g, "<code>$1</code>")
163
- .replace(/~~(.+?)~~/g, "<del>$1</del>")
164
- .replace(/^- \[x\]/gm, '<span class="sn-check checked">✓</span>')
165
- .replace(/^- \[ \]/gm, '<span class="sn-check">☐</span>')
166
- .replace(/\n/g, "<br>");
167
- }
168
-
169
- var html = '<div class="sn-title">' + fmt(title) + '</div>';
170
- if (body.trim()) {
171
- html += fmt(body);
172
- }
173
- return html;
174
- }
175
-
176
140
  function syncTitle(noteEl, text) {
177
141
  var spacer = noteEl.querySelector(".sticky-note-spacer");
178
142
  if (spacer) spacer.textContent = getTitle(text) || "Untitled";
179
143
  }
180
144
 
181
- // --- HTML-to-Markdown reverse conversion (for contenteditable) ---
182
-
183
- function nodeToMd(node) {
184
- if (node.nodeType === 3) return node.textContent;
185
- if (node.nodeType !== 1) return "";
186
-
187
- var tag = node.tagName;
188
- var inner = childrenToMd(node);
189
-
190
- switch (tag) {
191
- case "STRONG": case "B": return "**" + inner + "**";
192
- case "EM": case "I": return "*" + inner + "*";
193
- case "DEL": case "S": case "STRIKE": return "~~" + inner + "~~";
194
- case "CODE": return "`" + inner + "`";
195
- case "BR": return "\n";
196
- case "DIV":
197
- if (node.classList.contains("sn-title")) return inner;
198
- if (node.classList.contains("sn-placeholder")) return "";
199
- // Browser-generated div from Enter key = new line
200
- return "\n" + inner;
201
- case "P": return "\n" + inner;
202
- case "A": return node.getAttribute("href") || inner;
203
- case "SPAN":
204
- if (node.classList.contains("sn-check")) {
205
- return node.classList.contains("checked") ? "- [x]" : "- [ ]";
206
- }
207
- if (node.classList.contains("sn-placeholder")) return "";
208
- return inner;
209
- default: return inner;
210
- }
211
- }
212
-
213
- function childrenToMd(el) {
214
- var result = "";
215
- for (var i = 0; i < el.childNodes.length; i++) {
216
- result += nodeToMd(el.childNodes[i]);
217
- }
218
- return result;
219
- }
220
-
221
- function extractMdFromRendered(rendered) {
222
- var titleEl = rendered.querySelector(".sn-title");
223
- if (titleEl) {
224
- var titleMd = childrenToMd(titleEl);
225
- var rest = "";
226
- var afterTitle = false;
227
- for (var i = 0; i < rendered.childNodes.length; i++) {
228
- var child = rendered.childNodes[i];
229
- if (child === titleEl) { afterTitle = true; continue; }
230
- if (afterTitle) rest += nodeToMd(child);
231
- }
232
- if (rest && rest.charAt(0) === "\n") rest = rest.substring(1);
233
- return titleMd + (rest ? "\n" + rest : "");
234
- }
235
- var md = childrenToMd(rendered);
236
- return md.replace(/^\n+/, "");
237
- }
238
-
239
145
  // --- Note rendering ---
240
146
 
241
147
  function renderNote(data) {
@@ -613,13 +519,29 @@ function setupTextEdit(textarea, rendered, noteId, mdBtn) {
613
519
  var noteEl = textarea.closest(".sticky-note");
614
520
  var mdMode = false;
615
521
 
522
+ function saveRenderedMarkdown() {
523
+ var markdown = extractMarkdown(rendered);
524
+ textarea.value = markdown;
525
+ debouncedTextUpdate(noteId, markdown);
526
+ syncTitle(noteEl, markdown);
527
+ rendered.classList.toggle("is-empty", !markdown.trim());
528
+ }
529
+
530
+ function toggleChecklistItem(check) {
531
+ var checked = !check.classList.contains("checked");
532
+ check.classList.toggle("checked", checked);
533
+ check.textContent = checked ? "✓" : "☐";
534
+ check.setAttribute("aria-checked", checked ? "true" : "false");
535
+ saveRenderedMarkdown();
536
+ }
537
+
616
538
  // MD button: toggle between contenteditable rendered view and raw textarea
617
539
  mdBtn.addEventListener("click", function (e) {
618
540
  e.stopPropagation();
619
541
  mdMode = !mdMode;
620
542
  if (mdMode) {
621
543
  // Switch to raw markdown editing
622
- var md = extractMdFromRendered(rendered);
544
+ var md = extractMarkdown(rendered);
623
545
  textarea.value = md;
624
546
  textarea.style.display = "";
625
547
  rendered.style.display = "none";
@@ -646,11 +568,15 @@ function setupTextEdit(textarea, rendered, noteId, mdBtn) {
646
568
 
647
569
  // Sync contenteditable changes to textarea (data store) and save
648
570
  rendered.addEventListener("input", function () {
649
- var md = extractMdFromRendered(rendered);
650
- textarea.value = md;
651
- debouncedTextUpdate(noteId, md);
652
- syncTitle(noteEl, md);
653
- rendered.classList.toggle("is-empty", !md.trim());
571
+ saveRenderedMarkdown();
572
+ });
573
+
574
+ rendered.addEventListener("click", function (e) {
575
+ var check = e.target.closest && e.target.closest(".sn-check");
576
+ if (!check || !rendered.contains(check)) return;
577
+ e.preventDefault();
578
+ e.stopPropagation();
579
+ toggleChecklistItem(check);
654
580
  });
655
581
 
656
582
  // On blur, re-render to normalize HTML structure
@@ -658,7 +584,7 @@ function setupTextEdit(textarea, rendered, noteId, mdBtn) {
658
584
  // Don't re-render if clicking format toolbar (it prevents default, but just in case)
659
585
  if (e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(".sn-format-toolbar")) return;
660
586
  closeFormatToolbar();
661
- var md = extractMdFromRendered(rendered);
587
+ var md = extractMarkdown(rendered);
662
588
  textarea.value = md;
663
589
  if (md.trim()) {
664
590
  rendered.innerHTML = renderMiniMarkdown(md);
@@ -696,6 +622,12 @@ function setupTextEdit(textarea, rendered, noteId, mdBtn) {
696
622
 
697
623
  // Insert <br> on Enter instead of <div>
698
624
  rendered.addEventListener("keydown", function (e) {
625
+ if (e.target.classList && e.target.classList.contains("sn-check") &&
626
+ (e.key === "Enter" || e.key === " ")) {
627
+ e.preventDefault();
628
+ toggleChecklistItem(e.target);
629
+ return;
630
+ }
699
631
  if (e.key === "Enter" && !e.shiftKey) {
700
632
  e.preventDefault();
701
633
  document.execCommand("insertLineBreak");
@@ -979,7 +911,7 @@ function renderArchiveCards() {
979
911
  if (notes.size === 0) {
980
912
  var empty = document.createElement("div");
981
913
  empty.className = "notes-archive-empty";
982
- empty.innerHTML = iconHtml("sticky-note") + "<p>No sticky notes yet</p><p class=\"notes-archive-empty-sub\">Create one with the " + iconHtml("sticky-note") + " button in the title bar</p>";
914
+ empty.innerHTML = iconHtml("sticky-note") + "<p>Keep what matters across sessions</p><p class=\"notes-archive-empty-sub\">Use sticky notes for checklists, goals, handoffs, and durable project knowledge. You and every Clay agent can find them in future sessions.</p><p class=\"notes-archive-empty-sub\">Create one with the " + iconHtml("sticky-note") + " button in the title bar</p>";
983
915
  grid.appendChild(empty);
984
916
  refreshIcons();
985
917
  return;
@@ -2,7 +2,7 @@
2
2
 
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
- var MEMORY_CONTRACT = "Shared project memory on the user's board. One note = one fact, decision, or reminder, written like a real sticky note: a phrase or at most two short sentences. It must be worth a human's attention a week later and stand on its own without the current conversation. Never paragraphs, lists of steps, logs, or announcement notes about your own activity. Update an existing note instead of adding a near-duplicate; delete notes that stop being true.";
5
+ var MEMORY_CONTRACT = "Shared project memory that persists across sessions and is visible to the user and other Clay agents. People may use the board freely. As a Clay agent, use notes specifically for actionable or durable work memory: checklists and to-do lists, work goals, handoffs, unfinished work, durable decisions and constraints, and knowledge that should still be available after the current session ends. Every note starts with a concise plain-text title on the first line. Add a detailed body whenever another worker would otherwise need to reconstruct the context. Keep one coherent topic per note, update an existing note instead of adding a near-duplicate, and delete notes that stop being true. Do not use notes as a transcript, a routine progress log, or an announcement of your own activity.";
6
6
 
7
7
  function getToolDefs(handlers) {
8
8
  return [
@@ -14,10 +14,10 @@ function getToolDefs(handlers) {
14
14
  },
15
15
  {
16
16
  name: "write_note",
17
- description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. Long board notes are allowed, with a 2000-character abuse guard.",
17
+ description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. Long task and handoff notes are allowed, with a generous 20000-character abuse guard.",
18
18
  inputSchema: buildShape({
19
19
  id: { type: "string", description: "Existing note id to update. Omit to create a note." },
20
- text: { type: "string", description: "Sticky-note text. Keep the register concise even though the board permits up to 2000 characters." },
20
+ text: { type: "string", description: "Sticky-note text. Put the title on the first line, followed by the detailed handoff body. The board permits up to 20000 characters." },
21
21
  color: { type: "string", enum: ["yellow", "blue", "green", "pink", "orange", "purple"], description: "Optional sticky-note color." },
22
22
  }, ["text"]),
23
23
  handler: function (args) { return handlers.write(args || {}); },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.1.0-beta.1",
3
+ "version": "3.1.0",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",