slashvibe-mcp 0.2.2 → 0.2.3

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -40
  3. package/config.js +171 -3
  4. package/index.js +136 -16
  5. package/intelligence/index.js +38 -0
  6. package/intelligence/infer.js +316 -0
  7. package/intelligence/patterns.js +651 -0
  8. package/intelligence/proactive.js +358 -0
  9. package/intelligence/serendipity.js +306 -0
  10. package/notify.js +141 -18
  11. package/package.json +8 -4
  12. package/presence.js +5 -1
  13. package/protocol/index.js +88 -1
  14. package/protocol/telegram-commands.js +199 -0
  15. package/store/api.js +360 -25
  16. package/store/index.js +7 -7
  17. package/store/local.js +67 -11
  18. package/store/profiles.js +287 -0
  19. package/store/reservations.js +321 -0
  20. package/store/skills.js +378 -0
  21. package/tools/_actions.js +270 -14
  22. package/tools/_connection-queue.js +257 -0
  23. package/tools/_discovery-enhanced.js +290 -0
  24. package/tools/_discovery.js +346 -0
  25. package/tools/_proactive-discovery.js +301 -0
  26. package/tools/admin-inbox.js +218 -0
  27. package/tools/agent-treasury.js +288 -0
  28. package/tools/agents.js +122 -0
  29. package/tools/arcade.js +173 -0
  30. package/tools/artifact-create.js +236 -0
  31. package/tools/artifact-view.js +174 -0
  32. package/tools/ask-expert.js +160 -0
  33. package/tools/auto-suggest-connections.js +304 -0
  34. package/tools/away.js +68 -0
  35. package/tools/back.js +51 -0
  36. package/tools/become-expert.js +150 -0
  37. package/tools/bootstrap-skills.js +231 -0
  38. package/tools/bridge-dashboard.js +342 -0
  39. package/tools/bridge-health.js +400 -0
  40. package/tools/bridge-live.js +384 -0
  41. package/tools/bridges.js +383 -0
  42. package/tools/bye.js +4 -0
  43. package/tools/collaborative-drawing.js +286 -0
  44. package/tools/colorguess.js +281 -0
  45. package/tools/crossword.js +369 -0
  46. package/tools/discover-insights.js +379 -0
  47. package/tools/discover-momentum.js +256 -0
  48. package/tools/discover.js +395 -0
  49. package/tools/discovery-analytics.js +345 -0
  50. package/tools/discovery-auto-suggest.js +275 -0
  51. package/tools/discovery-bootstrap.js +267 -0
  52. package/tools/discovery-daily.js +375 -0
  53. package/tools/discovery-dashboard.js +385 -0
  54. package/tools/discovery-digest.js +314 -0
  55. package/tools/discovery-hub.js +357 -0
  56. package/tools/discovery-insights.js +384 -0
  57. package/tools/discovery-momentum.js +281 -0
  58. package/tools/discovery-monitor.js +319 -0
  59. package/tools/discovery-proactive.js +300 -0
  60. package/tools/dm.js +62 -9
  61. package/tools/draw.js +317 -0
  62. package/tools/drawing.js +310 -0
  63. package/tools/echo.js +16 -0
  64. package/tools/farcaster.js +307 -0
  65. package/tools/feed.js +196 -0
  66. package/tools/game.js +218 -110
  67. package/tools/games-catalog.js +376 -0
  68. package/tools/games.js +313 -0
  69. package/tools/genesis.js +233 -0
  70. package/tools/guessnumber.js +194 -0
  71. package/tools/hangman.js +129 -0
  72. package/tools/help.js +269 -0
  73. package/tools/idea.js +210 -0
  74. package/tools/inbox.js +148 -25
  75. package/tools/init.js +651 -33
  76. package/tools/insights.js +123 -0
  77. package/tools/invite.js +142 -21
  78. package/tools/l2-bridge.js +272 -0
  79. package/tools/l2-status.js +217 -0
  80. package/tools/l2.js +206 -0
  81. package/tools/migrate.js +156 -0
  82. package/tools/mint.js +377 -0
  83. package/tools/multiplayer-game.js +275 -0
  84. package/tools/multiplayer-tictactoe.js +303 -0
  85. package/tools/mute.js +97 -0
  86. package/tools/notifications.js +415 -0
  87. package/tools/observe.js +200 -0
  88. package/tools/onboarding.js +147 -0
  89. package/tools/open.js +14 -2
  90. package/tools/party-game.js +314 -0
  91. package/tools/presence-agent.js +167 -0
  92. package/tools/profile.js +219 -0
  93. package/tools/pulse.js +218 -0
  94. package/tools/react.js +4 -0
  95. package/tools/release.js +83 -0
  96. package/tools/report.js +109 -0
  97. package/tools/reputation.js +175 -0
  98. package/tools/request.js +217 -0
  99. package/tools/reservations.js +116 -0
  100. package/tools/reserve.js +111 -0
  101. package/tools/riddle.js +240 -0
  102. package/tools/run-bootstrap.js +69 -0
  103. package/tools/settings.js +112 -0
  104. package/tools/ship.js +182 -0
  105. package/tools/shipback.js +326 -0
  106. package/tools/skills-analytics.js +349 -0
  107. package/tools/skills-bootstrap.js +301 -0
  108. package/tools/skills-dashboard.js +268 -0
  109. package/tools/skills-exchange.js +342 -0
  110. package/tools/skills.js +380 -0
  111. package/tools/smart-intro.js +353 -0
  112. package/tools/social-inbox.js +326 -69
  113. package/tools/social-post.js +251 -66
  114. package/tools/social-processor.js +445 -0
  115. package/tools/solo-game.js +390 -0
  116. package/tools/start.js +205 -83
  117. package/tools/storybuilder.js +331 -0
  118. package/tools/suggest-tags.js +186 -0
  119. package/tools/tag-suggestions.js +257 -0
  120. package/tools/telegram-bot.js +183 -0
  121. package/tools/telegram-setup.js +214 -0
  122. package/tools/tictactoe.js +155 -0
  123. package/tools/tip.js +120 -0
  124. package/tools/token.js +103 -0
  125. package/tools/twentyquestions.js +143 -0
  126. package/tools/wallet.js +127 -0
  127. package/tools/webhook-test.js +388 -0
  128. package/tools/who.js +118 -25
  129. package/tools/wordassociation.js +247 -0
  130. package/tools/workshop-buddy.js +394 -0
  131. package/tools/workshop.js +327 -0
  132. package/version.json +12 -3
  133. package/tools/board.js +0 -130
@@ -0,0 +1,287 @@
1
+ /**
2
+ * User Profiles Store — Persistent user profiles with interests, tags, and projects
3
+ *
4
+ * Stores rich user data for matchmaking:
5
+ * - What they're building
6
+ * - Interests (broad topics they care about)
7
+ * - Tags (specific skills/technologies)
8
+ * - Activity patterns
9
+ * - Connection history
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const config = require('../config');
15
+
16
+ const PROFILES_FILE = path.join(config.VIBE_DIR, 'profiles.json');
17
+
18
+ // Load all profiles from disk
19
+ function loadProfiles() {
20
+ try {
21
+ if (fs.existsSync(PROFILES_FILE)) {
22
+ const data = fs.readFileSync(PROFILES_FILE, 'utf8');
23
+ return JSON.parse(data);
24
+ }
25
+ } catch (e) {
26
+ console.warn('Failed to load profiles:', e.message);
27
+ }
28
+ return {};
29
+ }
30
+
31
+ // Save all profiles to disk
32
+ function saveProfiles(profiles) {
33
+ try {
34
+ fs.writeFileSync(PROFILES_FILE, JSON.stringify(profiles, null, 2));
35
+ } catch (e) {
36
+ console.error('Failed to save profiles:', e.message);
37
+ }
38
+ }
39
+
40
+ // Get a single user's profile
41
+ async function getProfile(handle) {
42
+ const profiles = loadProfiles();
43
+ const key = handle.toLowerCase().replace('@', '');
44
+
45
+ return profiles[key] || {
46
+ handle: key,
47
+ building: null,
48
+ interests: [],
49
+ tags: [],
50
+ lastSeen: null,
51
+ firstSeen: null,
52
+ connections: [],
53
+ ships: []
54
+ };
55
+ }
56
+
57
+ // Update user profile
58
+ async function updateProfile(handle, updates) {
59
+ const profiles = loadProfiles();
60
+ const key = handle.toLowerCase().replace('@', '');
61
+
62
+ const existing = profiles[key] || {
63
+ handle: key,
64
+ building: null,
65
+ interests: [],
66
+ tags: [],
67
+ lastSeen: null,
68
+ firstSeen: null,
69
+ connections: [],
70
+ ships: []
71
+ };
72
+
73
+ // Merge updates
74
+ const updated = { ...existing, ...updates };
75
+
76
+ // Ensure arrays are properly formatted
77
+ if (updates.interests) {
78
+ updated.interests = Array.isArray(updates.interests)
79
+ ? updates.interests
80
+ : updates.interests.split(',').map(s => s.trim()).filter(s => s);
81
+ }
82
+
83
+ if (updates.tags) {
84
+ updated.tags = Array.isArray(updates.tags)
85
+ ? updates.tags
86
+ : updates.tags.split(',').map(s => s.trim()).filter(s => s);
87
+ }
88
+
89
+ // Update timestamps
90
+ updated.lastSeen = Date.now();
91
+ if (!existing.firstSeen) {
92
+ updated.firstSeen = Date.now();
93
+ }
94
+
95
+ profiles[key] = updated;
96
+ saveProfiles(profiles);
97
+
98
+ return updated;
99
+ }
100
+
101
+ // Get all profiles
102
+ async function getAllProfiles() {
103
+ const profiles = loadProfiles();
104
+ return Object.values(profiles);
105
+ }
106
+
107
+ // Record a connection between users
108
+ async function recordConnection(from, to, reason) {
109
+ const profiles = loadProfiles();
110
+ const fromKey = from.toLowerCase().replace('@', '');
111
+ const toKey = to.toLowerCase().replace('@', '');
112
+
113
+ const connection = {
114
+ timestamp: Date.now(),
115
+ reason,
116
+ suggested_by: 'discovery-agent'
117
+ };
118
+
119
+ // Add to both profiles
120
+ if (!profiles[fromKey]) profiles[fromKey] = createEmptyProfile(fromKey);
121
+ if (!profiles[toKey]) profiles[toKey] = createEmptyProfile(toKey);
122
+
123
+ if (!profiles[fromKey].connections) profiles[fromKey].connections = [];
124
+ if (!profiles[toKey].connections) profiles[toKey].connections = [];
125
+
126
+ profiles[fromKey].connections.push({ handle: toKey, ...connection });
127
+ profiles[toKey].connections.push({ handle: fromKey, ...connection });
128
+
129
+ saveProfiles(profiles);
130
+ }
131
+
132
+ // Record when someone ships something
133
+ async function recordShip(handle, what) {
134
+ const profiles = loadProfiles();
135
+ const key = handle.toLowerCase().replace('@', '');
136
+
137
+ if (!profiles[key]) {
138
+ profiles[key] = createEmptyProfile(key);
139
+ }
140
+
141
+ if (!profiles[key].ships) {
142
+ profiles[key].ships = [];
143
+ }
144
+
145
+ profiles[key].ships.unshift({
146
+ what,
147
+ timestamp: Date.now()
148
+ });
149
+
150
+ // Keep only last 10 ships
151
+ profiles[key].ships = profiles[key].ships.slice(0, 10);
152
+
153
+ saveProfiles(profiles);
154
+ }
155
+
156
+ // Update last seen time
157
+ async function updateLastSeen(handle) {
158
+ const profiles = loadProfiles();
159
+ const key = handle.toLowerCase().replace('@', '');
160
+
161
+ if (profiles[key]) {
162
+ profiles[key].lastSeen = Date.now();
163
+ saveProfiles(profiles);
164
+ }
165
+ }
166
+
167
+ // Get connection history between two users
168
+ async function getConnectionHistory(handle1, handle2) {
169
+ const profile = await getProfile(handle1);
170
+ const key2 = handle2.toLowerCase().replace('@', '');
171
+
172
+ return profile.connections?.filter(c => c.handle === key2) || [];
173
+ }
174
+
175
+ // Check if users have been connected before
176
+ async function hasBeenConnected(handle1, handle2) {
177
+ const history = await getConnectionHistory(handle1, handle2);
178
+ return history.length > 0;
179
+ }
180
+
181
+ // Get users by interest
182
+ async function getUsersByInterest(interest) {
183
+ const profiles = await getAllProfiles();
184
+ const searchTerm = interest.toLowerCase();
185
+
186
+ return profiles.filter(p =>
187
+ p.interests?.some(i => i.toLowerCase().includes(searchTerm))
188
+ );
189
+ }
190
+
191
+ // Get users by tag
192
+ async function getUsersByTag(tag) {
193
+ const profiles = await getAllProfiles();
194
+ const searchTerm = tag.toLowerCase();
195
+
196
+ return profiles.filter(p =>
197
+ p.tags?.some(t => t.toLowerCase().includes(searchTerm))
198
+ );
199
+ }
200
+
201
+ // Get trending interests
202
+ async function getTrendingInterests() {
203
+ const profiles = await getAllProfiles();
204
+ const interestCount = {};
205
+
206
+ for (const profile of profiles) {
207
+ if (profile.interests) {
208
+ for (const interest of profile.interests) {
209
+ interestCount[interest] = (interestCount[interest] || 0) + 1;
210
+ }
211
+ }
212
+ }
213
+
214
+ return Object.entries(interestCount)
215
+ .sort(([,a], [,b]) => b - a)
216
+ .slice(0, 10)
217
+ .map(([interest, count]) => ({ interest, count }));
218
+ }
219
+
220
+ // Get trending tags
221
+ async function getTrendingTags() {
222
+ const profiles = await getAllProfiles();
223
+ const tagCount = {};
224
+
225
+ for (const profile of profiles) {
226
+ if (profile.tags) {
227
+ for (const tag of profile.tags) {
228
+ tagCount[tag] = (tagCount[tag] || 0) + 1;
229
+ }
230
+ }
231
+ }
232
+
233
+ return Object.entries(tagCount)
234
+ .sort(([,a], [,b]) => b - a)
235
+ .slice(0, 15)
236
+ .map(([tag, count]) => ({ tag, count }));
237
+ }
238
+
239
+ // Create empty profile structure
240
+ function createEmptyProfile(handle) {
241
+ return {
242
+ handle,
243
+ building: null,
244
+ interests: [],
245
+ tags: [],
246
+ lastSeen: Date.now(),
247
+ firstSeen: Date.now(),
248
+ connections: [],
249
+ ships: []
250
+ };
251
+ }
252
+
253
+ // Clean up old profiles (for maintenance)
254
+ async function cleanupOldProfiles(daysThreshold = 30) {
255
+ const profiles = loadProfiles();
256
+ const cutoff = Date.now() - (daysThreshold * 24 * 60 * 60 * 1000);
257
+
258
+ let cleaned = 0;
259
+ for (const [key, profile] of Object.entries(profiles)) {
260
+ if (profile.lastSeen && profile.lastSeen < cutoff) {
261
+ delete profiles[key];
262
+ cleaned++;
263
+ }
264
+ }
265
+
266
+ if (cleaned > 0) {
267
+ saveProfiles(profiles);
268
+ }
269
+
270
+ return cleaned;
271
+ }
272
+
273
+ module.exports = {
274
+ getProfile,
275
+ updateProfile,
276
+ getAllProfiles,
277
+ recordConnection,
278
+ recordShip,
279
+ updateLastSeen,
280
+ getConnectionHistory,
281
+ hasBeenConnected,
282
+ getUsersByInterest,
283
+ getUsersByTag,
284
+ getTrendingInterests,
285
+ getTrendingTags,
286
+ cleanupOldProfiles
287
+ };
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Reservations Store — Local-first file reservations
3
+ *
4
+ * Advisory locks to signal edit intent and reduce conflicts.
5
+ * Stored locally in ~/.vibe/reservations/
6
+ *
7
+ * Key behaviors:
8
+ * - Scope auto-detected from git remote, fallback to cwd
9
+ * - Paths are relative to scope
10
+ * - TTL is client-enforced (check expires_ts on read)
11
+ * - Warn on conflict (overlapping exclusive paths), don't block
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { execSync } = require('child_process');
17
+ const crypto = require('crypto');
18
+
19
+ const RESERVATIONS_DIR = path.join(process.env.HOME, '.vibe', 'reservations');
20
+ const ACTIVE_FILE = path.join(RESERVATIONS_DIR, 'active.jsonl');
21
+ const HISTORY_FILE = path.join(RESERVATIONS_DIR, 'history.jsonl');
22
+ const INDEX_FILE = path.join(RESERVATIONS_DIR, 'index.json');
23
+
24
+ // Ensure directories exist
25
+ function ensureDir() {
26
+ if (!fs.existsSync(RESERVATIONS_DIR)) {
27
+ fs.mkdirSync(RESERVATIONS_DIR, { recursive: true });
28
+ }
29
+ }
30
+
31
+ // Generate reservation ID: rsv-{nanoid}
32
+ function generateId() {
33
+ return 'rsv-' + crypto.randomBytes(4).toString('hex');
34
+ }
35
+
36
+ // Get git remote URL for scope
37
+ function getGitRemote() {
38
+ try {
39
+ const remote = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf8' }).trim();
40
+ // Normalize: git@github.com:user/repo.git → github.com/user/repo
41
+ if (remote.startsWith('git@')) {
42
+ return remote.replace('git@', '').replace(':', '/').replace('.git', '');
43
+ }
44
+ // https://github.com/user/repo.git → github.com/user/repo
45
+ if (remote.startsWith('https://')) {
46
+ return remote.replace('https://', '').replace('.git', '');
47
+ }
48
+ return remote;
49
+ } catch (e) {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ // Get current working directory name as fallback scope
55
+ function getLocalScope() {
56
+ try {
57
+ const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { encoding: 'utf8' }).trim();
58
+ return `local:${path.basename(root)}`;
59
+ } catch (e) {
60
+ return `local:${path.basename(process.cwd())}`;
61
+ }
62
+ }
63
+
64
+ // Get scope for reservations
65
+ function getScope() {
66
+ const remote = getGitRemote();
67
+ if (remote) {
68
+ return `repo:${remote}`;
69
+ }
70
+ return getLocalScope();
71
+ }
72
+
73
+ // Read all active reservations
74
+ function readActive() {
75
+ ensureDir();
76
+ if (!fs.existsSync(ACTIVE_FILE)) {
77
+ return [];
78
+ }
79
+ const content = fs.readFileSync(ACTIVE_FILE, 'utf8');
80
+ const reservations = content
81
+ .split('\n')
82
+ .filter(line => line.trim())
83
+ .map(line => {
84
+ try {
85
+ return JSON.parse(line);
86
+ } catch (e) {
87
+ return null;
88
+ }
89
+ })
90
+ .filter(r => r !== null);
91
+
92
+ // Filter out expired
93
+ const now = new Date().toISOString();
94
+ return reservations.filter(r => r.expires_ts > now && r.status === 'active');
95
+ }
96
+
97
+ // Write active reservations (rewrite entire file)
98
+ function writeActive(reservations) {
99
+ ensureDir();
100
+ const content = reservations.map(r => JSON.stringify(r)).join('\n') + (reservations.length ? '\n' : '');
101
+ fs.writeFileSync(ACTIVE_FILE, content);
102
+ }
103
+
104
+ // Append to history
105
+ function appendHistory(reservation) {
106
+ ensureDir();
107
+ fs.appendFileSync(HISTORY_FILE, JSON.stringify(reservation) + '\n');
108
+ }
109
+
110
+ // Update index (for quick lookups)
111
+ function updateIndex(reservations) {
112
+ ensureDir();
113
+ const index = {
114
+ count: reservations.length,
115
+ byPath: {},
116
+ byId: {},
117
+ updatedAt: new Date().toISOString()
118
+ };
119
+
120
+ for (const r of reservations) {
121
+ index.byId[r.reservation_id] = r;
122
+ for (const p of r.paths) {
123
+ if (!index.byPath[p]) {
124
+ index.byPath[p] = [];
125
+ }
126
+ index.byPath[p].push(r.reservation_id);
127
+ }
128
+ }
129
+
130
+ fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2));
131
+ }
132
+
133
+ // Check for conflicts with existing reservations
134
+ function checkConflicts(paths, exclusive = true, scope = null) {
135
+ const active = readActive();
136
+ const targetScope = scope || getScope();
137
+ const conflicts = [];
138
+
139
+ for (const reservation of active) {
140
+ // Only check same scope
141
+ if (reservation.scope !== targetScope) continue;
142
+ // Only check exclusive reservations
143
+ if (!reservation.exclusive) continue;
144
+
145
+ // Check path overlap
146
+ for (const newPath of paths) {
147
+ for (const existingPath of reservation.paths) {
148
+ // Exact match
149
+ if (newPath === existingPath) {
150
+ conflicts.push({
151
+ path: newPath,
152
+ reservation_id: reservation.reservation_id,
153
+ owner: reservation.owner,
154
+ reason: reservation.reason,
155
+ expires_ts: reservation.expires_ts
156
+ });
157
+ }
158
+ // Check if new path is under existing (existing: src/, new: src/auth.ts)
159
+ else if (newPath.startsWith(existingPath + '/')) {
160
+ conflicts.push({
161
+ path: newPath,
162
+ conflictsWith: existingPath,
163
+ reservation_id: reservation.reservation_id,
164
+ owner: reservation.owner,
165
+ reason: reservation.reason,
166
+ expires_ts: reservation.expires_ts
167
+ });
168
+ }
169
+ // Check if existing path is under new (existing: src/auth.ts, new: src/)
170
+ else if (existingPath.startsWith(newPath + '/')) {
171
+ conflicts.push({
172
+ path: existingPath,
173
+ conflictsWith: newPath,
174
+ reservation_id: reservation.reservation_id,
175
+ owner: reservation.owner,
176
+ reason: reservation.reason,
177
+ expires_ts: reservation.expires_ts
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ return conflicts;
185
+ }
186
+
187
+ // Create a new reservation
188
+ function create(owner, paths, options = {}) {
189
+ const {
190
+ ttl_seconds = 3600,
191
+ exclusive = true,
192
+ reason = null,
193
+ thread_id = null
194
+ } = options;
195
+
196
+ const scope = getScope();
197
+ const now = new Date();
198
+ const expires = new Date(now.getTime() + ttl_seconds * 1000);
199
+
200
+ // Check for conflicts
201
+ const conflicts = checkConflicts(paths, exclusive, scope);
202
+
203
+ const reservation = {
204
+ reservation_id: generateId(),
205
+ scope,
206
+ paths,
207
+ exclusive,
208
+ reason,
209
+ thread_id,
210
+ owner,
211
+ status: 'active',
212
+ ttl_seconds,
213
+ issued_ts: now.toISOString(),
214
+ expires_ts: expires.toISOString()
215
+ };
216
+
217
+ // Add to active
218
+ const active = readActive();
219
+ active.push(reservation);
220
+ writeActive(active);
221
+ updateIndex(active);
222
+
223
+ return {
224
+ reservation,
225
+ conflicts: conflicts.length > 0 ? conflicts : null,
226
+ warning: conflicts.length > 0 ? `${conflicts.length} conflicting reservation(s) found` : null
227
+ };
228
+ }
229
+
230
+ // Release a reservation
231
+ function release(reservation_id, owner = null) {
232
+ const active = readActive();
233
+ const index = active.findIndex(r => r.reservation_id === reservation_id);
234
+
235
+ if (index === -1) {
236
+ return { success: false, error: 'not_found', message: 'Reservation not found or already expired' };
237
+ }
238
+
239
+ const reservation = active[index];
240
+
241
+ // Optional owner check
242
+ if (owner && reservation.owner !== owner) {
243
+ return { success: false, error: 'not_owner', message: 'You are not the owner of this reservation' };
244
+ }
245
+
246
+ // Mark as released and move to history
247
+ reservation.status = 'released';
248
+ reservation.released_ts = new Date().toISOString();
249
+ appendHistory(reservation);
250
+
251
+ // Remove from active
252
+ active.splice(index, 1);
253
+ writeActive(active);
254
+ updateIndex(active);
255
+
256
+ return { success: true, reservation };
257
+ }
258
+
259
+ // List reservations
260
+ function list(options = {}) {
261
+ const { active_only = true, path_filter = null, scope_filter = null } = options;
262
+
263
+ let reservations = readActive();
264
+
265
+ // Filter by scope
266
+ if (scope_filter) {
267
+ reservations = reservations.filter(r => r.scope === scope_filter);
268
+ } else {
269
+ // Default: only show current scope
270
+ const currentScope = getScope();
271
+ reservations = reservations.filter(r => r.scope === currentScope);
272
+ }
273
+
274
+ // Filter by path
275
+ if (path_filter) {
276
+ reservations = reservations.filter(r =>
277
+ r.paths.some(p => p.includes(path_filter) || path_filter.includes(p))
278
+ );
279
+ }
280
+
281
+ return reservations;
282
+ }
283
+
284
+ // Get a single reservation by ID
285
+ function get(reservation_id) {
286
+ const active = readActive();
287
+ return active.find(r => r.reservation_id === reservation_id) || null;
288
+ }
289
+
290
+ // Cleanup expired reservations (called periodically)
291
+ function cleanup() {
292
+ const active = readActive();
293
+ const now = new Date().toISOString();
294
+ const expired = active.filter(r => r.expires_ts <= now);
295
+ const remaining = active.filter(r => r.expires_ts > now);
296
+
297
+ // Move expired to history
298
+ for (const r of expired) {
299
+ r.status = 'expired';
300
+ r.expired_ts = now;
301
+ appendHistory(r);
302
+ }
303
+
304
+ if (expired.length > 0) {
305
+ writeActive(remaining);
306
+ updateIndex(remaining);
307
+ }
308
+
309
+ return { expired: expired.length, remaining: remaining.length };
310
+ }
311
+
312
+ module.exports = {
313
+ create,
314
+ release,
315
+ list,
316
+ get,
317
+ getScope,
318
+ checkConflicts,
319
+ cleanup,
320
+ generateId
321
+ };