clay-server 5.2.0-beta.4 → 5.2.0-beta.6

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 (301) hide show
  1. package/lib/account-auth-socket.js +1 -0
  2. package/lib/builtin-mates.js +17 -14
  3. package/lib/canonical-clay-record-authority.js +15 -0
  4. package/lib/canonical-clay-scheduling-authority.js +19 -0
  5. package/lib/claude-hook-installer.js +1 -239
  6. package/lib/claude-jsonl-watcher.js +1 -204
  7. package/lib/clay-mate-chat-receipts.js +26 -0
  8. package/lib/clay-mate-chat.js +87 -0
  9. package/lib/clay-mate-knowledge-input.js +26 -0
  10. package/lib/clay-mate-knowledge.js +95 -0
  11. package/lib/clay-mate-lifecycle.js +95 -0
  12. package/lib/clay-product-control-input.js +33 -0
  13. package/lib/clay-product-control-tools.js +51 -0
  14. package/lib/clay-product-control.js +237 -0
  15. package/lib/clay-project-lifecycle.js +154 -0
  16. package/lib/clay-project-record-input.js +44 -0
  17. package/lib/clay-project-records.js +83 -0
  18. package/lib/clay-scheduling-input.js +21 -0
  19. package/lib/clay-scheduling-receipts.js +26 -0
  20. package/lib/clay-scheduling.js +119 -0
  21. package/lib/clay-screen-controls.js +96 -0
  22. package/lib/clay-skill-install.js +1 -33
  23. package/lib/clay-workspace-controls.js +83 -0
  24. package/lib/clay-workspace-input.js +17 -0
  25. package/lib/clay-workspace-receipts.js +26 -0
  26. package/lib/cli-sessions.js +1 -300
  27. package/lib/codex-defaults.js +1 -28
  28. package/lib/context-usage.js +3 -2
  29. package/lib/daemon-project-access.js +11 -0
  30. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  31. package/lib/daemon-project-lifecycle.js +262 -0
  32. package/lib/daemon-projects.js +1 -1
  33. package/lib/daemon.js +78 -276
  34. package/lib/default-ai-runtime.js +4 -8
  35. package/lib/email-mcp-server.js +1 -1
  36. package/lib/issues-service.js +29 -15
  37. package/lib/kiro-defaults.js +1 -22
  38. package/lib/knowledge-db-import.js +5 -4
  39. package/lib/knowledge-db-service.js +9 -6
  40. package/lib/knowledge-document-index.js +2 -1
  41. package/lib/knowledge-product-operations.js +46 -0
  42. package/lib/mate-creation-mcp-server.js +2 -0
  43. package/lib/mate-knowledge-canonical-access.js +10 -0
  44. package/lib/mate-knowledge-service.js +7 -4
  45. package/lib/mate-lifecycle-service.js +94 -0
  46. package/lib/mate-message-delivery.js +43 -0
  47. package/lib/mate-ready-creation.js +9 -1
  48. package/lib/mate-schedule-input.js +29 -0
  49. package/lib/mate-schedule-lifecycle.js +151 -0
  50. package/lib/mate-scheduling.js +103 -0
  51. package/lib/mates.js +4 -3
  52. package/lib/mcp-proxy.js +2 -32
  53. package/lib/mcp-query-bridge.js +1 -1
  54. package/lib/mcp-source-adapters.js +1 -91
  55. package/lib/mcp-source-context.js +1 -42
  56. package/lib/mcp-source-discovery.js +1 -169
  57. package/lib/mcp-source-plugins.js +1 -68
  58. package/lib/mcp-source-restrictions.js +1 -47
  59. package/lib/mcp-tool-policy.js +1 -26
  60. package/lib/notes-checklist.js +50 -0
  61. package/lib/notes.js +49 -3
  62. package/lib/os-user-diagnostics.js +2 -4
  63. package/lib/os-users.js +24 -70
  64. package/lib/project-admin-overview.js +51 -0
  65. package/lib/project-ask-user.js +4 -2
  66. package/lib/project-connection.js +8 -7
  67. package/lib/project-delegated-follow-up.js +2 -1
  68. package/lib/project-directory-claim.js +65 -0
  69. package/lib/project-file-operations.js +61 -0
  70. package/lib/project-filesystem.js +13 -87
  71. package/lib/project-full-access.js +23 -2
  72. package/lib/project-http.js +4 -4
  73. package/lib/project-knowledge.js +32 -18
  74. package/lib/project-lifecycle-operations.js +98 -0
  75. package/lib/project-lifecycle-process-witness.js +32 -0
  76. package/lib/project-lifecycle-runtime.js +50 -0
  77. package/lib/project-linux-access.js +22 -0
  78. package/lib/project-logs-canonical-clay.js +27 -0
  79. package/lib/project-logs-clay-query.js +16 -0
  80. package/lib/project-logs-schema.js +4 -2
  81. package/lib/project-logs-service.js +8 -7
  82. package/lib/project-logs.js +1 -0
  83. package/lib/project-loop.js +1 -0
  84. package/lib/project-mate-chat-control.js +55 -0
  85. package/lib/project-mate-creation-proposal.js +18 -3
  86. package/lib/project-mcp.js +2 -2
  87. package/lib/project-models.js +14 -31
  88. package/lib/project-notifications.js +74 -16
  89. package/lib/project-ownership.js +77 -0
  90. package/lib/project-pair-result-delivery.js +14 -3
  91. package/lib/project-pair-usage.js +12 -13
  92. package/lib/project-pending-message-queue.js +4 -1
  93. package/lib/project-product-control.js +87 -0
  94. package/lib/project-product-session-queue.js +46 -0
  95. package/lib/project-push-access.js +28 -0
  96. package/lib/project-scheduled-message-queue.js +30 -0
  97. package/lib/project-scheduled-messages.js +63 -64
  98. package/lib/project-scheduled-task-control.js +83 -0
  99. package/lib/project-scheduled-tasks.js +13 -2
  100. package/lib/project-scheduling-access.js +10 -0
  101. package/lib/project-session-actions.js +65 -0
  102. package/lib/project-session-delete.js +8 -1
  103. package/lib/project-session-github.js +47 -12
  104. package/lib/project-session-history-actions.js +167 -0
  105. package/lib/project-session-pair.js +1 -1
  106. package/lib/project-session-runtime-actions.js +64 -0
  107. package/lib/project-session-spawn.js +2 -3
  108. package/lib/project-session-vendor.js +55 -0
  109. package/lib/project-sessions.js +82 -267
  110. package/lib/project-user-message.js +31 -1
  111. package/lib/project-vendor-login.js +6 -36
  112. package/lib/project-workspace-operations.js +126 -0
  113. package/lib/project-workspace-query.js +9 -2
  114. package/lib/project.js +95 -3
  115. package/lib/public/app.js +1 -0
  116. package/lib/public/css/clay-tool-activity.css +51 -0
  117. package/lib/public/css/home-mate-creation.css +5 -0
  118. package/lib/public/css/messages.css +6 -2
  119. package/lib/public/css/overlays.css +4 -2
  120. package/lib/public/css/project-admin-overview.css +13 -0
  121. package/lib/public/css/session-folders.css +11 -0
  122. package/lib/public/css/session-github.css +3 -0
  123. package/lib/public/css/sticky-notes.css +61 -1
  124. package/lib/public/index.html +1 -1
  125. package/lib/public/modules/admin-settings-projects.js +6 -2
  126. package/lib/public/modules/app-connection.js +8 -1
  127. package/lib/public/modules/app-favicon.js +11 -2
  128. package/lib/public/modules/app-header.js +4 -2
  129. package/lib/public/modules/app-loop-ui.js +2 -0
  130. package/lib/public/modules/app-message-router.js +1 -1
  131. package/lib/public/modules/app-messages.js +54 -8
  132. package/lib/public/modules/app-notifications.js +1 -2
  133. package/lib/public/modules/app-panels.js +61 -123
  134. package/lib/public/modules/app-projects.js +5 -0
  135. package/lib/public/modules/app-rate-limit.js +7 -27
  136. package/lib/public/modules/chat-render-runtime.js +3 -0
  137. package/lib/public/modules/clay-owner-navigation.js +27 -0
  138. package/lib/public/modules/clay-tool-activity.js +140 -0
  139. package/lib/public/modules/clay-tool-presentation.js +338 -0
  140. package/lib/public/modules/command-palette.js +1 -1
  141. package/lib/public/modules/default-ai.js +11 -3
  142. package/lib/public/modules/home-debate-planning.js +5 -5
  143. package/lib/public/modules/home-mate-chat.js +8 -6
  144. package/lib/public/modules/home-mate-creation.js +73 -1
  145. package/lib/public/modules/mcp-connection-view.js +1 -1
  146. package/lib/public/modules/permission-control.js +5 -4
  147. package/lib/public/modules/personal-settings-dialog.js +7 -1
  148. package/lib/public/modules/project-access-recovery.js +17 -0
  149. package/lib/public/modules/project-admin-overview.js +21 -0
  150. package/lib/public/modules/scheduler.js +2 -0
  151. package/lib/public/modules/session-create-form.js +10 -10
  152. package/lib/public/modules/session-folder-layout.js +1 -1
  153. package/lib/public/modules/session-folder-view-menu.js +9 -0
  154. package/lib/public/modules/session-github.js +65 -8
  155. package/lib/public/modules/settings-defaults.js +27 -28
  156. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  157. package/lib/public/modules/sticky-note-markdown.js +72 -6
  158. package/lib/public/modules/sticky-notes-actions.js +30 -0
  159. package/lib/public/modules/sticky-notes-browser.js +9 -2
  160. package/lib/public/modules/sticky-notes-card.js +23 -7
  161. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  162. package/lib/public/modules/sticky-notes-editor.js +108 -44
  163. package/lib/public/modules/sticky-notes-shared.js +50 -2
  164. package/lib/public/modules/sticky-notes-state.js +66 -0
  165. package/lib/public/modules/sticky-notes.js +34 -51
  166. package/lib/public/modules/thinking-lifecycle.js +33 -3
  167. package/lib/public/modules/thinking-view.js +23 -1
  168. package/lib/public/modules/tools.js +36 -7
  169. package/lib/public/modules/tui-grab.js +1 -2
  170. package/lib/public/modules/user-settings-appearance.js +10 -3
  171. package/lib/public/modules/user-settings-editors.js +1 -1
  172. package/lib/public/modules/user-settings-layout.js +11 -2
  173. package/lib/public/modules/user-settings-preferences.js +6 -2
  174. package/lib/public/modules/user-settings-profile.js +12 -2
  175. package/lib/public/modules/user-settings.js +3 -2
  176. package/lib/public/modules/vendor-login-browser.js +5 -3
  177. package/lib/public/modules/vendor-login.js +2 -2
  178. package/lib/public/modules/vendor-priority.js +15 -1
  179. package/lib/public/muse-avatar.svg +4 -0
  180. package/lib/public/style.css +2 -0
  181. package/lib/safe-bash-commands.js +3 -45
  182. package/lib/scheduled-task-records.js +3 -1
  183. package/lib/scheduler.js +2 -0
  184. package/lib/sdk-bridge.js +334 -403
  185. package/lib/sdk-foreground-admission.js +16 -0
  186. package/lib/sdk-mcp-descriptors.js +2 -37
  187. package/lib/sdk-message-processor.js +52 -17
  188. package/lib/sdk-quota-snapshot.js +1 -1
  189. package/lib/sdk-response-recovery.js +1 -0
  190. package/lib/sdk-worker.js +2 -677
  191. package/lib/server-admin.js +63 -16
  192. package/lib/server-dm.js +1 -1
  193. package/lib/server-global-ws.js +10 -20
  194. package/lib/server-home-chat-events.js +7 -3
  195. package/lib/server-home-chat.js +4 -16
  196. package/lib/server-home-mate-creation.js +1 -0
  197. package/lib/server-home-mcp-consent.js +3 -3
  198. package/lib/server-mates.js +25 -67
  199. package/lib/server-owner-ui.js +77 -0
  200. package/lib/server-path-access.js +38 -0
  201. package/lib/server-project-removal.js +22 -0
  202. package/lib/server.js +100 -30
  203. package/lib/session-config-transition.js +45 -0
  204. package/lib/session-folders.js +7 -2
  205. package/lib/session-fork-history.js +21 -0
  206. package/lib/session-github.js +9 -2
  207. package/lib/session-hygiene.js +3 -35
  208. package/lib/session-permission-mode.js +4 -3
  209. package/lib/session-skill-scope.js +11 -0
  210. package/lib/session-visibility.js +5 -2
  211. package/lib/sessions.js +27 -89
  212. package/lib/terminal-manager.js +52 -0
  213. package/lib/tool-llm.js +1 -1
  214. package/lib/tui-transcript-index.js +1 -132
  215. package/lib/users-permissions.js +2 -8
  216. package/lib/users.js +4 -0
  217. package/lib/vendor-login-browser.js +1 -11
  218. package/lib/workspace-query-access.js +4 -1
  219. package/lib/ws-schema.js +8 -2
  220. package/lib/yoke/acp-agent-profiles.js +5 -3
  221. package/lib/yoke/adapters/antigravity.js +3 -14
  222. package/lib/yoke/adapters/claude-query-contract.js +9 -0
  223. package/lib/yoke/adapters/claude.js +54 -50
  224. package/lib/yoke/adapters/codex-query-contract.js +24 -0
  225. package/lib/yoke/adapters/codex.js +53 -111
  226. package/lib/yoke/adapters/kiro.js +3 -1
  227. package/lib/yoke/adapters/muse-query-contract.js +7 -0
  228. package/lib/yoke/adapters/muse.js +122 -0
  229. package/lib/yoke/auth-events.js +17 -0
  230. package/lib/yoke/claude-hook-installer.js +267 -0
  231. package/lib/yoke/claude-jsonl-watcher.js +204 -0
  232. package/lib/yoke/claude-maintenance.js +55 -0
  233. package/lib/yoke/claude-permission-patterns.js +53 -0
  234. package/lib/yoke/claude-response-recovery.js +155 -0
  235. package/lib/yoke/claude-transcript-hygiene.js +34 -0
  236. package/lib/yoke/claude-worker-startup.js +28 -0
  237. package/lib/yoke/clay-skill-install.js +33 -0
  238. package/lib/yoke/cli-sessions.js +300 -0
  239. package/lib/yoke/codex-app-server.js +7 -12
  240. package/lib/yoke/codex-idle-recovery.js +81 -1
  241. package/lib/yoke/codex-recovery-admission.js +164 -0
  242. package/lib/yoke/configuration.js +13 -0
  243. package/lib/yoke/discovery.js +70 -0
  244. package/lib/yoke/errors.js +8 -0
  245. package/lib/yoke/host-environment.js +7 -0
  246. package/lib/yoke/index.js +9 -51
  247. package/lib/yoke/interface.js +116 -0
  248. package/lib/yoke/kiro-acp-server.js +5 -5
  249. package/lib/yoke/legacy-sessions.js +89 -0
  250. package/lib/yoke/login-protocol.js +25 -0
  251. package/lib/yoke/maintenance.js +12 -0
  252. package/lib/yoke/mcp-proxy.js +32 -0
  253. package/lib/yoke/mcp-source-adapters.js +91 -0
  254. package/lib/yoke/mcp-source-context.js +42 -0
  255. package/lib/yoke/mcp-source-discovery.js +169 -0
  256. package/lib/yoke/mcp-source-plugins.js +68 -0
  257. package/lib/yoke/mcp-source-restrictions.js +47 -0
  258. package/lib/yoke/mcp-tool-policy-normalize.js +28 -0
  259. package/lib/yoke/model-preferences.js +6 -0
  260. package/lib/yoke/muse-artifacts.js +36 -0
  261. package/lib/yoke/muse-controls.js +50 -0
  262. package/lib/yoke/muse-input.js +64 -0
  263. package/lib/yoke/muse-items.js +157 -0
  264. package/lib/yoke/muse-policy.js +52 -0
  265. package/lib/yoke/muse-query.js +428 -0
  266. package/lib/yoke/muse-runtime.js +211 -0
  267. package/lib/yoke/muse-services.js +79 -0
  268. package/lib/yoke/muse-skills.js +79 -0
  269. package/lib/yoke/muse-turns.js +71 -0
  270. package/lib/yoke/options/acp.js +2 -0
  271. package/lib/yoke/options/antigravity.js +2 -0
  272. package/lib/yoke/options/claude.js +21 -0
  273. package/lib/yoke/options/codex-defaults.js +29 -0
  274. package/lib/yoke/options/codex-settings.js +21 -0
  275. package/lib/yoke/options/codex.js +6 -0
  276. package/lib/yoke/options/kiro-defaults.js +22 -0
  277. package/lib/yoke/options/kiro.js +5 -0
  278. package/lib/yoke/options/muse.js +5 -0
  279. package/lib/yoke/provider-contract.js +5 -0
  280. package/lib/yoke/providers/antigravity-executable.js +8 -0
  281. package/lib/yoke/providers/claude-executable.js +40 -0
  282. package/lib/yoke/providers/claude-model-descriptions.js +14 -0
  283. package/lib/yoke/providers/claude-process.js +97 -0
  284. package/lib/yoke/providers/claude-provision.js +75 -0
  285. package/lib/yoke/providers/codex-errors.js +8 -0
  286. package/lib/yoke/providers/muse-executable.js +17 -0
  287. package/lib/yoke/provisioning.js +6 -0
  288. package/lib/yoke/query-options.js +10 -0
  289. package/lib/yoke/response-recovery.js +41 -0
  290. package/lib/yoke/sdk-mcp-descriptors.js +37 -0
  291. package/lib/yoke/sdk-server-factory.js +7 -0
  292. package/lib/yoke/session-history.js +8 -0
  293. package/lib/yoke/session-tool-transport.js +34 -13
  294. package/lib/yoke/settings.js +23 -0
  295. package/lib/yoke/source-locations.js +4 -0
  296. package/lib/yoke/title-query.js +18 -7
  297. package/lib/yoke/tui-transcript-index.js +132 -0
  298. package/lib/yoke/usage.js +48 -0
  299. package/lib/yoke/user-input.js +13 -0
  300. package/lib/yoke/vendor-registry.js +81 -1
  301. package/package.json +3 -2
@@ -1,300 +1 @@
1
- var fs = require("fs");
2
- var path = require("path");
3
- var readline = require("readline");
4
- var utils = require("./utils");
5
- var { REAL_HOME } = require("./config");
6
-
7
- var encodeCwd = utils.encodeCwd;
8
-
9
- /**
10
- * Parse the first ~20 lines of a CLI session JSONL file to extract metadata.
11
- * Returns null if the file can't be parsed or has no user messages.
12
- */
13
- function parseSessionFile(filePath, maxLines) {
14
- if (maxLines == null) maxLines = 20;
15
- return new Promise(function (resolve) {
16
- var sessionId = path.basename(filePath, ".jsonl");
17
- var result = {
18
- sessionId: sessionId,
19
- firstPrompt: "",
20
- model: null,
21
- gitBranch: null,
22
- startTime: null,
23
- lastActivity: null,
24
- };
25
-
26
- var lineCount = 0;
27
- var foundUser = false;
28
- var stream;
29
- try {
30
- stream = fs.createReadStream(filePath, { encoding: "utf8" });
31
- } catch (e) {
32
- return resolve(null);
33
- }
34
-
35
- var rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
36
-
37
- rl.on("line", function (line) {
38
- lineCount++;
39
- if (lineCount > maxLines) {
40
- rl.close();
41
- stream.destroy();
42
- return;
43
- }
44
-
45
- var obj;
46
- try { obj = JSON.parse(line); } catch (e) { return; }
47
-
48
- // Skip file-history-snapshot, queue-operation, and other non-message records
49
- if (obj.type === "user" && obj.message && obj.message.role === "user") {
50
- if (!foundUser) {
51
- foundUser = true;
52
- result.sessionId = obj.sessionId || sessionId;
53
- result.gitBranch = obj.gitBranch || null;
54
- if (obj.timestamp) result.startTime = obj.timestamp;
55
- var content = obj.message.content || "";
56
- if (typeof content === "string") {
57
- result.firstPrompt = content.substring(0, 100);
58
- } else if (Array.isArray(content)) {
59
- for (var i = 0; i < content.length; i++) {
60
- if (content[i].type === "text" && content[i].text) {
61
- result.firstPrompt = content[i].text.substring(0, 100);
62
- break;
63
- }
64
- }
65
- }
66
- }
67
- // Track latest user timestamp for lastActivity
68
- if (obj.timestamp) result.lastActivity = obj.timestamp;
69
- }
70
-
71
- // Extract model from first assistant message
72
- if (!result.model && obj.message && obj.message.role === "assistant" && obj.message.model) {
73
- result.model = obj.message.model;
74
- }
75
- });
76
-
77
- rl.on("close", function () {
78
- if (!foundUser) return resolve(null);
79
-
80
- // Use file mtime as fallback for lastActivity, or as a better proxy
81
- // since we only read the first ~20 lines
82
- try {
83
- var stat = fs.statSync(filePath);
84
- var mtime = stat.mtime.toISOString();
85
- // File mtime is always more accurate for "last activity" since we
86
- // don't read the entire file
87
- result.lastActivity = mtime;
88
- } catch (e) {}
89
-
90
- resolve(result);
91
- });
92
-
93
- rl.on("error", function () {
94
- resolve(null);
95
- });
96
-
97
- stream.on("error", function () {
98
- rl.close();
99
- resolve(null);
100
- });
101
- });
102
- }
103
-
104
- /**
105
- * List CLI sessions for a given project directory.
106
- * Reads ~/.claude/projects/{encoded-cwd}/ and parses JSONL metadata.
107
- * Returns array sorted by lastActivity descending (most recent first).
108
- */
109
- function listCliSessions(cwd) {
110
- var encoded = encodeCwd(cwd);
111
- var projectDir = path.join(REAL_HOME, ".claude", "projects", encoded);
112
-
113
- return new Promise(function (resolve) {
114
- fs.readdir(projectDir, { withFileTypes: true }, function (err, entries) {
115
- if (err) return resolve([]);
116
-
117
- var jsonlFiles = [];
118
- for (var i = 0; i < entries.length; i++) {
119
- if (entries[i].isFile() && entries[i].name.endsWith(".jsonl")) {
120
- jsonlFiles.push(path.join(projectDir, entries[i].name));
121
- }
122
- }
123
-
124
- if (jsonlFiles.length === 0) return resolve([]);
125
-
126
- var pending = jsonlFiles.length;
127
- var results = [];
128
-
129
- for (var j = 0; j < jsonlFiles.length; j++) {
130
- parseSessionFile(jsonlFiles[j]).then(function (session) {
131
- if (session) results.push(session);
132
- pending--;
133
- if (pending === 0) {
134
- results.sort(function (a, b) {
135
- var ta = a.lastActivity || "";
136
- var tb = b.lastActivity || "";
137
- return ta < tb ? 1 : ta > tb ? -1 : 0;
138
- });
139
- resolve(results);
140
- }
141
- });
142
- }
143
- });
144
- });
145
- }
146
-
147
- /**
148
- * Get the most recent CLI session for a given project directory.
149
- * Returns the session object or null if none found.
150
- */
151
- function getMostRecentCliSession(cwd) {
152
- return listCliSessions(cwd).then(function (sessions) {
153
- return sessions.length > 0 ? sessions[0] : null;
154
- });
155
- }
156
-
157
- /**
158
- * Extract user message text from a CLI JSONL content field.
159
- * Content can be a string or an array of content blocks.
160
- */
161
- function extractText(content) {
162
- if (typeof content === "string") return content;
163
- if (!Array.isArray(content)) return "";
164
- var parts = [];
165
- for (var i = 0; i < content.length; i++) {
166
- if (content[i].type === "text" && content[i].text) {
167
- parts.push(content[i].text);
168
- }
169
- }
170
- return parts.join("");
171
- }
172
-
173
- /**
174
- * Read a full CLI session JSONL file and convert it to relay-compatible
175
- * history entries (user_message, delta, tool_start, tool_executing, tool_result).
176
- * Returns a Promise that resolves to an array of history entries.
177
- */
178
- // Convert one parsed CLI jsonl record into client history entries. Shared by
179
- // the streaming (async) and synchronous readers so the format stays in lockstep.
180
- // `state.toolCounter` carries across lines to mint unique tool ids.
181
- function appendCliRecord(obj, state, history) {
182
- if (!obj || !obj.message) return;
183
-
184
- // User prompt
185
- if (obj.type === "user" && obj.message.role === "user") {
186
- // Skip tool_result records (they have type "user" but content is tool results)
187
- var content = obj.message.content;
188
- if (Array.isArray(content) && content.length > 0 && content[0].type === "tool_result") {
189
- return;
190
- }
191
- var text = extractText(content);
192
- if (text) history.push({ type: "user_message", text: text });
193
- return;
194
- }
195
-
196
- // Assistant message
197
- if (obj.message.role === "assistant" && Array.isArray(obj.message.content)) {
198
- for (var i = 0; i < obj.message.content.length; i++) {
199
- var block = obj.message.content[i];
200
-
201
- if (block.type === "text" && block.text) {
202
- history.push({ type: "delta", text: block.text });
203
- }
204
-
205
- if (block.type === "tool_use") {
206
- var toolId = "cli-tool-" + (++state.toolCounter);
207
- var toolName = block.name || "Tool";
208
- history.push({ type: "tool_start", id: toolId, name: toolName });
209
- history.push({
210
- type: "tool_executing",
211
- id: toolId,
212
- name: toolName,
213
- input: block.input || {},
214
- });
215
- // Emit ask_user_answered so the client re-enables input after replaying AskUserQuestion
216
- if (toolName === "AskUserQuestion") {
217
- history.push({ type: "ask_user_answered", toolId: toolId });
218
- }
219
- history.push({ type: "tool_result", id: toolId, content: "" });
220
- }
221
- }
222
- }
223
- }
224
-
225
- function readCliSessionHistory(home, cwd, sessionId) {
226
- var encoded = encodeCwd(cwd);
227
- var filePath = path.join(home || REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
228
-
229
- return new Promise(function (resolve) {
230
- var history = [];
231
- var stream;
232
- try {
233
- stream = fs.createReadStream(filePath, { encoding: "utf8" });
234
- } catch (e) {
235
- return resolve([]);
236
- }
237
-
238
- var rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
239
- var state = { toolCounter: 0 };
240
-
241
- rl.on("line", function (line) {
242
- var obj;
243
- try { obj = JSON.parse(line); } catch (e) { return; }
244
- appendCliRecord(obj, state, history);
245
- });
246
-
247
- rl.on("close", function () {
248
- resolve(history);
249
- });
250
-
251
- rl.on("error", function () {
252
- resolve([]);
253
- });
254
-
255
- stream.on("error", function () {
256
- rl.close();
257
- resolve([]);
258
- });
259
- });
260
- }
261
-
262
- // Synchronous variant for callers that run inside a synchronous request
263
- // handler (e.g. switch_session, which must populate session.history before
264
- // the session_switched broadcast). Reads the whole jsonl - these transcripts
265
- // are small enough that blocking on a local read is fine.
266
- // Modified-time (ms) of a CLI session's jsonl, or 0 if missing. Lets callers
267
- // cheaply detect that the transcript grew (e.g. after a TUI turn) and re-read.
268
- function cliSessionFileMtime(home, cwd, sessionId) {
269
- var encoded = encodeCwd(cwd);
270
- var filePath = path.join(home || REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
271
- try { return fs.statSync(filePath).mtimeMs; } catch (e) { return 0; }
272
- }
273
-
274
- function readCliSessionHistorySync(home, cwd, sessionId) {
275
- var encoded = encodeCwd(cwd);
276
- var filePath = path.join(home || REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
277
- var raw;
278
- try { raw = fs.readFileSync(filePath, "utf8"); } catch (e) { return []; }
279
- var history = [];
280
- var state = { toolCounter: 0 };
281
- var lines = raw.split("\n");
282
- for (var i = 0; i < lines.length; i++) {
283
- if (!lines[i]) continue;
284
- var obj;
285
- try { obj = JSON.parse(lines[i]); } catch (e) { continue; }
286
- appendCliRecord(obj, state, history);
287
- }
288
- return history;
289
- }
290
-
291
- module.exports = {
292
- listCliSessions: listCliSessions,
293
- getMostRecentCliSession: getMostRecentCliSession,
294
- readCliSessionHistory: readCliSessionHistory,
295
- readCliSessionHistorySync: readCliSessionHistorySync,
296
- cliSessionFileMtime: cliSessionFileMtime,
297
- parseSessionFile: parseSessionFile,
298
- encodeCwd: encodeCwd,
299
- extractText: extractText,
300
- };
1
+ module.exports = require("./yoke/cli-sessions");
@@ -1,28 +1 @@
1
- var CODEX_DEFAULTS = {
2
- approval: "on-request",
3
- sandbox: "danger-full-access",
4
- webSearch: "live",
5
- };
6
-
7
- var CODEX_APPROVAL_POLICIES = ["untrusted", "on-request", "granular", "never"];
8
-
9
- function normalizeCodexApproval(value) {
10
- if (value === "on-failure") return CODEX_DEFAULTS.approval;
11
- if (CODEX_APPROVAL_POLICIES.indexOf(value) === -1) return CODEX_DEFAULTS.approval;
12
- return value;
13
- }
14
-
15
- function getCodexConfig(sm) {
16
- return {
17
- approval: normalizeCodexApproval(sm && sm.codexApproval),
18
- sandbox: (sm && sm.codexSandbox) || CODEX_DEFAULTS.sandbox,
19
- webSearch: (sm && sm.codexWebSearch) || CODEX_DEFAULTS.webSearch,
20
- };
21
- }
22
-
23
- module.exports = {
24
- CODEX_DEFAULTS: CODEX_DEFAULTS,
25
- CODEX_APPROVAL_POLICIES: CODEX_APPROVAL_POLICIES,
26
- normalizeCodexApproval: normalizeCodexApproval,
27
- getCodexConfig: getCodexConfig,
28
- };
1
+ module.exports = require("./yoke/options/codex-defaults");
@@ -16,8 +16,9 @@ function readUsage(session, detail) {
16
16
  return query.getContextUsage({ detail: level });
17
17
  }).then(function(data) {
18
18
  if (!data || session.queryInstance !== query || requests.get(session) !== state || state.sequence !== sequence) return null;
19
- session.lastContextUsage = data;
20
- return data;
19
+ var normalized = require("./yoke/usage").context(data, session.vendor || "claude");
20
+ session.lastContextUsage = normalized;
21
+ return normalized;
21
22
  }).finally(function() { delete state.pending[level]; });
22
23
  state.pending[level] = request;
23
24
  return request;
@@ -27,6 +27,12 @@ function uniqueUsers(values) {
27
27
  return out;
28
28
  }
29
29
 
30
+ function pendingLifecycleOperation(config, project) {
31
+ return Object.values(config.projectLifecycleOperations || {}).find(function (operation) {
32
+ return operation.phase === 'pending' && operation.slug === project.slug && operation.projectKnowledgeId === project.projectKnowledgeId && operation.expected === JSON.stringify(project);
33
+ });
34
+ }
35
+
30
36
  function getProjectAccess(config, slug, osUsers) {
31
37
  var parentSlug = parentSlugFor(slug);
32
38
  var project = findProject(config, parentSlug || slug);
@@ -42,6 +48,8 @@ function getProjectAccess(config, slug, osUsers) {
42
48
  if (!parentSlug) {
43
49
  return {
44
50
  slug: slug,
51
+ lifecycleExecutionPaused: project.lifecycleExecutionPaused === true || !!pendingLifecycleOperation(config, project),
52
+ ownershipRecovery: project.ownershipRecovery || null,
45
53
  visibility: visibility,
46
54
  allowedUsers: parentAllowedUsers,
47
55
  ownerId: project.ownerId || null,
@@ -54,6 +62,8 @@ function getProjectAccess(config, slug, osUsers) {
54
62
  var worktreeAllowedUsers = uniqueUsers(grants);
55
63
  return {
56
64
  slug: slug,
65
+ lifecycleExecutionPaused: project.lifecycleExecutionPaused === true || !!pendingLifecycleOperation(config, project),
66
+ ownershipRecovery: project.ownershipRecovery || null,
57
67
  visibility: visibility,
58
68
  allowedUsers: uniqueUsers(parentAllowedUsers.concat(worktreeAllowedUsers)),
59
69
  ownerId: project.ownerId || null,
@@ -129,6 +139,7 @@ function reconcileWorktreeGrants(config, parentSlug, existingSlugs) {
129
139
  }
130
140
 
131
141
  module.exports = {
142
+ pendingLifecycleOperation: pendingLifecycleOperation,
132
143
  parentSlugFor: parentSlugFor,
133
144
  getProjectAccess: getProjectAccess,
134
145
  setAllowedUsers: setAllowedUsers,
@@ -0,0 +1,15 @@
1
+ // The daemon and production UI fixtures share these exact callback adapters.
2
+ function attachLifecycleCallbacks(ctx) {
3
+ function attempt(action) {
4
+ try { return action(); }
5
+ catch (error) { return Object.assign({ ok: false, status: 'failed', error: error.message, persisted: false, retrySafe: true }, error.lifecycleResult || {}); }
6
+ }
7
+ return {
8
+ onAddProject: function (candidate, user) { return attempt(function () { return ctx.service.register(candidate, user); }); },
9
+ onCreateProject: function (name, user) { return attempt(function () { return ctx.service.create(name, user); }); },
10
+ onRemoveProject: function (slug, userId) { return ctx.service.remove(slug, userId); },
11
+ onSetProjectTitle: function (slug, title) { return attempt(function () { return ctx.service.update(slug, 'rename', title); }); },
12
+ onSetProjectEnv: function (slug, value) { return attempt(function () { return ctx.service.update(slug, 'environment', value); }); },
13
+ };
14
+ }
15
+ module.exports = { attachLifecycleCallbacks: attachLifecycleCallbacks };
@@ -0,0 +1,262 @@
1
+ // Shared ordinary-project mutations. Persistence precedes runtime publication.
2
+ var fs = require('fs');
3
+ var path = require('path');
4
+ var cp = require('child_process');
5
+ var access = require('./daemon-project-access');
6
+ var ownership = require('./project-ownership');
7
+ var configModule = require('./config');
8
+ function attachProjectLifecycle(ctx) {
9
+ var pending = new Map();
10
+ var operations = require('./project-lifecycle-operations').attachLifecycleOperations({ config: config, find: find, snapshot: snapshot, save: save, record: record,
11
+ storageDir: configModule.CONFIG_DIR, directory: identity, relay: relay, publish: publish, publishRuntime: publishRuntime });
12
+ function config() { return ctx.getConfig(); }
13
+ function relay() { return ctx.getRelay(); }
14
+ function find(slug) { return (config().projects || []).find(function (p) { return p.slug === slug; }); }
15
+ function ordinary(project) {
16
+ if (!project || ownership.isPersonal(project) || access.parentSlugFor(project.slug)) throw new Error('Choose a registered ordinary parent project. Clay, Mates and worktrees are excluded.');
17
+ if (project.lifecycleState) throw new Error('Project removal is still pending; inspect its receipt before another action.');
18
+ return project;
19
+ }
20
+ function save(next) { ctx.saveConfig(next); ctx.setConfig(next); }
21
+ function publish() {
22
+ try { configModule.syncClayrc(config().projects); } catch (error) {}
23
+ relay().broadcastAll({ type: 'projects_updated', projects: relay().getProjects(), projectCount: config().projects.length });
24
+ }
25
+ function identity(candidate) {
26
+ var canonical = fs.realpathSync(candidate), stat = fs.statSync(canonical);
27
+ if (!stat.isDirectory()) throw new Error('Choose an existing directory.');
28
+ return { path: canonical, dev: stat.dev, ino: stat.ino, uid: stat.uid };
29
+ }
30
+ function existing(candidate, user) {
31
+ var target = identity(candidate), found = (config().projects || []).find(function (project) { return require('./project-directory-claim').canonical(project.path) === target.path; });
32
+ if (!found) return null;
33
+ var users = require('./users');
34
+ if (users.isMultiUser() ? !user || !users.canAccessProject(user.id, access.getProjectAccess(config(), found.slug, config().osUsers)) : !!found.ownerId && found.ownerId !== (user && user.id || null)) throw new Error('Directory access is not permitted.');
35
+ ordinary(found);
36
+ return found;
37
+ }
38
+ function registration(candidate, user) {
39
+ var target = identity(candidate);
40
+ var dataRoot = require('./project-directory-claim').canonical(configModule.CONFIG_DIR);
41
+ var relative = path.relative(dataRoot, target.path), inverse = path.relative(target.path, dataRoot);
42
+ if (relative !== '..' && relative.indexOf('..' + path.sep) !== 0 && !path.isAbsolute(relative) || inverse !== '..' && inverse.indexOf('..' + path.sep) !== 0 && !path.isAbsolute(inverse)) throw new Error('Clay private storage cannot be registered as an ordinary project.');
43
+ (config().projects || []).forEach(function (p) {
44
+ var root = identity(p.path).path;
45
+ function inside(a, b) { var rel = path.relative(a, b); return rel !== '..' && rel.indexOf('..' + path.sep) !== 0 && !path.isAbsolute(rel); }
46
+ if (inside(target.path, root) || inside(root, target.path) && p.ownerId !== (user && user.id)) throw new Error('Directory intersects a registered project.');
47
+ });
48
+ return target;
49
+ }
50
+ function assertSupported(action) {
51
+ if (config().osUsers && ['visibility', 'allowed_users', 'owner'].indexOf(action) >= 0) throw new Error('OS filesystem sharing and ownership changes are unavailable through canonical Clay; the OS sharing/ownership transaction has no completion acknowledgement.');
52
+ }
53
+ function creationPlan(name) {
54
+ var current = config(), base = current.osUsers ? '/var/clay/projects' : current.projectsDir || path.join(configModule.REAL_HOME, 'clay-projects');
55
+ var canonical = require('./project-directory-claim').canonical;
56
+ var slug = configModule.generateSlug(path.join(base, name), current.projects.map(function (p) { return p.slug; }));
57
+ return { base: base, path: path.join(canonical(base), slug), slug: slug };
58
+ }
59
+ function creationRevision(anchor) {
60
+ var plan = creationPlan('inspection');
61
+ var canonical = require('./project-directory-claim').canonical;
62
+ if (!anchor) {
63
+ anchor = plan.base;
64
+ while (!fs.existsSync(anchor)) { var parent = path.dirname(anchor); if (parent === anchor) break; anchor = parent; }
65
+ }
66
+ return JSON.stringify({ base: plan.base, canonicalBase: canonical(plan.base), anchor: anchor, directory: identity(anchor), osUsers: config().osUsers, projects: config().projects });
67
+ }
68
+ function inspect(slug) {
69
+ var project = ordinary(find(slug));
70
+ return { project: JSON.parse(JSON.stringify(project)), directory: identity(project.path) };
71
+ }
72
+ function stamp(slug) { return JSON.stringify(inspect(slug)); }
73
+ function receipt(key, fingerprint, userId) {
74
+ var item = key && (config().projectControlReceipts || {})[key];
75
+ if (!item) return null;
76
+ if (item.fingerprint !== fingerprint || item.ownerId !== (userId || null)) throw new Error('requestId already names another exact project action.');
77
+ var live = item.slug && find(item.slug);
78
+ if (!live && item.result.filesDeleted !== false && item.result.outcome !== 'removal_reserved_runtime_pending') throw new Error('The receipt target registration was deleted. No action was repeated.');
79
+ if (live && (live.projectKnowledgeId !== item.projectKnowledgeId || live.ownerId !== item.ownerId && !(item.action === 'owner' && item.result.status === 'completed'))) throw new Error('The receipt target was replaced or transferred.');
80
+ return item.result;
81
+ }
82
+ function record(next, correlation, result, project, action) {
83
+ if (!correlation) return;
84
+ var receipts = Object.assign({}, next.projectControlReceipts || {});
85
+ if (!receipts[correlation.key] && Object.keys(receipts).length >= 5000) throw new Error('Project control receipt capacity reached.');
86
+ receipts[correlation.key] = { action: action || correlation.action, sourceKey: correlation.sourceKey || null, fingerprint: correlation.fingerprint, ownerId: correlation.ownerId || null, slug: project && project.slug, projectKnowledgeId: project && project.projectKnowledgeId, result: result };
87
+ next.projectControlReceipts = receipts;
88
+ }
89
+ function runtime(slug) { var found; relay().forEachProject(function (project, candidate) { if (candidate === slug) found = project; }); return found; }
90
+ function publishRuntime(project, action) {
91
+ if (config().osUsers && (action === 'register' || action === 'create')) require('./project-linux-access').verify(project.path, ctx.findUser(project.ownerId), ctx.resolveIdentity);
92
+ var current = runtime(project.slug), status = current && current.getStatus();
93
+ if (status && (status.path !== project.path || status.projectKnowledgeId !== project.projectKnowledgeId)) throw new Error('A replacement runtime occupies the saved registration.');
94
+ if (!current && relay().addProject(project.path, project.slug, project.title || null, project.icon || null, project.ownerId || null, null, { projectKnowledgeId: project.projectKnowledgeId }) === false) throw new Error('Saved project runtime publication was not acknowledged.');
95
+ relay().setProjectTitle(project.slug, project.title || null);
96
+ relay().forEachProject(function (target, slug) {
97
+ if (slug === project.slug || access.parentSlugFor(slug) === project.slug) {
98
+ target.setProjectOwner(project.ownerId || null);
99
+ if ((action === 'environment' || action === 'recovered') && target.refreshEnvironmentRuntime) target.refreshEnvironmentRuntime();
100
+ }
101
+ });
102
+ if (action === 'owner' || action === 'visibility' || action === 'allowed_users' || action === 'recovered') relay().refreshProjectAccess();
103
+ if ((action === 'create' || action === 'register') && ctx.registerWorktrees) ctx.registerWorktrees(project, project.ownerId && ctx.findUser(project.ownerId));
104
+ }
105
+ function publication(next, project, action, result, correlation) {
106
+ var op = operations.begin(next, project, action, result, correlation);
107
+ save(next);
108
+ try {
109
+ publishRuntime(project, action);
110
+ if (correlation && correlation.authorize) correlation.authorize();
111
+ operations.exact(op);
112
+ var completed = snapshot(); operations.complete(completed, op, project); save(completed);
113
+ try { publish(); } catch (error) { result.notificationPending = true; }
114
+ return Object.assign({}, result, { operationId: op.id });
115
+ } catch (error) { error.lifecycleResult = Object.assign({}, error.lifecycleResult || {}, operations.partial(op, error.message)); throw error; }
116
+ }
117
+ function receiptPreview(fingerprint, sourceKey, ownerId) {
118
+ return Object.values(config().projectControlReceipts || {}).find(function (item) {
119
+ return item.action === 'owner' && item.result.status === 'completed' && item.fingerprint === fingerprint && item.sourceKey === sourceKey && item.ownerId === ownerId;
120
+ });
121
+ }
122
+ function snapshot() { var lastConfig = config(); return Object.assign({}, lastConfig, JSON.parse(JSON.stringify(lastConfig))); }
123
+ function creatingAccount(user, existingOnly) {
124
+ var users = require('./users');
125
+ if (!users.isMultiUser()) return null;
126
+ var live = user && ctx.findUser(user.id);
127
+ if (!live || !existingOnly && !users.getEffectivePermissions(live, config().osUsers).createProject) throw new Error('Live project creation permission is required.');
128
+ return live;
129
+ }
130
+ function create(name, user, correlation, validate) {
131
+ user = creatingAccount(user);
132
+ if (!/^[a-zA-Z0-9_-]{1,100}$/.test(name) || (/^mate-/i.test(name) || name.indexOf('--') >= 0)) throw new Error('Invalid ordinary project name.');
133
+ if (validate) validate();
134
+ var current = config(), base = current.osUsers ? '/var/clay/projects' : current.projectsDir || path.join(configModule.REAL_HOME, 'clay-projects');
135
+ var creatingIdentity = JSON.stringify([user && user.id, user && user.linuxUser]);
136
+ var osIdentity = null;
137
+ if (current.osUsers) {
138
+ if (!user || !user.linuxUser) throw new Error('A provisioned Linux account is required.');
139
+ osIdentity = ctx.resolveIdentity(user.linuxUser);
140
+ if (!Number.isInteger(osIdentity.uid) || !Number.isInteger(osIdentity.gid)) throw new Error('Linux identity unavailable.');
141
+ }
142
+ var slug = configModule.generateSlug(path.join(base, name), current.projects.map(function (p) { return p.slug; }));
143
+ var claim = require('./project-directory-claim').claim({ baseDir: base, slug: slug, projects: current.projects, userId: user && user.id });
144
+ var persisted = false;
145
+ try {
146
+ claim.assertOwned();
147
+ var options = { cwd: claim.path, stdio: 'pipe' };
148
+ if (osIdentity) {
149
+ fs.chownSync(claim.path, osIdentity.uid, osIdentity.gid);
150
+ options = Object.assign({}, options, { uid: osIdentity.uid, gid: osIdentity.gid, env: { HOME: osIdentity.home, USER: user.linuxUser, PATH: '/usr/local/bin:/usr/bin:/bin' } });
151
+ var wrapped = require('./os-users').wrapSpawnAsUser('git', ['init'], options);
152
+ cp.execFileSync(wrapped.command, wrapped.args, wrapped.options);
153
+ } else cp.execFileSync('git', ['init'], options);
154
+ claim.assertOwned();
155
+ user = creatingAccount(user);
156
+ if (JSON.stringify([user && user.id, user && user.linuxUser]) !== creatingIdentity) throw new Error('Creating OS account identity changed.');
157
+ if (osIdentity && JSON.stringify(ctx.resolveIdentity(user.linuxUser)) !== JSON.stringify(osIdentity)) throw new Error('Resolved OS identity changed during creation.');
158
+ if (osIdentity) { fs.chmodSync(claim.path, 0o700); require('./project-linux-access').verify(claim.path, user, ctx.resolveIdentity); }
159
+ if (validate) validate();
160
+ var next = snapshot(), project = { slug: slug, path: claim.path, ownerId: user && user.id || null, visibility: 'private', addedAt: Date.now(), projectKnowledgeId: require('./project-log-context').createProjectKnowledgeId() };
161
+ next.projects.push(project);
162
+ var result = { ok: true, slug: slug, status: 'completed' };
163
+ return publication(next, project, 'create', result, correlation);
164
+ } catch (error) {
165
+ persisted = !!(error.lifecycleResult && error.lifecycleResult.persisted);
166
+ if (!persisted) { try { if (!claim.cleanup()) error.message += '; claimed directory was replaced; cleanup skipped'; } catch (cleanupError) { error.message += '; directory cleanup failed: ' + cleanupError.message; } }
167
+ else error.message += '; registration persisted; inspect operationId and explicitly reconcile rather than creating again';
168
+ throw error;
169
+ }
170
+ }
171
+ function register(candidate, user, correlation, validate) {
172
+ user = creatingAccount(user, true);
173
+ if (validate) validate();
174
+ var registered = existing(candidate, user);
175
+ if (registered) {
176
+ if (config().osUsers) require('./project-linux-access').verify(registered.path, user, ctx.resolveIdentity);
177
+ if (!runtime(registered.slug)) {
178
+ var pendingOperation = Object.values(config().projectLifecycleOperations || {}).reverse().find(function (item) { return item.slug === registered.slug && item.phase === 'pending'; });
179
+ if (pendingOperation) return operations.partial(pendingOperation);
180
+ throw new Error('The registration exists but its runtime is unavailable.');
181
+ }
182
+ return { ok: true, status: 'completed', slug: registered.slug, existing: true };
183
+ }
184
+ user = creatingAccount(user);
185
+ var target = registration(candidate, user), next = snapshot();
186
+ if (config().osUsers) require('./project-linux-access').verify(candidate, user, ctx.resolveIdentity);
187
+ var slug = configModule.generateSlug(target.path, next.projects.map(function (p) { return p.slug; }));
188
+ if (/^mate-/.test(slug) || access.parentSlugFor(slug)) throw new Error('Reserved private project identity.');
189
+ var removed = (next.removedProjects || []).find(function (p) { return p.path === target.path && (p.userId || null) === (user && user.id || null); });
190
+ var project = { slug: slug, path: target.path, ownerId: user && user.id || null, visibility: 'private', addedAt: Date.now(), projectKnowledgeId: removed && removed.projectKnowledgeId || require('./project-log-context').createProjectKnowledgeId() };
191
+ next.projects.push(project); next.removedProjects = (next.removedProjects || []).filter(function (p) { return p.path !== target.path; });
192
+ if (validate) validate();
193
+ var result = { ok: true, status: 'completed', slug: slug };
194
+ return publication(next, project, 'register', result, correlation);
195
+ }
196
+ function update(slug, action, value, correlation, validate) {
197
+ if (validate) validate();
198
+ ordinary(find(slug));
199
+ var next = snapshot(), project = next.projects.find(function (p) { return p.slug === slug; });
200
+ if (action === 'rename') {
201
+ if (value !== null && (typeof value !== 'string' || value.length > 100 || /[\u0000-\u001f\u007f]/.test(value))) throw new Error('Invalid project title.');
202
+ project.title = value;
203
+ } else if (action === 'environment') {
204
+ if (typeof value !== 'string' || value.length > 100000 || /\u0000/.test(value)) throw new Error('Invalid project environment.');
205
+ var envError = require('./runtime-env').validateEnvString(value);
206
+ if (envError) throw new Error(envError);
207
+ project.envrc = value;
208
+ } else if (action === 'visibility') {
209
+ if (next.osUsers) throw new Error('OS filesystem sharing changes are unavailable through this service.');
210
+ if (value !== 'public' && value !== 'private') throw new Error('Invalid visibility.'); project.visibility = value;
211
+ } else if (action === 'allowed_users') {
212
+ if (next.osUsers) throw new Error('OS filesystem sharing changes are unavailable through this service.');
213
+ if (!Array.isArray(value) || value.length > 100 || value.some(function (id, i) { return !ctx.findUser(id) || value.indexOf(id) !== i; })) throw new Error('Choose existing unique accounts.');
214
+ project.allowedUsers = value;
215
+ } else if (action === 'owner') {
216
+ if (next.osUsers) throw new Error('OS ownership transfers are unavailable through this service.');
217
+ if (typeof value !== 'string' || !ctx.findUser(value)) throw new Error('Choose an existing owner account.');
218
+ project.ownerId = value;
219
+ } else if (action === 'session_visibility') {
220
+ var changed = access.setSessionVisibilityDefault(next, slug, value); if (!changed.ok) throw new Error(changed.error);
221
+ } else throw new Error('Unsupported project mutation.');
222
+ var result = { ok: true, status: 'completed', slug: slug };
223
+ if (action === 'environment') result.runtimeRefreshed = true;
224
+ if (action === 'environment') result.timing = 'Applies to newly created coding-agent processes. Active processes keep their current environment.';
225
+ return publication(next, project, action, result, correlation);
226
+ }
227
+ async function remove(slug, userId, correlation, validate) {
228
+ if (validate) validate();
229
+ var project = ordinary(find(slug)), next = snapshot();
230
+ var savedProject = next.projects.find(function (item) { return item.slug === slug; });
231
+ var result = { ok: true, status: 'completed', slug: slug, filesDeleted: false, semantics: 'Removed the Clay registration and stopped project runtimes. Project files and stored history remain on disk.' };
232
+ var targets = relay().getProjectLifecycleTargets(slug);
233
+ var op = operations.begin(next, savedProject, 'remove', result, correlation, targets); save(next);
234
+ return operations.reconcile(slug, op.id, { userId: userId || null }, correlation && correlation.authorize || function () {}, null);
235
+ }
236
+ function execute(args, user, correlation, validate) {
237
+ var old = correlation && receipt(correlation.key, correlation.fingerprint, correlation.ownerId);
238
+ if (old) return pending.get(correlation.key) || old;
239
+ if (args.action === 'reconcile' || args.action === 'recover_registration') return operations.reconcile(args.projectSlug, args.value, { userId: user && user.id || null }, correlation.authorize, validate, args.action === 'recover_registration');
240
+ if (args.action === 'create') return create(args.name, user, correlation, validate);
241
+ if (args.action === 'register') return register(args.path, user, correlation, validate);
242
+ if (args.action !== 'remove') return update(args.projectSlug, args.action, args.value, correlation, validate);
243
+ var promise = remove(args.projectSlug, user && user.id, correlation, validate).catch(function (error) {
244
+ var current = find(args.projectSlug);
245
+ if (current && current.lifecycleState === 'removing' && !error.lifecycleResult) error.lifecycleResult = { outcome: 'removal_reserved_runtime_pending', persisted: true, retrySafe: false, slug: args.projectSlug, operationId: current.lifecycleOperationId, recoveryAction: 'reconcile' };
246
+ throw error;
247
+ });
248
+ if (correlation) { pending.set(correlation.key, promise); promise.finally(function () { pending.delete(correlation.key); }).catch(function () {}); }
249
+ return promise;
250
+ }
251
+ function resumeRecovered(slug, userId) {
252
+ var project = ordinary(find(slug));
253
+ var pendingOperation = access.pendingLifecycleOperation(config(), project);
254
+ if (pendingOperation) throw new Error('Review and reconcile pending project operation ' + pendingOperation.id + ' before resuming execution.');
255
+ if (!project.lifecycleExecutionPaused) return true;
256
+ if (project.ownerId !== userId || !ctx.findUser(userId)) throw new Error('Only the current owner can explicitly resume recovered project execution.');
257
+ var next = snapshot(); delete next.projects.find(function (item) { return item.slug === slug; }).lifecycleExecutionPaused; save(next); return true;
258
+ }
259
+ function canRestore(project) { return !project.lifecycleState; }
260
+ return { resumeRecovered: resumeRecovered, inspectRecovery: operations.inspect, receiptPreview: receiptPreview, canRestore: canRestore, assertSupported: assertSupported, creationPlan: creationPlan, creationRevision: creationRevision, inspect: inspect, stamp: stamp, registration: registration, receipt: receipt, execute: execute, create: create, register: register, update: update, remove: remove };
261
+ }
262
+ module.exports = { attachProjectLifecycle: attachProjectLifecycle };