youmd 0.10.2 → 0.12.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.
Files changed (205) hide show
  1. package/dist/commands/brain.d.ts.map +1 -1
  2. package/dist/commands/brain.js +91 -28
  3. package/dist/commands/brain.js.map +1 -1
  4. package/dist/commands/chat.d.ts.map +1 -1
  5. package/dist/commands/chat.js +6 -1
  6. package/dist/commands/chat.js.map +1 -1
  7. package/dist/commands/conversations.d.ts +5 -0
  8. package/dist/commands/conversations.d.ts.map +1 -0
  9. package/dist/commands/conversations.js +116 -0
  10. package/dist/commands/conversations.js.map +1 -0
  11. package/dist/commands/doctor.d.ts +1 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +16 -1
  14. package/dist/commands/doctor.js.map +1 -1
  15. package/dist/commands/founder.d.ts +13 -0
  16. package/dist/commands/founder.d.ts.map +1 -0
  17. package/dist/commands/founder.js +189 -0
  18. package/dist/commands/founder.js.map +1 -0
  19. package/dist/commands/handoff.d.ts +5 -0
  20. package/dist/commands/handoff.d.ts.map +1 -0
  21. package/dist/commands/handoff.js +432 -0
  22. package/dist/commands/handoff.js.map +1 -0
  23. package/dist/commands/orchestrate.d.ts +15 -0
  24. package/dist/commands/orchestrate.d.ts.map +1 -1
  25. package/dist/commands/orchestrate.js +294 -0
  26. package/dist/commands/orchestrate.js.map +1 -1
  27. package/dist/commands/project.d.ts.map +1 -1
  28. package/dist/commands/project.js +8 -1
  29. package/dist/commands/project.js.map +1 -1
  30. package/dist/commands/reconcile.d.ts +15 -0
  31. package/dist/commands/reconcile.d.ts.map +1 -0
  32. package/dist/commands/reconcile.js +260 -0
  33. package/dist/commands/reconcile.js.map +1 -0
  34. package/dist/commands/session.d.ts +28 -0
  35. package/dist/commands/session.d.ts.map +1 -0
  36. package/dist/commands/session.js +895 -0
  37. package/dist/commands/session.js.map +1 -0
  38. package/dist/commands/stack.d.ts +2 -0
  39. package/dist/commands/stack.d.ts.map +1 -1
  40. package/dist/commands/stack.js +26 -0
  41. package/dist/commands/stack.js.map +1 -1
  42. package/dist/commands/sync.d.ts.map +1 -1
  43. package/dist/commands/sync.js +11 -0
  44. package/dist/commands/sync.js.map +1 -1
  45. package/dist/commands/tasks.d.ts +57 -0
  46. package/dist/commands/tasks.d.ts.map +1 -1
  47. package/dist/commands/tasks.js +236 -31
  48. package/dist/commands/tasks.js.map +1 -1
  49. package/dist/generated/capability-registry.d.ts +311 -71
  50. package/dist/generated/capability-registry.d.ts.map +1 -1
  51. package/dist/generated/capability-registry.js +346 -71
  52. package/dist/generated/capability-registry.js.map +1 -1
  53. package/dist/index.js +113 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/lib/api.d.ts +362 -1
  56. package/dist/lib/api.d.ts.map +1 -1
  57. package/dist/lib/api.js +171 -0
  58. package/dist/lib/api.js.map +1 -1
  59. package/dist/lib/brain/adapters.d.ts +20 -1
  60. package/dist/lib/brain/adapters.d.ts.map +1 -1
  61. package/dist/lib/brain/adapters.js +113 -2
  62. package/dist/lib/brain/adapters.js.map +1 -1
  63. package/dist/lib/brain/approval-key-trust.d.ts +29 -0
  64. package/dist/lib/brain/approval-key-trust.d.ts.map +1 -0
  65. package/dist/lib/brain/approval-key-trust.js +139 -0
  66. package/dist/lib/brain/approval-key-trust.js.map +1 -0
  67. package/dist/lib/brain/cache.d.ts +2 -2
  68. package/dist/lib/brain/cache.d.ts.map +1 -1
  69. package/dist/lib/brain/cache.js +4 -4
  70. package/dist/lib/brain/cache.js.map +1 -1
  71. package/dist/lib/brain/content.d.ts +56 -0
  72. package/dist/lib/brain/content.d.ts.map +1 -0
  73. package/dist/lib/brain/content.js +362 -0
  74. package/dist/lib/brain/content.js.map +1 -0
  75. package/dist/lib/brain/descriptor-validator.d.ts +11 -0
  76. package/dist/lib/brain/descriptor-validator.d.ts.map +1 -0
  77. package/dist/lib/brain/descriptor-validator.js +185 -0
  78. package/dist/lib/brain/descriptor-validator.js.map +1 -0
  79. package/dist/lib/brain/discovery.d.ts.map +1 -1
  80. package/dist/lib/brain/discovery.js +103 -60
  81. package/dist/lib/brain/discovery.js.map +1 -1
  82. package/dist/lib/brain/index.d.ts +2 -0
  83. package/dist/lib/brain/index.d.ts.map +1 -1
  84. package/dist/lib/brain/index.js +8 -2
  85. package/dist/lib/brain/index.js.map +1 -1
  86. package/dist/lib/brain/proposal.d.ts +53 -5
  87. package/dist/lib/brain/proposal.d.ts.map +1 -1
  88. package/dist/lib/brain/proposal.js +341 -64
  89. package/dist/lib/brain/proposal.js.map +1 -1
  90. package/dist/lib/brain/safe-file.d.ts +25 -1
  91. package/dist/lib/brain/safe-file.d.ts.map +1 -1
  92. package/dist/lib/brain/safe-file.js +250 -30
  93. package/dist/lib/brain/safe-file.js.map +1 -1
  94. package/dist/lib/brain/safe-open.d.ts +2 -0
  95. package/dist/lib/brain/safe-open.d.ts.map +1 -0
  96. package/dist/lib/brain/safe-open.js +77 -0
  97. package/dist/lib/brain/safe-open.js.map +1 -0
  98. package/dist/lib/brain/safety.d.ts.map +1 -1
  99. package/dist/lib/brain/safety.js +5 -0
  100. package/dist/lib/brain/safety.js.map +1 -1
  101. package/dist/lib/brain/staged-apply.d.ts +2 -1
  102. package/dist/lib/brain/staged-apply.d.ts.map +1 -1
  103. package/dist/lib/brain/staged-apply.js +6 -3
  104. package/dist/lib/brain/staged-apply.js.map +1 -1
  105. package/dist/lib/brain/store.d.ts +3 -0
  106. package/dist/lib/brain/store.d.ts.map +1 -1
  107. package/dist/lib/brain/store.js +68 -0
  108. package/dist/lib/brain/store.js.map +1 -1
  109. package/dist/lib/brain/sync.d.ts +87 -0
  110. package/dist/lib/brain/sync.d.ts.map +1 -0
  111. package/dist/lib/brain/sync.js +238 -0
  112. package/dist/lib/brain/sync.js.map +1 -0
  113. package/dist/lib/brain/types.d.ts +23 -0
  114. package/dist/lib/brain/types.d.ts.map +1 -1
  115. package/dist/lib/config.d.ts +7 -0
  116. package/dist/lib/config.d.ts.map +1 -1
  117. package/dist/lib/config.js +3 -0
  118. package/dist/lib/config.js.map +1 -1
  119. package/dist/lib/daemon.js +1 -1
  120. package/dist/lib/daemon.js.map +1 -1
  121. package/dist/lib/goalRegistryFile.d.ts +169 -0
  122. package/dist/lib/goalRegistryFile.d.ts.map +1 -0
  123. package/dist/lib/goalRegistryFile.js +518 -0
  124. package/dist/lib/goalRegistryFile.js.map +1 -0
  125. package/dist/lib/goalsFile.d.ts +101 -0
  126. package/dist/lib/goalsFile.d.ts.map +1 -0
  127. package/dist/lib/goalsFile.js +413 -0
  128. package/dist/lib/goalsFile.js.map +1 -0
  129. package/dist/lib/handoff.d.ts +170 -0
  130. package/dist/lib/handoff.d.ts.map +1 -0
  131. package/dist/lib/handoff.js +316 -0
  132. package/dist/lib/handoff.js.map +1 -0
  133. package/dist/lib/machine-identity.d.ts +2 -0
  134. package/dist/lib/machine-identity.d.ts.map +1 -1
  135. package/dist/lib/machine-identity.js +13 -6
  136. package/dist/lib/machine-identity.js.map +1 -1
  137. package/dist/lib/onboarding.d.ts +4 -1
  138. package/dist/lib/onboarding.d.ts.map +1 -1
  139. package/dist/lib/onboarding.js +11 -3
  140. package/dist/lib/onboarding.js.map +1 -1
  141. package/dist/lib/orchestrator/loop.d.ts +7 -0
  142. package/dist/lib/orchestrator/loop.d.ts.map +1 -1
  143. package/dist/lib/orchestrator/loop.js +53 -0
  144. package/dist/lib/orchestrator/loop.js.map +1 -1
  145. package/dist/lib/orchestrator/run-journal.d.ts +178 -0
  146. package/dist/lib/orchestrator/run-journal.d.ts.map +1 -0
  147. package/dist/lib/orchestrator/run-journal.js +912 -0
  148. package/dist/lib/orchestrator/run-journal.js.map +1 -0
  149. package/dist/lib/orchestrator/supervisor.d.ts +11 -0
  150. package/dist/lib/orchestrator/supervisor.d.ts.map +1 -1
  151. package/dist/lib/orchestrator/supervisor.js +304 -116
  152. package/dist/lib/orchestrator/supervisor.js.map +1 -1
  153. package/dist/lib/orchestrator/tools.d.ts +2 -0
  154. package/dist/lib/orchestrator/tools.d.ts.map +1 -1
  155. package/dist/lib/orchestrator/tools.js +214 -1
  156. package/dist/lib/orchestrator/tools.js.map +1 -1
  157. package/dist/lib/redact.d.ts.map +1 -1
  158. package/dist/lib/redact.js +31 -6
  159. package/dist/lib/redact.js.map +1 -1
  160. package/dist/lib/remote-agent-event.d.ts +19 -0
  161. package/dist/lib/remote-agent-event.d.ts.map +1 -0
  162. package/dist/lib/remote-agent-event.js +85 -0
  163. package/dist/lib/remote-agent-event.js.map +1 -0
  164. package/dist/lib/remote-command.d.ts.map +1 -1
  165. package/dist/lib/remote-command.js +19 -0
  166. package/dist/lib/remote-command.js.map +1 -1
  167. package/dist/lib/remote-executor.d.ts +7 -1
  168. package/dist/lib/remote-executor.d.ts.map +1 -1
  169. package/dist/lib/remote-executor.js +391 -0
  170. package/dist/lib/remote-executor.js.map +1 -1
  171. package/dist/lib/sessionCheckpoint.d.ts +104 -0
  172. package/dist/lib/sessionCheckpoint.d.ts.map +1 -0
  173. package/dist/lib/sessionCheckpoint.js +191 -0
  174. package/dist/lib/sessionCheckpoint.js.map +1 -0
  175. package/dist/lib/sessionHandoff.d.ts +47 -0
  176. package/dist/lib/sessionHandoff.d.ts.map +1 -0
  177. package/dist/lib/sessionHandoff.js +305 -0
  178. package/dist/lib/sessionHandoff.js.map +1 -0
  179. package/dist/lib/stream.d.ts +3 -1
  180. package/dist/lib/stream.d.ts.map +1 -1
  181. package/dist/lib/stream.js +8 -3
  182. package/dist/lib/stream.js.map +1 -1
  183. package/dist/lib/taskLedgerProposal.d.ts +102 -0
  184. package/dist/lib/taskLedgerProposal.d.ts.map +1 -0
  185. package/dist/lib/taskLedgerProposal.js +408 -0
  186. package/dist/lib/taskLedgerProposal.js.map +1 -0
  187. package/dist/lib/tasksFile.d.ts +23 -2
  188. package/dist/lib/tasksFile.d.ts.map +1 -1
  189. package/dist/lib/tasksFile.js +122 -10
  190. package/dist/lib/tasksFile.js.map +1 -1
  191. package/dist/mcp/registry.d.ts.map +1 -1
  192. package/dist/mcp/registry.js +110 -0
  193. package/dist/mcp/registry.js.map +1 -1
  194. package/native/prebuilds/darwin-universal/youmd_safe_open.node +0 -0
  195. package/native/safe-open/youmd_safe_open.c +224 -0
  196. package/package.json +11 -3
  197. package/scripts/build-safe-open.mjs +63 -0
  198. package/scripts/skillstack-sync/com.you.context-sync.plist +1 -1
  199. package/scripts/skillstack-sync/com.you.identity-sync.plist +1 -1
  200. package/scripts/skillstack-sync/com.you.orchestrator-watch.plist +1 -1
  201. package/scripts/skillstack-sync/com.you.realtime-sync.plist +7 -1
  202. package/scripts/skillstack-sync/com.you.skillstack-sync.plist +1 -1
  203. package/scripts/skillstack-sync/context-sync.sh +19 -7
  204. package/scripts/skillstack-sync/install-daemons-linux.sh +6 -0
  205. package/scripts/smoke-safe-open-node.cjs +50 -0
@@ -0,0 +1,224 @@
1
+ #define NAPI_VERSION 8
2
+ #include <node_api.h>
3
+
4
+ #include <errno.h>
5
+ #include <fcntl.h>
6
+ #include <limits.h>
7
+ #include <stdint.h>
8
+ #include <stdlib.h>
9
+ #include <string.h>
10
+ #include <sys/stat.h>
11
+ #include <unistd.h>
12
+
13
+ #ifndef O_CLOEXEC
14
+ #define O_CLOEXEC 0
15
+ #endif
16
+
17
+ static napi_value fail(napi_env env, const char *reason) {
18
+ napi_throw_error(env, reason, reason);
19
+ return NULL;
20
+ }
21
+
22
+ static const char *errno_reason(int value) {
23
+ if (value == ELOOP) return "symlink_rejected";
24
+ if (value == ENOTDIR || value == EINVAL || value == ENAMETOOLONG) return "unsafe_path";
25
+ if (value == ENOENT || value == ESTALE) return "stale_claim";
26
+ return "unreadable";
27
+ }
28
+
29
+ static char *get_string(napi_env env, napi_value value, size_t maximum) {
30
+ size_t length = 0;
31
+ if (napi_get_value_string_utf8(env, value, NULL, 0, &length) != napi_ok || length > maximum) {
32
+ fail(env, "unsafe_path");
33
+ return NULL;
34
+ }
35
+ char *result = malloc(length + 1);
36
+ if (result == NULL) {
37
+ fail(env, "unreadable");
38
+ return NULL;
39
+ }
40
+ if (napi_get_value_string_utf8(env, value, result, length + 1, &length) != napi_ok) {
41
+ free(result);
42
+ fail(env, "unsafe_path");
43
+ return NULL;
44
+ }
45
+ if (memchr(result, '\0', length) != NULL) {
46
+ free(result);
47
+ fail(env, "unsafe_path");
48
+ return NULL;
49
+ }
50
+ result[length] = '\0';
51
+ return result;
52
+ }
53
+
54
+ static int get_u64(napi_env env, napi_value value, uint64_t *result) {
55
+ char *text = get_string(env, value, 32);
56
+ if (text == NULL) return 0;
57
+ char *end = NULL;
58
+ errno = 0;
59
+ unsigned long long parsed = strtoull(text, &end, 10);
60
+ int valid = errno == 0 && end != text && *end == '\0';
61
+ free(text);
62
+ if (!valid) {
63
+ fail(env, "unsafe_path");
64
+ return 0;
65
+ }
66
+ *result = (uint64_t)parsed;
67
+ return 1;
68
+ }
69
+
70
+ static int invoke_hook(napi_env env, napi_value hook, const char *stage, uint32_t index, const char *segment) {
71
+ napi_valuetype type;
72
+ if (hook == NULL || napi_typeof(env, hook, &type) != napi_ok || type != napi_function) return 1;
73
+ napi_value global, args[3], ignored;
74
+ if (napi_get_global(env, &global) != napi_ok ||
75
+ napi_create_string_utf8(env, stage, NAPI_AUTO_LENGTH, &args[0]) != napi_ok ||
76
+ napi_create_uint32(env, index, &args[1]) != napi_ok ||
77
+ napi_create_string_utf8(env, segment == NULL ? "" : segment, NAPI_AUTO_LENGTH, &args[2]) != napi_ok) {
78
+ fail(env, "unreadable");
79
+ return 0;
80
+ }
81
+ return napi_call_function(env, global, hook, 3, args, &ignored) == napi_ok;
82
+ }
83
+
84
+ static int unchanged(const struct stat *before, const struct stat *after) {
85
+ return before->st_dev == after->st_dev && before->st_ino == after->st_ino && before->st_size == after->st_size &&
86
+ #if defined(__APPLE__)
87
+ before->st_mtimespec.tv_sec == after->st_mtimespec.tv_sec && before->st_mtimespec.tv_nsec == after->st_mtimespec.tv_nsec &&
88
+ before->st_ctimespec.tv_sec == after->st_ctimespec.tv_sec && before->st_ctimespec.tv_nsec == after->st_ctimespec.tv_nsec;
89
+ #else
90
+ before->st_mtim.tv_sec == after->st_mtim.tv_sec && before->st_mtim.tv_nsec == after->st_mtim.tv_nsec &&
91
+ before->st_ctim.tv_sec == after->st_ctim.tv_sec && before->st_ctim.tv_nsec == after->st_ctim.tv_nsec;
92
+ #endif
93
+ }
94
+
95
+ static napi_value safe_read(napi_env env, napi_callback_info info) {
96
+ size_t argc = 6;
97
+ napi_value argv[6];
98
+ if (napi_get_cb_info(env, info, &argc, argv, NULL, NULL) != napi_ok || argc < 5) return fail(env, "unsafe_path");
99
+
100
+ char *root = get_string(env, argv[0], PATH_MAX - 1);
101
+ if (root == NULL) return NULL;
102
+ char *relative = get_string(env, argv[1], PATH_MAX - 1);
103
+ if (relative == NULL) { free(root); return NULL; }
104
+ uint32_t maximum = 0;
105
+ uint64_t expected_dev = 0, expected_ino = 0;
106
+ if (napi_get_value_uint32(env, argv[2], &maximum) != napi_ok || maximum == 0 ||
107
+ !get_u64(env, argv[3], &expected_dev) || !get_u64(env, argv[4], &expected_ino)) {
108
+ free(root); free(relative); return NULL;
109
+ }
110
+ napi_value hook = argc >= 6 ? argv[5] : NULL;
111
+ size_t root_length = strlen(root);
112
+ if (root[0] != '/' || (root_length > 1 && root[root_length - 1] == '/') ||
113
+ relative[0] == '\0' || relative[0] == '/' || relative[strlen(relative) - 1] == '/') {
114
+ free(root); free(relative); return fail(env, "unsafe_path");
115
+ }
116
+
117
+ /* Anchor at /, then traverse every root component with openat. Opening the
118
+ absolute root pathname directly would still follow symlinks in parents. */
119
+ int directory_fd = open("/", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
120
+ if (directory_fd < 0) {
121
+ const char *reason = errno_reason(errno); free(root); free(relative); return fail(env, reason);
122
+ }
123
+ char *root_cursor = root + 1;
124
+ uint32_t root_index = 0;
125
+ while (*root_cursor != '\0') {
126
+ char *slash = strchr(root_cursor, '/');
127
+ size_t length = slash == NULL ? strlen(root_cursor) : (size_t)(slash - root_cursor);
128
+ if (length == 0 || length > NAME_MAX || (length == 1 && root_cursor[0] == '.') ||
129
+ (length == 2 && root_cursor[0] == '.' && root_cursor[1] == '.')) {
130
+ close(directory_fd); free(root); free(relative); return fail(env, "unsafe_path");
131
+ }
132
+ char segment[NAME_MAX + 1];
133
+ memcpy(segment, root_cursor, length); segment[length] = '\0';
134
+ if (!invoke_hook(env, hook, "before_root_directory_open", root_index, segment)) {
135
+ close(directory_fd); free(root); free(relative); return NULL;
136
+ }
137
+ int next_fd = openat(directory_fd, segment, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
138
+ if (next_fd < 0) {
139
+ const char *reason = errno_reason(errno); close(directory_fd); free(root); free(relative); return fail(env, reason);
140
+ }
141
+ close(directory_fd); directory_fd = next_fd;
142
+ if (!invoke_hook(env, hook, "after_root_directory_open", root_index, segment)) {
143
+ close(directory_fd); free(root); free(relative); return NULL;
144
+ }
145
+ if (slash == NULL) break;
146
+ root_cursor = slash + 1;
147
+ root_index += 1;
148
+ }
149
+ free(root);
150
+ struct stat root_stat;
151
+ if (fstat(directory_fd, &root_stat) != 0 || !S_ISDIR(root_stat.st_mode)) {
152
+ close(directory_fd); free(relative); return fail(env, "unsafe_path");
153
+ }
154
+ if ((uint64_t)root_stat.st_dev != expected_dev || (uint64_t)root_stat.st_ino != expected_ino) {
155
+ close(directory_fd); free(relative); return fail(env, "stale_claim");
156
+ }
157
+ if (!invoke_hook(env, hook, "after_root_open", 0, "")) { close(directory_fd); free(relative); return NULL; }
158
+
159
+ char *cursor = relative;
160
+ uint32_t index = 0;
161
+ int file_fd = -1;
162
+ while (*cursor != '\0') {
163
+ char *slash = strchr(cursor, '/');
164
+ size_t length = slash == NULL ? strlen(cursor) : (size_t)(slash - cursor);
165
+ if (length == 0 || length > NAME_MAX || (length == 1 && cursor[0] == '.') ||
166
+ (length == 2 && cursor[0] == '.' && cursor[1] == '.')) {
167
+ close(directory_fd); free(relative); return fail(env, "unsafe_path");
168
+ }
169
+ char segment[NAME_MAX + 1];
170
+ memcpy(segment, cursor, length); segment[length] = '\0';
171
+ int final = slash == NULL;
172
+ if (!invoke_hook(env, hook, final ? "before_file_open" : "before_directory_open", index, segment)) {
173
+ close(directory_fd); free(relative); return NULL;
174
+ }
175
+ int next_fd = openat(directory_fd, segment, O_RDONLY | O_NOFOLLOW | O_CLOEXEC |
176
+ (final ? O_NONBLOCK : O_DIRECTORY));
177
+ if (next_fd < 0) {
178
+ const char *reason = errno_reason(errno); close(directory_fd); free(relative); return fail(env, reason);
179
+ }
180
+ if (final) { file_fd = next_fd; break; }
181
+ close(directory_fd); directory_fd = next_fd;
182
+ if (!invoke_hook(env, hook, "after_directory_open", index, segment)) { close(directory_fd); free(relative); return NULL; }
183
+ cursor = slash + 1; index += 1;
184
+ }
185
+ free(relative); close(directory_fd);
186
+ if (file_fd < 0) return fail(env, "unsafe_path");
187
+
188
+ struct stat before;
189
+ if (fstat(file_fd, &before) != 0 || !S_ISREG(before.st_mode)) { close(file_fd); return fail(env, "unsafe_path"); }
190
+ if (before.st_size < 0 || (uint64_t)before.st_size > maximum) { close(file_fd); return fail(env, "budget_exceeded"); }
191
+ size_t size = (size_t)before.st_size;
192
+ unsigned char *bytes = malloc(size == 0 ? 1 : size);
193
+ if (bytes == NULL) { close(file_fd); return fail(env, "unreadable"); }
194
+ size_t offset = 0;
195
+ while (offset < size) {
196
+ ssize_t count = pread(file_fd, bytes + offset, size - offset, (off_t)offset);
197
+ if (count < 0 && errno == EINTR) continue;
198
+ if (count <= 0) { free(bytes); close(file_fd); return fail(env, "unreadable"); }
199
+ offset += (size_t)count;
200
+ }
201
+ unsigned char extra;
202
+ ssize_t extra_count;
203
+ do { extra_count = pread(file_fd, &extra, 1, (off_t)size); } while (extra_count < 0 && errno == EINTR);
204
+ struct stat after;
205
+ int stable = fstat(file_fd, &after) == 0 && extra_count == 0 && unchanged(&before, &after);
206
+ close(file_fd);
207
+ if (!stable) { free(bytes); return fail(env, "stale_claim"); }
208
+
209
+ napi_value result;
210
+ napi_status status = napi_create_buffer_copy(env, size, bytes, NULL, &result);
211
+ free(bytes);
212
+ return status == napi_ok ? result : fail(env, "unreadable");
213
+ }
214
+
215
+ static napi_value init(napi_env env, napi_value exports) {
216
+ napi_value function, version;
217
+ if (napi_create_function(env, "safeRead", NAPI_AUTO_LENGTH, safe_read, NULL, &function) != napi_ok ||
218
+ napi_set_named_property(env, exports, "safeRead", function) != napi_ok ||
219
+ napi_create_uint32(env, NAPI_VERSION, &version) != napi_ok ||
220
+ napi_set_named_property(env, exports, "napiVersion", version) != napi_ok) return NULL;
221
+ return exports;
222
+ }
223
+
224
+ NAPI_MODULE(youmd_safe_open, init)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "youmd",
3
- "version": "0.10.2",
3
+ "version": "0.12.0",
4
4
  "description": "Identity context protocol for the agent internet — an MCP where the context is you. CLI for the You.md platform.",
5
5
  "bin": {
6
6
  "you": "dist/you.js",
@@ -10,17 +10,25 @@
10
10
  "main": "dist/index.js",
11
11
  "files": [
12
12
  "dist/**/*",
13
+ "native/**/*",
13
14
  "skills/**/*",
14
15
  "examples/**/*",
15
16
  "scripts/**/*"
16
17
  ],
17
18
  "scripts": {
18
19
  "build": "tsc",
20
+ "build:native": "node scripts/build-safe-open.mjs",
19
21
  "dev": "ts-node src/index.ts",
20
22
  "start": "node dist/index.js",
23
+ "pretest": "npm run build:native",
21
24
  "test": "vitest run",
25
+ "pretest:brain-content": "npm run build:native",
26
+ "test:brain-content": "vitest run src/__tests__/brain-content.test.ts src/__tests__/brain-safe-open-native.test.ts",
27
+ "pretest:brain-content:abi": "npm run build:native",
28
+ "test:brain-content:abi": "node scripts/smoke-safe-open-node.cjs",
22
29
  "test:watch": "vitest",
23
- "prepublishOnly": "node scripts/check-version-exists.mjs && npm run build",
30
+ "prepack": "npm run build:native && npm run build",
31
+ "prepublishOnly": "node scripts/check-version-exists.mjs && npm run build:native && npm run build",
24
32
  "postinstall": "node -e \"try{require('./dist/postinstall.js')}catch(e){if(e&&e.code!=='MODULE_NOT_FOUND')throw e}\""
25
33
  },
26
34
  "keywords": [
@@ -45,7 +53,7 @@
45
53
  "@modelcontextprotocol/sdk": "^1.29.0",
46
54
  "chalk": "^4.1.2",
47
55
  "commander": "^12.1.0",
48
- "convex": "1.33.1",
56
+ "convex": "1.43.0",
49
57
  "gray-matter": "^4.0.3",
50
58
  "jimp": "^0.22.12",
51
59
  "js-yaml": "^4.1.1",
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const cliRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const outputDirectory = path.join(cliRoot, "native", "prebuilds", "darwin-universal");
11
+ const output = path.join(outputDirectory, "youmd_safe_open.node");
12
+ const source = path.join(cliRoot, "native", "safe-open", "youmd_safe_open.c");
13
+
14
+ if (process.platform !== "darwin") {
15
+ console.error("safe-open: non-darwin build skipped (Linux uses procfd; other runtimes degrade to metadata-only)");
16
+ process.exit(0);
17
+ }
18
+
19
+ const includeCandidates = [
20
+ process.env.NODE_INCLUDE_DIR,
21
+ process.env.npm_config_nodedir && path.join(process.env.npm_config_nodedir, "include", "node"),
22
+ path.resolve(path.dirname(process.execPath), "..", "include", "node"),
23
+ "/opt/homebrew/include/node",
24
+ "/usr/local/include/node",
25
+ ].filter(Boolean);
26
+ const includeDirectory = includeCandidates.find((candidate) => fs.existsSync(path.join(candidate, "node_api.h")));
27
+ if (!includeDirectory) throw new Error("safe-open: node_api.h not found; set NODE_INCLUDE_DIR");
28
+
29
+ const xcrun = (args) => execFileSync("xcrun", args, { encoding: "utf8" }).trim();
30
+ const clang = xcrun(["--find", "clang"]);
31
+ const sdk = xcrun(["--show-sdk-path"]);
32
+ fs.mkdirSync(outputDirectory, { recursive: true });
33
+ const temporary = `${output}.${process.pid}.tmp`;
34
+ try {
35
+ execFileSync(clang, [
36
+ "-std=c11",
37
+ "-O2",
38
+ "-Wall",
39
+ "-Wextra",
40
+ "-Werror",
41
+ "-fvisibility=hidden",
42
+ "-bundle",
43
+ "-undefined", "dynamic_lookup",
44
+ "-mmacosx-version-min=10.15",
45
+ "-isysroot", sdk,
46
+ "-I", includeDirectory,
47
+ "-arch", "arm64",
48
+ "-arch", "x86_64",
49
+ source,
50
+ "-o", temporary,
51
+ ], { stdio: "inherit" });
52
+ execFileSync("codesign", ["--force", "--sign", "-", temporary], { stdio: "inherit" });
53
+ fs.renameSync(temporary, output);
54
+ } finally {
55
+ if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
56
+ }
57
+
58
+ const lipo = execFileSync("lipo", ["-info", output], { encoding: "utf8" }).trim();
59
+ if (!lipo.includes("x86_64") || !lipo.includes("arm64")) throw new Error(`safe-open: expected universal binary; ${lipo}`);
60
+ const addon = createRequire(import.meta.url)(output);
61
+ if (addon.napiVersion !== 8 || typeof addon.safeRead !== "function") throw new Error("safe-open: invalid Node-API v8 exports");
62
+ // npm pack --json reserves stdout for its JSON document.
63
+ console.error(`safe-open: built ${path.relative(cliRoot, output)} (${lipo})`);
@@ -32,7 +32,7 @@
32
32
  <key>HOME</key>
33
33
  <string>__HOME__</string>
34
34
  <key>PATH</key>
35
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
35
+ <string>__HOME__/.you/bin:__HOME__/.youmd/bin:__HOME__/.youmd/npm-global/bin:__HOME__/.npm-global/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
36
36
  </dict>
37
37
  </dict>
38
38
  </plist>
@@ -32,7 +32,7 @@
32
32
  <key>HOME</key>
33
33
  <string>__HOME__</string>
34
34
  <key>PATH</key>
35
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
35
+ <string>__HOME__/.you/bin:__HOME__/.youmd/bin:__HOME__/.youmd/npm-global/bin:__HOME__/.npm-global/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
36
36
  </dict>
37
37
  </dict>
38
38
  </plist>
@@ -32,7 +32,7 @@
32
32
  <key>HOME</key>
33
33
  <string>__HOME__</string>
34
34
  <key>PATH</key>
35
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
35
+ <string>__HOME__/.you/bin:__HOME__/.youmd/bin:__HOME__/.youmd/npm-global/bin:__HOME__/.npm-global/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
36
36
  </dict>
37
37
  </dict>
38
38
  </plist>
@@ -29,7 +29,13 @@
29
29
  <key>HOME</key>
30
30
  <string>__HOME__</string>
31
31
  <key>PATH</key>
32
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
32
+ <string>__HOME__/.you/bin:__HOME__/.youmd/bin:__HOME__/.youmd/npm-global/bin:__HOME__/.npm-global/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
33
+ <!-- Dedicated launchd jobs own these cadences; the realtime worker
34
+ must not spawn duplicate project/stack sync processes. -->
35
+ <key>YOUMD_LIVE_SYNC_STACK</key>
36
+ <string>0</string>
37
+ <key>YOUMD_LIVE_SYNC_CONTEXT</key>
38
+ <string>0</string>
33
39
  </dict>
34
40
  </dict>
35
41
  </plist>
@@ -32,7 +32,7 @@
32
32
  <key>HOME</key>
33
33
  <string>__HOME__</string>
34
34
  <key>PATH</key>
35
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
35
+ <string>__HOME__/.you/bin:__HOME__/.youmd/bin:__HOME__/.youmd/npm-global/bin:__HOME__/.npm-global/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
36
36
  </dict>
37
37
  </dict>
38
38
  </plist>
@@ -33,8 +33,10 @@
33
33
  # ./context-sync.sh --dry-run # log intended actions only, write nothing
34
34
  #
35
35
  # Env overrides:
36
- # CONTEXT_REPOS space-separated list of repo DIRECTORY NAMES under
37
- # ~/Desktop/CODE_2025/ (not full paths)
36
+ # CONTEXT_ROOTS space-separated absolute roots to scan. Defaults to the
37
+ # standard CODE_YOU, CODE_2025, and CODE_2026 machine roots.
38
+ # CONTEXT_REPOS space-separated list of repo DIRECTORY NAMES under each
39
+ # context root (not full paths)
38
40
  # GIT_USER_NAME commit author name (default: Houston Golden)
39
41
  # GIT_USER_EMAIL commit author email (default: houston@bamf.ai)
40
42
 
@@ -50,7 +52,9 @@ LOG_FILE="${LOG_DIR}/context-sync.log"
50
52
  GIT_USER_NAME="${GIT_USER_NAME:-Houston Golden}"
51
53
  GIT_USER_EMAIL="${GIT_USER_EMAIL:-houston@bamf.ai}"
52
54
 
53
- CODE_ROOT="${HOME}/Desktop/CODE_2025"
55
+ DEFAULT_CONTEXT_ROOTS="${HOME}/Desktop/CODE_YOU ${HOME}/Desktop/CODE_2025 ${HOME}/Desktop/CODE_2026"
56
+ CONTEXT_ROOTS="${CONTEXT_ROOTS:-${DEFAULT_CONTEXT_ROOTS}}"
57
+ CODE_ROOT=""
54
58
 
55
59
  # Curated list of Houston's OWN repos with rich agent context.
56
60
  # These all have houstongolden/* or hubify-projects/* remotes — confirmed safe
@@ -60,7 +64,7 @@ CODE_ROOT="${HOME}/Desktop/CODE_2025"
60
64
  # - disler/*, coffeefuelbump/*, etc. → not Houston's remote
61
65
  # - repos with no origin → detected and skipped at runtime
62
66
  # - gstack, agent-shared, scistack → handled by sync.sh
63
- DEFAULT_CONTEXT_REPOS="bamfaiapp bigbounce hubify-aios myo hubifycode badapp claws"
67
+ DEFAULT_CONTEXT_REPOS="youmd bamfaiapp bigbounce hubify-aios myo hubifycode badapp claws"
64
68
  CONTEXT_REPOS="${CONTEXT_REPOS:-${DEFAULT_CONTEXT_REPOS}}"
65
69
 
66
70
  # The ONLY paths we will ever stage. Nothing else is touched.
@@ -395,13 +399,21 @@ main() {
395
399
 
396
400
  log "============================================================"
397
401
  log "context-sync START [${mode}] on $(hostname_short)"
402
+ log "Roots: ${CONTEXT_ROOTS}"
398
403
  log "Repos: ${CONTEXT_REPOS}"
399
404
  log "Context paths: ${CONTEXT_PATHSPECS}"
400
405
  log "============================================================"
401
406
 
402
- local repo_name
403
- for repo_name in ${CONTEXT_REPOS}; do
404
- sync_context_repo "${repo_name}" || true
407
+ local code_root repo_name
408
+ for code_root in ${CONTEXT_ROOTS}; do
409
+ if [ ! -d "${code_root}" ]; then
410
+ log_warn "Skipping context root: directory not found at ${code_root}."
411
+ continue
412
+ fi
413
+ CODE_ROOT="${code_root}"
414
+ for repo_name in ${CONTEXT_REPOS}; do
415
+ sync_context_repo "${repo_name}" || true
416
+ done
405
417
  done
406
418
 
407
419
  log "============================================================"
@@ -79,6 +79,12 @@ write_service() {
79
79
  printf 'Environment=HOME=%s\n' "${HOME}"
80
80
  printf 'Environment=YOU_HOME=%s\n' "${YOU_HOME}"
81
81
  printf 'Environment=PATH=%s\n' "${UNIT_PATH}"
82
+ if [ "${unit}" = "you-realtime-sync.service" ]; then
83
+ # The interval units below own stack/context sync. Keep the realtime
84
+ # brain focused on live state, inventory, repair, and remote commands.
85
+ printf 'Environment=YOUMD_LIVE_SYNC_STACK=0\n'
86
+ printf 'Environment=YOUMD_LIVE_SYNC_CONTEXT=0\n'
87
+ fi
82
88
  # Run via a login shell so PATH is sourced from the user's profile — the CLI shells out to
83
89
  # node/git and the claude/codex/cursor harnesses by name, and those may live under nvm/fnm
84
90
  # prefixes not in the static Environment=PATH above (mirrors the macOS `/bin/zsh -lc` plists).
@@ -0,0 +1,50 @@
1
+ /* eslint-disable @typescript-eslint/no-require-imports */
2
+ const assert = require("node:assert/strict");
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ const addon = require(path.resolve(
8
+ __dirname,
9
+ "../native/prebuilds/darwin-universal/youmd_safe_open.node",
10
+ ));
11
+
12
+ assert.equal(addon.napiVersion, 8, "expected the ABI-stable Node-API v8 addon");
13
+ assert.equal(typeof addon.safeRead, "function", "safeRead export is missing");
14
+
15
+ const root = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), "youmd-node-abi-")));
16
+ const outside = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), "youmd-node-abi-outside-")));
17
+
18
+ try {
19
+ fs.mkdirSync(path.join(root, "project-context"));
20
+ fs.writeFileSync(path.join(root, "project-context", "PRD.md"), "verified\n");
21
+ fs.writeFileSync(path.join(outside, "PRD.md"), "outside\n");
22
+
23
+ const stat = fs.lstatSync(root, { bigint: true });
24
+ const bytes = addon.safeRead(
25
+ root,
26
+ "project-context/PRD.md",
27
+ 1024,
28
+ stat.dev.toString(),
29
+ stat.ino.toString(),
30
+ );
31
+ assert.equal(bytes.toString("utf8"), "verified\n");
32
+
33
+ fs.rmSync(path.join(root, "project-context"), { recursive: true });
34
+ fs.symlinkSync(outside, path.join(root, "project-context"), "dir");
35
+ assert.throws(
36
+ () => addon.safeRead(
37
+ root,
38
+ "project-context/PRD.md",
39
+ 1024,
40
+ stat.dev.toString(),
41
+ stat.ino.toString(),
42
+ ),
43
+ /symlink_rejected|unsafe_path/,
44
+ );
45
+
46
+ console.log(`safe-open ABI smoke passed on Node ${process.version}`);
47
+ } finally {
48
+ fs.rmSync(root, { recursive: true, force: true });
49
+ fs.rmSync(outside, { recursive: true, force: true });
50
+ }