clay-server 3.8.1 → 4.0.0-beta.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.
Files changed (222) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/background-task-timing.js +105 -0
  3. package/lib/builtin-mates.js +6 -6
  4. package/lib/claude-hook-installer.js +15 -0
  5. package/lib/clay-history-mcp-server.js +62 -297
  6. package/lib/daemon.js +23 -1
  7. package/lib/debate-model-selection.js +119 -0
  8. package/lib/home-debate-tool-policy.js +164 -0
  9. package/lib/home-response-notifications.js +26 -0
  10. package/lib/knowledge-import.js +436 -0
  11. package/lib/knowledge-record-store.js +188 -0
  12. package/lib/knowledge-search.js +224 -0
  13. package/lib/mate-creation-mcp-server.js +55 -0
  14. package/lib/mate-knowledge-mcp-server.js +151 -0
  15. package/lib/mate-knowledge-migration.js +283 -0
  16. package/lib/mate-knowledge-service.js +488 -0
  17. package/lib/mate-knowledge-sync.js +482 -0
  18. package/lib/mate-ready-creation.js +95 -0
  19. package/lib/mates.js +2 -0
  20. package/lib/notes.js +2 -1
  21. package/lib/project-ask-user.js +64 -0
  22. package/lib/project-capsule-catalog.js +101 -0
  23. package/lib/project-connection.js +17 -0
  24. package/lib/project-debate-proposal.js +133 -17
  25. package/lib/project-debate.js +214 -110
  26. package/lib/project-delegated-follow-up.js +84 -0
  27. package/lib/project-delegated-session.js +53 -0
  28. package/lib/project-filesystem.js +7 -9
  29. package/lib/project-knowledge.js +23 -0
  30. package/lib/project-logs-comments.js +137 -0
  31. package/lib/project-logs-mcp-server.js +290 -0
  32. package/lib/project-logs-query.js +167 -0
  33. package/lib/project-logs-root.js +67 -0
  34. package/lib/project-logs-schema.js +259 -0
  35. package/lib/project-logs-service.js +357 -0
  36. package/lib/project-logs-snapshot.js +202 -0
  37. package/lib/project-logs-store.js +498 -0
  38. package/lib/project-logs-versioning.js +128 -0
  39. package/lib/project-logs.js +347 -0
  40. package/lib/project-mate-creation-proposal.js +120 -0
  41. package/lib/project-mate-interaction.js +14 -0
  42. package/lib/project-mate-knowledge.js +110 -0
  43. package/lib/project-memory.js +36 -7
  44. package/lib/project-models.js +157 -32
  45. package/lib/project-session-notes.js +2 -2
  46. package/lib/project-session-pair.js +8 -8
  47. package/lib/project-sessions.js +66 -51
  48. package/lib/project-vendor-login.js +414 -0
  49. package/lib/project-worker-proposal.js +25 -25
  50. package/lib/project-workspace-query.js +144 -0
  51. package/lib/project.js +433 -102
  52. package/lib/public/app.js +109 -48
  53. package/lib/public/css/avatar-imprints.css +0 -1
  54. package/lib/public/css/base.css +4 -0
  55. package/lib/public/css/capsule-ui.css +389 -0
  56. package/lib/public/css/command-palette.css +306 -6
  57. package/lib/public/css/home-capsule-library.css +243 -0
  58. package/lib/public/css/home-capsule-source.css +192 -0
  59. package/lib/public/css/home-debate-live.css +146 -0
  60. package/lib/public/css/home-debate-planning.css +125 -0
  61. package/lib/public/css/home-debates-archive.css +281 -0
  62. package/lib/public/css/home-hub.css +896 -405
  63. package/lib/public/css/home-mate-creation.css +34 -0
  64. package/lib/public/css/home-mate-model-picker.css +131 -0
  65. package/lib/public/css/home-mate-settings.css +344 -0
  66. package/lib/public/css/home-session-actions.css +265 -0
  67. package/lib/public/css/home-sidebar.css +667 -0
  68. package/lib/public/css/icon-strip.css +1 -2
  69. package/lib/public/css/input.css +79 -25
  70. package/lib/public/css/mates.css +7 -34
  71. package/lib/public/css/pane.css +1 -1
  72. package/lib/public/css/playbook.css +0 -80
  73. package/lib/public/css/project-logs.css +234 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/sticky-notes.css +53 -117
  76. package/lib/public/css/tui-attention.css +0 -44
  77. package/lib/public/css/workspace-assignment.css +45 -0
  78. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  79. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  80. package/lib/public/index.html +96 -74
  81. package/lib/public/modules/app-connection.js +100 -5
  82. package/lib/public/modules/app-dm.js +43 -160
  83. package/lib/public/modules/app-home-hub.js +328 -585
  84. package/lib/public/modules/app-message-cards.js +242 -0
  85. package/lib/public/modules/app-message-router.js +84 -0
  86. package/lib/public/modules/app-messages.js +124 -38
  87. package/lib/public/modules/app-notifications.js +8 -119
  88. package/lib/public/modules/app-panels.js +43 -2
  89. package/lib/public/modules/app-projects.js +9 -1
  90. package/lib/public/modules/app-rendering.js +21 -602
  91. package/lib/public/modules/avatar-imprint.js +11 -8
  92. package/lib/public/modules/background-tasks-ui.js +139 -16
  93. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  94. package/lib/public/modules/chat-render-runtime.js +198 -0
  95. package/lib/public/modules/command-palette.js +225 -436
  96. package/lib/public/modules/dm-render.js +82 -0
  97. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  98. package/lib/public/modules/home-capsule-library.js +146 -0
  99. package/lib/public/modules/home-capsule-source.js +383 -0
  100. package/lib/public/modules/home-chat-identity.js +35 -0
  101. package/lib/public/modules/home-chat-scroll.js +77 -0
  102. package/lib/public/modules/home-chat-stream-state.js +34 -0
  103. package/lib/public/modules/home-composer-focus.js +25 -0
  104. package/lib/public/modules/home-conversations-sheet.js +179 -0
  105. package/lib/public/modules/home-debate-activity.js +48 -0
  106. package/lib/public/modules/home-debate-controls.js +295 -0
  107. package/lib/public/modules/home-debate-launch.js +41 -0
  108. package/lib/public/modules/home-debate-live.js +284 -0
  109. package/lib/public/modules/home-debate-models.js +73 -0
  110. package/lib/public/modules/home-debate-planning.js +335 -0
  111. package/lib/public/modules/home-debates-archive.js +279 -0
  112. package/lib/public/modules/home-dock-resize.js +74 -0
  113. package/lib/public/modules/home-dock.js +446 -0
  114. package/lib/public/modules/home-mate-chat.js +496 -0
  115. package/lib/public/modules/home-mate-creation.js +109 -0
  116. package/lib/public/modules/home-mate-model-picker.js +281 -0
  117. package/lib/public/modules/home-mate-selection.js +38 -0
  118. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  119. package/lib/public/modules/home-mate-settings.js +383 -0
  120. package/lib/public/modules/home-session-actions.js +373 -0
  121. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  122. package/lib/public/modules/home-shell.js +9 -0
  123. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  124. package/lib/public/modules/home-sidebar.js +165 -0
  125. package/lib/public/modules/home-sub-surface.js +19 -0
  126. package/lib/public/modules/home-surface-boot.js +68 -0
  127. package/lib/public/modules/home-surface.js +141 -0
  128. package/lib/public/modules/home-tools.js +271 -0
  129. package/lib/public/modules/markdown.js +50 -2
  130. package/lib/public/modules/mate-management.js +86 -0
  131. package/lib/public/modules/mate-sidebar.js +0 -8
  132. package/lib/public/modules/mate-wizard.js +0 -7
  133. package/lib/public/modules/mention.js +17 -19
  134. package/lib/public/modules/pane-bridge.js +9 -0
  135. package/lib/public/modules/profile.js +4 -5
  136. package/lib/public/modules/project-logs-ambient.js +238 -0
  137. package/lib/public/modules/project-logs-render.js +359 -0
  138. package/lib/public/modules/project-logs.js +328 -0
  139. package/lib/public/modules/project-settings.js +34 -28
  140. package/lib/public/modules/project-switcher.js +11 -5
  141. package/lib/public/modules/project-workspace-assignment.js +23 -0
  142. package/lib/public/modules/scheduler.js +17 -3
  143. package/lib/public/modules/search-clay-chat.js +402 -0
  144. package/lib/public/modules/server-settings.js +14 -12
  145. package/lib/public/modules/session-actions.js +1 -1
  146. package/lib/public/modules/sidebar-mates.js +56 -552
  147. package/lib/public/modules/sidebar-mobile.js +12 -73
  148. package/lib/public/modules/sidebar-presence.js +37 -0
  149. package/lib/public/modules/sidebar-sessions.js +25 -1
  150. package/lib/public/modules/split-pair-ui.js +13 -13
  151. package/lib/public/modules/split-view.js +7 -0
  152. package/lib/public/modules/sticky-notes.js +106 -53
  153. package/lib/public/modules/tool-input-composition.js +48 -0
  154. package/lib/public/modules/tool-llm-status.js +167 -0
  155. package/lib/public/modules/tool-palette.js +76 -4
  156. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  157. package/lib/public/modules/tool-renderer-chart.js +158 -0
  158. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  159. package/lib/public/modules/tool-renderer.js +464 -0
  160. package/lib/public/modules/tool-runtime.js +309 -0
  161. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  162. package/lib/public/modules/tool-ui-tree.js +21 -0
  163. package/lib/public/modules/tui-attention.js +10 -0
  164. package/lib/public/modules/user-settings.js +16 -23
  165. package/lib/public/modules/vendor-login.js +287 -0
  166. package/lib/public/modules/worker-proposal.js +2 -2
  167. package/lib/public/modules/workspace-assignment-card.js +149 -0
  168. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  169. package/lib/public/style.css +14 -2
  170. package/lib/runtime-env.js +65 -0
  171. package/lib/sdk-bridge.js +120 -32
  172. package/lib/sdk-message-processor.js +34 -13
  173. package/lib/server-home-capsule-creation.js +36 -0
  174. package/lib/server-home-chat-events.js +287 -0
  175. package/lib/server-home-chat.js +518 -0
  176. package/lib/server-home-clay-entry.js +65 -0
  177. package/lib/server-home-clay-session-links.js +26 -0
  178. package/lib/server-home-debate-planning.js +214 -0
  179. package/lib/server-home-debates.js +137 -0
  180. package/lib/server-home-mate-creation.js +261 -0
  181. package/lib/server-home-models.js +241 -0
  182. package/lib/server-home-preferences.js +61 -0
  183. package/lib/server-mates.js +37 -0
  184. package/lib/server-tools.js +377 -0
  185. package/lib/server.js +166 -59
  186. package/lib/session-pair-mcp-server.js +6 -6
  187. package/lib/sessions.js +22 -4
  188. package/lib/tool-capsule-source.js +142 -0
  189. package/lib/tool-control-mcp-server.js +126 -0
  190. package/lib/tool-llm.js +48 -0
  191. package/lib/tool-storage.js +77 -0
  192. package/lib/tool-ui-spec-advanced.js +82 -0
  193. package/lib/tool-ui-spec.js +261 -0
  194. package/lib/tools-registry.js +286 -0
  195. package/lib/users-home-dock-preferences.js +77 -0
  196. package/lib/users-home-surface-preferences.js +99 -0
  197. package/lib/users-preferences.js +4 -7
  198. package/lib/users.js +12 -0
  199. package/lib/whats-new-content.js +3 -3
  200. package/lib/workspace-assignment-service.js +420 -0
  201. package/lib/workspace-query-mcp-server.js +154 -0
  202. package/lib/workspace-query-service.js +492 -0
  203. package/lib/ws-schema.js +107 -12
  204. package/lib/yoke/adapters/acp.js +2 -1
  205. package/lib/yoke/adapters/antigravity.js +2 -1
  206. package/lib/yoke/adapters/claude-worker.js +11 -4
  207. package/lib/yoke/adapters/claude.js +141 -46
  208. package/lib/yoke/adapters/codex.js +79 -11
  209. package/lib/yoke/adapters/kiro.js +2 -1
  210. package/lib/yoke/claude-user-input.js +38 -0
  211. package/lib/yoke/codex-app-server.js +20 -2
  212. package/lib/yoke/codex-background-tasks.js +8 -2
  213. package/lib/yoke/codex-user-input.js +64 -0
  214. package/lib/yoke/complete-once.js +164 -0
  215. package/lib/yoke/index.js +5 -1
  216. package/lib/yoke/interface.js +19 -0
  217. package/lib/yoke/mcp-bridge-server.js +2 -6
  218. package/lib/yoke/user-input.js +254 -0
  219. package/package.json +4 -3
  220. package/lib/public/css/home-chat.css +0 -380
  221. package/lib/public/modules/home-chat.js +0 -438
  222. package/lib/server-clay-home.js +0 -245
@@ -0,0 +1,482 @@
1
+ // Compatibility write-through bridge for Mate durable memory and knowledge.
2
+ //
3
+ // The startup importer alone goes stale the moment a Mate writes a digest or a
4
+ // user edits a knowledge file. This module keeps the Knowledge scope current
5
+ // after every known mutation, without changing how the legacy files are
6
+ // written. Legacy files stay authoritative and rollback-compatible; the new
7
+ // backend is a continuously reconciled mirror.
8
+ //
9
+ // Reconciliation is source-level, not event-level. A caller says "this source
10
+ // changed" and this module re-reads that one file and makes the scope match it:
11
+ // creating, revising, reviving, or tombstoning as required. That makes every
12
+ // call idempotent and makes deletions and rewrites work without the call site
13
+ // having to describe what changed.
14
+ //
15
+ // Identity is never mirrored. CLAUDE.md, base-template.md, identity-backup.md,
16
+ // and identity-history.jsonl define who a Mate is and remain canonical files.
17
+ // Sticky notes are a separate attention layer and are excluded too.
18
+
19
+ var fs = require("fs");
20
+ var path = require("path");
21
+ var crypto = require("crypto");
22
+ var config = require("./config");
23
+ var knowledgeImport = require("./knowledge-import");
24
+
25
+ var SOURCE_NAMESPACE = "mate-knowledge";
26
+ var SOURCE_VERSION = 2;
27
+ var MAX_SOURCE_BYTES = 64 * 1024 * 1024;
28
+
29
+ // Identity and template artifacts. Never mirrored, at any version.
30
+ var IDENTITY_FILES = ["base-template.md", "identity-backup.md", "identity-history.jsonl"];
31
+ // Sticky notes remain the separate attention layer, with their own store.
32
+ var EXCLUDED_FILES = ["sticky-notes.md"];
33
+
34
+ // Line-addressed journals get one record per line, keyed by line content.
35
+ var JOURNAL_KINDS = {
36
+ "session-digests.jsonl": "session-digest",
37
+ "user-observations.jsonl": "user-observation",
38
+ };
39
+
40
+ function matesHome() {
41
+ return path.resolve(path.join(config.CONFIG_DIR, "mates"));
42
+ }
43
+
44
+ // Derived from the Mate storage root path alone, so the startup importer and
45
+ // the live bridge always agree on a scope without needing the same context.
46
+ function ownerKeyForRoot(matesRoot) {
47
+ var resolved = path.resolve(matesRoot);
48
+ var parent = path.dirname(resolved);
49
+ var name = path.basename(resolved);
50
+ if (parent === matesHome() && name.indexOf("mate_") !== 0) return "u-" + name;
51
+ return "h-" + crypto.createHash("sha256").update(resolved).digest("hex").substring(0, 16);
52
+ }
53
+
54
+ function scopeIdFor(ownerKey, mateId) {
55
+ return "mate/" + ownerKey + "/" + mateId;
56
+ }
57
+
58
+ // A Mate project's cwd is its Mate directory, so every call site can pass what
59
+ // it already has.
60
+ function scopeForMateDir(mateDir) {
61
+ var resolved = path.resolve(mateDir);
62
+ var mateId = path.basename(resolved);
63
+ if (mateId.indexOf("mate_") !== 0) return null;
64
+ var matesRoot = path.dirname(resolved);
65
+ var ownerKey = ownerKeyForRoot(matesRoot);
66
+ var parent = path.dirname(path.resolve(matesRoot));
67
+ return {
68
+ mateId: mateId,
69
+ mateDir: resolved,
70
+ matesRoot: matesRoot,
71
+ ownerKey: ownerKey,
72
+ userId: parent === matesHome() ? path.basename(matesRoot) : null,
73
+ scopeId: scopeIdFor(ownerKey, mateId),
74
+ };
75
+ }
76
+
77
+ function sourceKindFor(name) {
78
+ if (IDENTITY_FILES.indexOf(name) !== -1) return null;
79
+ if (EXCLUDED_FILES.indexOf(name) !== -1) return null;
80
+ if (name === "memory-summary.md") return "memory-summary";
81
+ if (JOURNAL_KINDS[name]) return JOURNAL_KINDS[name];
82
+ if (/\.md$/.test(name)) return "knowledge-file";
83
+ if (/\.jsonl$/.test(name)) return "knowledge-journal";
84
+ return null;
85
+ }
86
+
87
+ function isJournal(name) {
88
+ return !!JOURNAL_KINDS[name] || /\.jsonl$/.test(name);
89
+ }
90
+
91
+ function normalizeActor(actor) {
92
+ var source = actor || {};
93
+ var type = ["user", "agent", "system", "migration"].indexOf(source.type) !== -1 ? source.type : "system";
94
+ return {
95
+ type: type,
96
+ userId: source.userId ? String(source.userId).substring(0, 120) : null,
97
+ displayName: source.displayName ? String(source.displayName).substring(0, 120) : null,
98
+ sessionKey: source.sessionKey ? String(source.sessionKey).substring(0, 200) : null,
99
+ vendor: source.vendor ? String(source.vendor).substring(0, 40) : null,
100
+ };
101
+ }
102
+
103
+ // Relative to the Mate directory. The scope already identifies the owner and
104
+ // the Mate, so an absolute filesystem path would add nothing but exposure.
105
+ function relativeSource(name) {
106
+ return "knowledge/" + name;
107
+ }
108
+
109
+ function provenance(scope, name, extra) {
110
+ var base = {
111
+ namespace: SOURCE_NAMESPACE,
112
+ sourceVersion: SOURCE_VERSION,
113
+ relPath: relativeSource(name),
114
+ fileName: name,
115
+ ownerKey: scope.ownerKey,
116
+ userId: scope.userId,
117
+ mateId: scope.mateId,
118
+ mateName: scope.mateName || null,
119
+ };
120
+ if (!extra) return base;
121
+ return Object.assign(base, extra);
122
+ }
123
+
124
+ // Identify a live record that this module wrote, by provenance rather than by
125
+ // parsing an import key. A record from another namespace, or from a source
126
+ // version this build does not understand, is left strictly alone: reconciling
127
+ // it could tombstone something whose meaning we do not know.
128
+ function isMateSourceEntry(entry) {
129
+ if (!entry || entry.deleted === true) return false;
130
+ if (entry.kind === "chunk") return false;
131
+ var source = entry.source;
132
+ if (!source || source.namespace !== SOURCE_NAMESPACE) return false;
133
+ if (typeof source.sourceVersion !== "number" || source.sourceVersion > SOURCE_VERSION) return false;
134
+ var name = source.fileName;
135
+ if (!name || typeof name !== "string") return false;
136
+ // The file name and the relative path must agree, so neither field alone is
137
+ // trusted, and the name must be a plain basename.
138
+ if (path.basename(name) !== name) return false;
139
+ return source.relPath === relativeSource(name);
140
+ }
141
+
142
+ function fileKey(name) {
143
+ return "file:" + name;
144
+ }
145
+
146
+ function journalKey(name, hash) {
147
+ return "journal:" + name + ":" + hash.substring(0, 24);
148
+ }
149
+
150
+ function newSummary() {
151
+ return {
152
+ created: 0,
153
+ revised: 0,
154
+ revived: 0,
155
+ unchanged: 0,
156
+ deleted: 0,
157
+ absent: 0,
158
+ "already-deleted": 0,
159
+ chunked: 0,
160
+ malformedLines: 0,
161
+ identitySkipped: 0,
162
+ excluded: 0,
163
+ unsupported: 0,
164
+ empty: 0,
165
+ failed: 0,
166
+ errors: [],
167
+ };
168
+ }
169
+
170
+ function record(summary, action) {
171
+ if (summary[action] === undefined) summary[action] = 0;
172
+ summary[action]++;
173
+ }
174
+
175
+ function readStat(filePath) {
176
+ try {
177
+ return fs.statSync(filePath);
178
+ } catch (e) {
179
+ return null;
180
+ }
181
+ }
182
+
183
+ // --- File-addressed sources -------------------------------------------
184
+
185
+ function syncFile(importer, scope, name, kind, filePath, stat, actor, summary) {
186
+ if (!stat) {
187
+ // The legacy file is gone. Tombstone rather than leaving stale content live.
188
+ var removed = importer.removeRecord(fileKey(name), {
189
+ actor: actor,
190
+ source: provenance(scope, name, { type: kind, reason: "source-removed" }),
191
+ });
192
+ record(summary, removed.action);
193
+ return;
194
+ }
195
+ if (stat.size > MAX_SOURCE_BYTES) {
196
+ summary.failed++;
197
+ summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Source exceeds the maximum representable size." });
198
+ return;
199
+ }
200
+ var content;
201
+ try {
202
+ content = fs.readFileSync(filePath, "utf8");
203
+ } catch (e) {
204
+ summary.failed++;
205
+ summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Unreadable source: " + e.message });
206
+ return;
207
+ }
208
+ if (!content.trim()) {
209
+ // An emptied file is a logical removal, not a blank record.
210
+ var emptied = importer.removeRecord(fileKey(name), {
211
+ actor: actor,
212
+ source: provenance(scope, name, { type: kind, reason: "source-emptied" }),
213
+ });
214
+ summary.empty++;
215
+ if (emptied.action === "deleted") summary.deleted++;
216
+ return;
217
+ }
218
+ var result = importer.importRecord({
219
+ importKey: fileKey(name),
220
+ sourceHash: knowledgeImport.contentHash(content),
221
+ kind: kind,
222
+ name: name,
223
+ content: content,
224
+ at: Math.round(stat.mtimeMs),
225
+ actor: actor,
226
+ source: provenance(scope, name, {
227
+ type: kind,
228
+ sourceBytes: stat.size,
229
+ sourceModifiedAt: stat.mtimeMs,
230
+ }),
231
+ });
232
+ record(summary, result.action);
233
+ if (content.length > knowledgeImport.MAX_CONTENT_CHARS) summary.chunked++;
234
+ // Verified whenever something was actually written. An unchanged source was
235
+ // already verified when it landed, so the steady-state path stays cheap.
236
+ if (result.action !== "unchanged") verifyRoundTrip(importer, fileKey(name), scope, name, summary);
237
+ }
238
+
239
+ // --- Line-addressed journals ------------------------------------------
240
+
241
+ // Reconciles the whole journal: every present line is imported, and every line
242
+ // previously imported from this journal that is no longer present is
243
+ // tombstoned. A digest deleted through the memory UI therefore stops being a
244
+ // live memory instead of lingering in the new backend.
245
+ function syncJournal(importer, scope, name, kind, filePath, stat, actor, summary) {
246
+ var raw = "";
247
+ if (stat) {
248
+ if (stat.size > MAX_SOURCE_BYTES) {
249
+ summary.failed++;
250
+ summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Source exceeds the maximum representable size." });
251
+ return;
252
+ }
253
+ try {
254
+ raw = fs.readFileSync(filePath, "utf8");
255
+ } catch (e) {
256
+ summary.failed++;
257
+ summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Unreadable source: " + e.message });
258
+ return;
259
+ }
260
+ }
261
+
262
+ var present = {};
263
+ var lines = raw.split("\n");
264
+ for (var i = 0; i < lines.length; i++) {
265
+ var line = lines[i].trim();
266
+ if (!line) continue;
267
+ var parsed = null;
268
+ try {
269
+ parsed = JSON.parse(line);
270
+ } catch (e) {
271
+ summary.malformedLines++;
272
+ continue;
273
+ }
274
+ // A journal entry is a plain object. An array or a bare scalar parses
275
+ // cleanly but is not a record, so it is skipped rather than imported.
276
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
277
+ summary.malformedLines++;
278
+ continue;
279
+ }
280
+ var hash = knowledgeImport.contentHash(line);
281
+ var key = journalKey(name, hash);
282
+ present[key] = true;
283
+ var result = importer.importRecord({
284
+ importKey: key,
285
+ sourceHash: hash,
286
+ kind: kind,
287
+ name: name,
288
+ content: line,
289
+ actor: actor,
290
+ source: provenance(scope, name, {
291
+ type: kind,
292
+ lineIndex: i,
293
+ sourceDate: typeof parsed.date === "string" ? parsed.date : null,
294
+ sourceBytes: Buffer.byteLength(line, "utf8"),
295
+ sourceModifiedAt: stat ? stat.mtimeMs : null,
296
+ }),
297
+ });
298
+ record(summary, result.action);
299
+ if (line.length > knowledgeImport.MAX_CONTENT_CHARS) summary.chunked++;
300
+ if (result.action === "created" || result.action === "revived") {
301
+ verifyRoundTrip(importer, key, scope, name, summary);
302
+ }
303
+ }
304
+
305
+ // Every live line this module imported from this journal that the legacy
306
+ // file no longer contains. Selected by provenance, so a record from another
307
+ // namespace sharing a file name is never touched.
308
+ var stale = [];
309
+ var live = importer.entries();
310
+ for (var e = 0; e < live.length; e++) {
311
+ if (!isMateSourceEntry(live[e])) continue;
312
+ if (live[e].source.fileName !== name) continue;
313
+ if (present[live[e].importKey]) continue;
314
+ stale.push(live[e].importKey);
315
+ }
316
+ for (var s = 0; s < stale.length; s++) {
317
+ var removed = importer.removeRecord(stale[s], {
318
+ actor: actor,
319
+ source: provenance(scope, name, { type: kind, reason: "line-removed" }),
320
+ });
321
+ record(summary, removed.action);
322
+ }
323
+ }
324
+
325
+ // No source may be reported as successfully mirrored while its content cannot
326
+ // be read back exactly.
327
+ function verifyRoundTrip(importer, importKey, scope, name, summary) {
328
+ var entries = importer.entries();
329
+ var entry = null;
330
+ for (var i = 0; i < entries.length; i++) {
331
+ if (entries[i].importKey === importKey) { entry = entries[i]; break; }
332
+ }
333
+ if (!entry) {
334
+ summary.failed++;
335
+ summary.errors.push({ scopeId: scope.scopeId, file: name, message: "Imported record is not readable: " + importKey });
336
+ return;
337
+ }
338
+ var read = importer.readContent(entry);
339
+ if (!read.complete) {
340
+ summary.failed++;
341
+ summary.errors.push({
342
+ scopeId: scope.scopeId,
343
+ file: name,
344
+ message: (entry.chunked ? "Chunked source did not reassemble: " : "Stored source did not verify: ") + (read.reason || "unknown"),
345
+ });
346
+ }
347
+ }
348
+
349
+ // --- Public API --------------------------------------------------------
350
+
351
+ function importerFor(scope, baseDir) {
352
+ return knowledgeImport.createKnowledgeImporter({ scopeId: scope.scopeId, baseDir: baseDir });
353
+ }
354
+
355
+ // Reconcile exactly one Mate knowledge source against the new backend. Safe to
356
+ // call after any legacy write, including a delete. Never throws.
357
+ function syncMateSource(opts) {
358
+ var options = opts || {};
359
+ var summary = options.summary || newSummary();
360
+ try {
361
+ var scope = options.scope || scopeForMateDir(options.mateDir);
362
+ if (!scope) return summary;
363
+ if (options.mateName) scope.mateName = options.mateName;
364
+ var name = path.basename(String(options.fileName || ""));
365
+ if (!name) return summary;
366
+ if (IDENTITY_FILES.indexOf(name) !== -1) { summary.identitySkipped++; return summary; }
367
+ if (EXCLUDED_FILES.indexOf(name) !== -1) { summary.excluded++; return summary; }
368
+ var kind = sourceKindFor(name);
369
+ if (!kind) { summary.unsupported++; return summary; }
370
+
371
+ var actor = normalizeActor(options.actor);
372
+ var importer = options.importer || importerFor(scope, options.baseDir);
373
+ var filePath = path.join(scope.mateDir, "knowledge", name);
374
+ var stat = readStat(filePath);
375
+
376
+ if (isJournal(name)) syncJournal(importer, scope, name, kind, filePath, stat, actor, summary);
377
+ else syncFile(importer, scope, name, kind, filePath, stat, actor, summary);
378
+ } catch (e) {
379
+ summary.failed++;
380
+ summary.errors.push({ message: e.message });
381
+ }
382
+ return summary;
383
+ }
384
+
385
+ // Alias that reads better at a call site reacting to a removal or a rewrite.
386
+ function reconcileSource(opts) {
387
+ return syncMateSource(opts);
388
+ }
389
+
390
+ // Reconcile every durable source in one Mate directory, in both directions.
391
+ //
392
+ // Enumerating the directory alone is not enough. A knowledge file or a whole
393
+ // journal can be deleted while the daemon is stopped, or by direct filesystem
394
+ // tooling, and nothing would ever call syncMateSource for it again. So the
395
+ // scope's own live records are also consulted: any source this module
396
+ // previously imported that is no longer on disk is reconciled, which tombstones
397
+ // a missing file record or every line of a missing journal.
398
+ function reconcileMate(opts) {
399
+ var options = opts || {};
400
+ var summary = options.summary || newSummary();
401
+ var scope = options.scope || scopeForMateDir(options.mateDir);
402
+ if (!scope) return summary;
403
+ if (options.mateName) scope.mateName = options.mateName;
404
+ var knowledgeDir = path.join(scope.mateDir, "knowledge");
405
+ var present = {};
406
+ var entries = [];
407
+ try {
408
+ entries = fs.readdirSync(knowledgeDir, { withFileTypes: true });
409
+ } catch (e) {
410
+ entries = [];
411
+ }
412
+ var importer = options.importer || importerFor(scope, options.baseDir);
413
+
414
+ function reconcileOne(fileName) {
415
+ syncMateSource({
416
+ scope: scope,
417
+ fileName: fileName,
418
+ actor: options.actor,
419
+ importer: importer,
420
+ baseDir: options.baseDir,
421
+ summary: summary,
422
+ });
423
+ }
424
+
425
+ for (var i = 0; i < entries.length; i++) {
426
+ if (!entries[i].isFile()) continue;
427
+ // Recorded as present whatever its kind, so an excluded or unsupported file
428
+ // that still exists is never mistaken for a deleted source.
429
+ present[entries[i].name] = true;
430
+ reconcileOne(entries[i].name);
431
+ }
432
+
433
+ var absent = {};
434
+ var live = importer.entries();
435
+ for (var e = 0; e < live.length; e++) {
436
+ if (!isMateSourceEntry(live[e])) continue;
437
+ var name = live[e].source.fileName;
438
+ if (present[name] || absent[name]) continue;
439
+ absent[name] = true;
440
+ }
441
+ var absentNames = Object.keys(absent);
442
+ for (var a = 0; a < absentNames.length; a++) reconcileOne(absentNames[a]);
443
+ return summary;
444
+ }
445
+
446
+ // Fire-and-forget wrapper for legacy write paths. A mirroring failure must
447
+ // never break the legacy write that already succeeded.
448
+ function syncQuietly(opts) {
449
+ try {
450
+ var summary = syncMateSource(opts);
451
+ if (summary.failed > 0 && summary.errors.length > 0) {
452
+ console.error("[knowledge-sync] Mate knowledge mirror failed:", summary.errors[0].message);
453
+ }
454
+ return summary;
455
+ } catch (e) {
456
+ console.error("[knowledge-sync] Mate knowledge mirror failed:", e.message);
457
+ return null;
458
+ }
459
+ }
460
+
461
+ module.exports = {
462
+ SOURCE_NAMESPACE: SOURCE_NAMESPACE,
463
+ SOURCE_VERSION: SOURCE_VERSION,
464
+ MAX_SOURCE_BYTES: MAX_SOURCE_BYTES,
465
+ IDENTITY_FILES: IDENTITY_FILES,
466
+ EXCLUDED_FILES: EXCLUDED_FILES,
467
+ JOURNAL_KINDS: JOURNAL_KINDS,
468
+ ownerKeyForRoot: ownerKeyForRoot,
469
+ scopeIdFor: scopeIdFor,
470
+ scopeForMateDir: scopeForMateDir,
471
+ sourceKindFor: sourceKindFor,
472
+ isMateSourceEntry: isMateSourceEntry,
473
+ normalizeActor: normalizeActor,
474
+ newSummary: newSummary,
475
+ fileKey: fileKey,
476
+ journalKey: journalKey,
477
+ importerFor: importerFor,
478
+ syncMateSource: syncMateSource,
479
+ reconcileSource: reconcileSource,
480
+ reconcileMate: reconcileMate,
481
+ syncQuietly: syncQuietly,
482
+ };
@@ -0,0 +1,95 @@
1
+ // Atomic finalization of an interviewed Mate. No placeholder is exposed unless
2
+ // the complete identity has passed validation and is ready to register.
3
+
4
+ var fs = require("fs");
5
+ var path = require("path");
6
+
7
+ function cleanLine(value, maxLength, field) {
8
+ if (typeof value !== "string") throw new Error(field + " is required.");
9
+ var clean = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
10
+ if (!clean) throw new Error(field + " is required.");
11
+ return clean.slice(0, maxLength);
12
+ }
13
+
14
+ function cleanList(value, maxItems) {
15
+ var source = Array.isArray(value) ? value : [];
16
+ var result = [];
17
+ for (var i = 0; i < source.length && result.length < maxItems; i++) {
18
+ if (typeof source[i] !== "string") continue;
19
+ var clean = source[i].replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
20
+ if (clean) result.push(clean);
21
+ }
22
+ return result;
23
+ }
24
+
25
+ function normalize(definition) {
26
+ var identity = typeof definition.identityMarkdown === "string" ? definition.identityMarkdown.replace(/\u0000/g, "").trim() : "";
27
+ if (identity.length < 200) throw new Error("The Mate identity is incomplete.");
28
+ if (identity.length > 30000) throw new Error("The Mate identity is too long.");
29
+ return {
30
+ name: cleanLine(definition.name, 80, "Mate name"),
31
+ bio: cleanLine(definition.bio, 280, "Mate bio"),
32
+ relationship: cleanLine(definition.relationship || "collaborator", 120, "Working relationship"),
33
+ activities: cleanList(definition.activities, 12),
34
+ communicationStyle: cleanList(definition.communicationStyle, 12),
35
+ autonomy: cleanLine(definition.autonomy || "Ask before consequential actions.", 500, "Autonomy"),
36
+ identityMarkdown: identity,
37
+ vendor: cleanLine(definition.vendor || "claude", 40, "Vendor"),
38
+ model: cleanLine(definition.model, 240, "Mate model"),
39
+ };
40
+ }
41
+
42
+ function createReadyMate(mates, mateCtx, definition) {
43
+ var ready = normalize(definition || {});
44
+ if (typeof mates.extractIdentity === "function" && mates.extractIdentity(ready.identityMarkdown).trim() !== ready.identityMarkdown.trim()) {
45
+ throw new Error("The proposed identity contains reserved system sections.");
46
+ }
47
+ var mate = null;
48
+ try {
49
+ mate = mates.createMate(mateCtx, {
50
+ relationship: ready.relationship,
51
+ activity: ready.activities,
52
+ communicationStyle: ready.communicationStyle,
53
+ vendor: ready.vendor,
54
+ model: ready.model,
55
+ autonomy: ready.autonomy,
56
+ });
57
+ var mateDir = mates.getMateDir(mateCtx, mate.id);
58
+ var identityPath = path.join(mateDir, "CLAUDE.md");
59
+ fs.writeFileSync(identityPath, ready.identityMarkdown + "\n");
60
+ mates.enforceAllSections(identityPath, { ctx: mateCtx, mateId: mate.id });
61
+ fs.mkdirSync(path.join(mateDir, "knowledge"), { recursive: true });
62
+ var yaml = "# Mate metadata\n";
63
+ yaml += "id: " + JSON.stringify(mate.id) + "\n";
64
+ yaml += "name: " + JSON.stringify(ready.name) + "\n";
65
+ yaml += "status: ready\n";
66
+ yaml += "createdBy: " + JSON.stringify(mate.createdBy) + "\n";
67
+ yaml += "createdAt: " + mate.createdAt + "\n";
68
+ yaml += "relationship: " + JSON.stringify(ready.relationship) + "\n";
69
+ yaml += "activities: " + JSON.stringify(ready.activities) + "\n";
70
+ yaml += "communicationStyle: " + JSON.stringify(ready.communicationStyle) + "\n";
71
+ yaml += "autonomy: " + JSON.stringify(ready.autonomy) + "\n";
72
+ yaml += "vendor: " + JSON.stringify(ready.vendor) + "\n";
73
+ yaml += "model: " + JSON.stringify(ready.model) + "\n";
74
+ fs.writeFileSync(path.join(mateDir, "mate.yaml"), yaml);
75
+ var updated = mates.updateMate(mateCtx, mate.id, {
76
+ name: ready.name,
77
+ bio: ready.bio,
78
+ status: "ready",
79
+ vendor: ready.vendor,
80
+ model: ready.model,
81
+ profile: Object.assign({}, mate.profile || {}, { displayName: ready.name, bio: ready.bio }),
82
+ seedData: Object.assign({}, mate.seedData || {}, { relationship: ready.relationship, activity: ready.activities, communicationStyle: ready.communicationStyle, autonomy: ready.autonomy }),
83
+ });
84
+ mates.backupIdentity(mateDir, ready.identityMarkdown);
85
+ mates.logIdentityChange(mateDir, "clay_interview", ready.identityMarkdown, "");
86
+ return updated;
87
+ } catch (error) {
88
+ if (mate && mate.id) {
89
+ try { mates.deleteMate(mateCtx, mate.id); } catch (cleanupError) {}
90
+ }
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ module.exports = { createReadyMate: createReadyMate, normalize: normalize };
package/lib/mates.js CHANGED
@@ -128,6 +128,7 @@ function createMate(ctx, seedData) {
128
128
  createdAt: Date.now(),
129
129
  seedData: seedData || {},
130
130
  vendor: (seedData && seedData.vendor) || "claude",
131
+ model: (seedData && seedData.model) || null,
131
132
  profile: {
132
133
  displayName: null,
133
134
  avatarColor: "#5857fc",
@@ -156,6 +157,7 @@ function createMate(ctx, seedData) {
156
157
  yaml += "relationship: " + (seedData.relationship || "assistant") + "\n";
157
158
  yaml += "activities: " + JSON.stringify(seedData.activity || []) + "\n";
158
159
  yaml += "vendor: " + (seedData.vendor || "claude") + "\n";
160
+ yaml += "model: " + JSON.stringify(seedData.model || null) + "\n";
159
161
  fs.writeFileSync(path.join(mateDir, "mate.yaml"), yaml);
160
162
 
161
163
  // Write initial CLAUDE.md (will be replaced by interview)
package/lib/notes.js CHANGED
@@ -53,7 +53,8 @@ function createNotesManager(opts) {
53
53
  y: typeof data.y === "number" ? data.y : 100,
54
54
  w: typeof data.w === "number" ? data.w : 240,
55
55
  h: typeof data.h === "number" ? data.h : 180,
56
- color: data.color || "yellow",
56
+ color: data.color || "purple",
57
+ opacity: typeof data.opacity === "number" ? data.opacity : 0.64,
57
58
  minimized: false,
58
59
  zIndex: notes.length + 1,
59
60
  createdAt: now,
@@ -0,0 +1,64 @@
1
+ var userInput = require("./yoke/user-input");
2
+
3
+ function attachAskUser(ctx) {
4
+ function isFirstDebateQuestion(session) {
5
+ var history = session && Array.isArray(session.history) ? session.history : [];
6
+ for (var i = 0; i < history.length; i++) {
7
+ if (history[i] && history[i].type === "tool_executing" && history[i].name === "AskUserQuestion") return false;
8
+ }
9
+ return !!(session && session.homeDebatePlanning === true);
10
+ }
11
+
12
+ function planningQuestions(session, questions) {
13
+ if (!isFirstDebateQuestion(session)) return questions;
14
+ var first = questions[0] || {};
15
+ return [Object.assign({}, first, { multiSelect: false, options: [] })];
16
+ }
17
+
18
+ function handleRequest(boundSession, request, respond) {
19
+ if (!boundSession) {
20
+ respond.cancel("This question is not bound to an active session.");
21
+ return;
22
+ }
23
+ if (boundSession.loop && boundSession.loop.active && boundSession.loop.role !== "crafting") {
24
+ respond.cancel("Autonomous mode. Make your own decision.");
25
+ return;
26
+ }
27
+ var questions = planningQuestions(boundSession, request.questions);
28
+ var input = { questions: questions };
29
+ if (!boundSession.pendingAskUser) boundSession.pendingAskUser = {};
30
+ var pending = {
31
+ input: input,
32
+ mode: "yoke",
33
+ request: request,
34
+ respond: respond,
35
+ sessionId: boundSession.localId,
36
+ postedAt: Date.now(),
37
+ };
38
+ boundSession.pendingAskUser[request.id] = pending;
39
+ if (respond && typeof respond.onSettle === "function") {
40
+ respond.onSettle(function () {
41
+ if (boundSession.pendingAskUser && boundSession.pendingAskUser[request.id] === pending) delete boundSession.pendingAskUser[request.id];
42
+ });
43
+ }
44
+ ctx.record(boundSession, { type: "tool_executing", id: request.id, name: "AskUserQuestion", input: input });
45
+ }
46
+
47
+ function createHandler(boundSession) {
48
+ return function (request, respond) { handleRequest(boundSession, request, respond); };
49
+ }
50
+
51
+ function createMcpServer(adapter, boundSession) {
52
+ return userInput.createFallbackServer(adapter, createHandler(boundSession), {
53
+ force: !!(boundSession && boundSession.debateSetupMode),
54
+ });
55
+ }
56
+
57
+ return {
58
+ createHandler: createHandler,
59
+ createMcpServer: createMcpServer,
60
+ getToolDefs: function (boundSession) { return userInput.fallbackToolDefs(createHandler(boundSession)); },
61
+ };
62
+ }
63
+
64
+ module.exports = { attachAskUser: attachAskUser };