@prmichaelsen/remember-mcp 3.15.3 → 3.15.5

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 (132) hide show
  1. package/AGENT.md +363 -5
  2. package/CHANGELOG.md +7 -0
  3. package/agent/commands/acp.clarification-capture.md +386 -0
  4. package/agent/commands/acp.clarification-create.md +50 -0
  5. package/agent/commands/acp.command-create.md +60 -0
  6. package/agent/commands/acp.design-create.md +62 -0
  7. package/agent/commands/acp.design-reference.md +355 -0
  8. package/agent/commands/acp.index.md +423 -0
  9. package/agent/commands/acp.init.md +48 -0
  10. package/agent/commands/acp.package-create.md +1 -0
  11. package/agent/commands/acp.package-info.md +1 -0
  12. package/agent/commands/acp.package-install.md +19 -0
  13. package/agent/commands/acp.package-list.md +1 -0
  14. package/agent/commands/acp.package-publish.md +1 -0
  15. package/agent/commands/acp.package-remove.md +1 -0
  16. package/agent/commands/acp.package-search.md +1 -0
  17. package/agent/commands/acp.package-update.md +1 -0
  18. package/agent/commands/acp.package-validate.md +1 -0
  19. package/agent/commands/acp.pattern-create.md +60 -0
  20. package/agent/commands/acp.plan.md +25 -0
  21. package/agent/commands/acp.proceed.md +621 -75
  22. package/agent/commands/acp.project-create.md +3 -0
  23. package/agent/commands/acp.project-info.md +3 -0
  24. package/agent/commands/acp.project-list.md +3 -1
  25. package/agent/commands/acp.project-set.md +1 -0
  26. package/agent/commands/acp.project-update.md +14 -3
  27. package/agent/commands/acp.projects-restore.md +228 -0
  28. package/agent/commands/acp.projects-sync.md +347 -0
  29. package/agent/commands/acp.report.md +13 -0
  30. package/agent/commands/acp.resume.md +3 -1
  31. package/agent/commands/acp.sessions.md +301 -0
  32. package/agent/commands/acp.status.md +13 -0
  33. package/agent/commands/acp.sync.md +1 -0
  34. package/agent/commands/acp.task-create.md +105 -3
  35. package/agent/commands/acp.update.md +1 -0
  36. package/agent/commands/acp.validate.md +32 -2
  37. package/agent/commands/acp.version-check-for-updates.md +1 -0
  38. package/agent/commands/acp.version-check.md +1 -0
  39. package/agent/commands/acp.version-update.md +1 -0
  40. package/agent/commands/command.template.md +23 -0
  41. package/agent/commands/git.commit.md +1 -0
  42. package/agent/commands/git.init.md +1 -0
  43. package/agent/design/complete-tool-set.md +157 -233
  44. package/agent/design/design.template.md +18 -0
  45. package/agent/design/user-preferences.md +11 -7
  46. package/agent/milestones/milestone-19-new-search-ghost-tools.md +46 -0
  47. package/agent/package.template.yaml +50 -0
  48. package/agent/patterns/pattern.template.md +18 -0
  49. package/agent/progress.yaml +162 -6
  50. package/agent/scripts/acp.common.sh +258 -15
  51. package/agent/scripts/acp.install.sh +91 -4
  52. package/agent/scripts/acp.package-create.sh +0 -1
  53. package/agent/scripts/acp.package-info.sh +19 -1
  54. package/agent/scripts/acp.package-install-optimized.sh +1 -1
  55. package/agent/scripts/acp.package-install.sh +388 -38
  56. package/agent/scripts/acp.package-list.sh +52 -4
  57. package/agent/scripts/acp.package-remove.sh +77 -1
  58. package/agent/scripts/acp.package-search.sh +2 -2
  59. package/agent/scripts/acp.package-update.sh +91 -12
  60. package/agent/scripts/acp.package-validate.sh +136 -1
  61. package/agent/scripts/acp.project-info.sh +34 -11
  62. package/agent/scripts/acp.project-list.sh +4 -0
  63. package/agent/scripts/acp.project-update.sh +66 -19
  64. package/agent/scripts/acp.projects-restore.sh +170 -0
  65. package/agent/scripts/acp.projects-sync.sh +155 -0
  66. package/agent/scripts/acp.sessions.sh +725 -0
  67. package/agent/scripts/acp.version-update.sh +21 -3
  68. package/agent/scripts/acp.yaml-parser.sh +20 -6
  69. package/agent/tasks/milestone-19-new-search-ghost-tools/task-203-create-search-by-tool.md +143 -0
  70. package/agent/tasks/milestone-19-new-search-ghost-tools/task-204-add-new-filters-existing-tools.md +77 -0
  71. package/agent/tasks/milestone-19-new-search-ghost-tools/task-205-add-feel-fields-create-update.md +137 -0
  72. package/agent/tasks/milestone-19-new-search-ghost-tools/task-206-add-byproperty-bysignificance-modes.md +135 -0
  73. package/agent/tasks/milestone-19-new-search-ghost-tools/task-207-add-emotional-composites-search-results.md +88 -0
  74. package/agent/tasks/milestone-19-new-search-ghost-tools/task-208-add-bybroad-byrandom-modes.md +115 -0
  75. package/agent/tasks/milestone-19-new-search-ghost-tools/task-209-create-ghost-memory-tools.md +192 -0
  76. package/agent/tasks/milestone-19-new-search-ghost-tools/task-210-create-get-core-tool.md +203 -0
  77. package/agent/tasks/milestone-19-new-search-ghost-tools/task-211-create-search-space-by-tool.md +182 -0
  78. package/agent/tasks/task-1-{title}.template.md +19 -0
  79. package/agent/tasks/unassigned/bug-report-remember-core-e2e-findings.md +99 -0
  80. package/dist/e2e-helpers.d.ts +26 -0
  81. package/dist/ghost-persona.e2e.d.ts +8 -0
  82. package/dist/memory-crud.e2e.d.ts +8 -0
  83. package/dist/preferences.e2e.d.ts +8 -0
  84. package/dist/relationships.e2e.d.ts +8 -0
  85. package/dist/search-modes.e2e.d.ts +8 -0
  86. package/dist/server-factory.js +2158 -45
  87. package/dist/server.js +1403 -44
  88. package/dist/shared-spaces.e2e.d.ts +8 -0
  89. package/dist/tools/create-ghost-memory.d.ts +70 -0
  90. package/dist/tools/create-memory.d.ts +175 -0
  91. package/dist/tools/get-core.d.ts +28 -0
  92. package/dist/tools/get-core.spec.d.ts +2 -0
  93. package/dist/tools/ghost-tools.spec.d.ts +2 -0
  94. package/dist/tools/query-ghost-memory.d.ts +34 -0
  95. package/dist/tools/query-memory.d.ts +4 -0
  96. package/dist/tools/search-by.d.ts +147 -0
  97. package/dist/tools/search-by.spec.d.ts +2 -0
  98. package/dist/tools/search-ghost-memory-by.d.ts +54 -0
  99. package/dist/tools/search-ghost-memory.d.ts +53 -0
  100. package/dist/tools/search-memory.d.ts +19 -0
  101. package/dist/tools/search-space-by.d.ts +78 -0
  102. package/dist/tools/search-space-by.spec.d.ts +2 -0
  103. package/dist/tools/search-space.d.ts +2 -0
  104. package/dist/tools/update-ghost-memory.d.ts +51 -0
  105. package/dist/tools/update-memory.d.ts +175 -0
  106. package/jest.e2e.config.js +11 -0
  107. package/package.json +2 -2
  108. package/src/e2e-helpers.ts +86 -0
  109. package/src/ghost-persona.e2e.ts +215 -0
  110. package/src/memory-crud.e2e.ts +203 -0
  111. package/src/preferences.e2e.ts +88 -0
  112. package/src/relationships.e2e.ts +156 -0
  113. package/src/search-modes.e2e.ts +184 -0
  114. package/src/server-factory.ts +56 -0
  115. package/src/shared-spaces.e2e.ts +204 -0
  116. package/src/tools/create-ghost-memory.ts +103 -0
  117. package/src/tools/create-memory.ts +45 -1
  118. package/src/tools/get-core.spec.ts +223 -0
  119. package/src/tools/get-core.ts +109 -0
  120. package/src/tools/ghost-tools.spec.ts +361 -0
  121. package/src/tools/query-ghost-memory.ts +63 -0
  122. package/src/tools/query-memory.ts +4 -0
  123. package/src/tools/search-by.spec.ts +325 -0
  124. package/src/tools/search-by.ts +298 -0
  125. package/src/tools/search-ghost-memory-by.ts +80 -0
  126. package/src/tools/search-ghost-memory.ts +73 -0
  127. package/src/tools/search-memory.ts +23 -0
  128. package/src/tools/search-space-by.spec.ts +289 -0
  129. package/src/tools/search-space-by.ts +173 -0
  130. package/src/tools/search-space.ts +20 -1
  131. package/src/tools/update-ghost-memory.ts +86 -0
  132. package/src/tools/update-memory.ts +45 -1
@@ -0,0 +1,725 @@
1
+ #!/usr/bin/env bash
2
+ # acp.sessions.sh - Global session tracking for concurrent multi-project agent work
3
+ # Part of Agent Context Protocol (ACP)
4
+ # Usage: ./acp.sessions.sh <subcommand> [options]
5
+ #
6
+ # Subcommands:
7
+ # register Register a new session
8
+ # deregister Remove a session
9
+ # list List active sessions
10
+ # clean Remove stale sessions
11
+ # heartbeat Update session activity
12
+ # count Output count of active sessions
13
+
14
+ set -e
15
+
16
+ # Get script directory
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+
19
+ # Source common utilities
20
+ source "${SCRIPT_DIR}/acp.common.sh"
21
+
22
+ # Source YAML parser
23
+ source_yaml_parser
24
+
25
+ # Sessions file location
26
+ SESSIONS_FILE="${HOME}/.acp/sessions.yaml"
27
+ SESSIONS_TEMPLATE="${SCRIPT_DIR}/../sessions.template.yaml"
28
+
29
+ # Stale thresholds (in seconds)
30
+ IDLE_THRESHOLD=1800 # 30 minutes
31
+ REMOVE_THRESHOLD=7200 # 2 hours
32
+
33
+ # ============================================================================
34
+ # HELPERS
35
+ # ============================================================================
36
+
37
+ # Generate a unique session ID
38
+ # Falls back to $RANDOM if xxd is unavailable
39
+ generate_session_id() {
40
+ local hex
41
+ if command -v xxd >/dev/null 2>&1; then
42
+ hex=$(head -c 3 /dev/urandom | xxd -p)
43
+ else
44
+ hex=$(printf '%06x' $RANDOM)
45
+ fi
46
+ echo "sess_${hex}"
47
+ }
48
+
49
+ # Ensure sessions.yaml exists, creating from template if needed
50
+ ensure_sessions_file() {
51
+ if [ ! -f "$SESSIONS_FILE" ]; then
52
+ mkdir -p "$(dirname "$SESSIONS_FILE")"
53
+ if [ -f "$SESSIONS_TEMPLATE" ]; then
54
+ cp "$SESSIONS_TEMPLATE" "$SESSIONS_FILE"
55
+ else
56
+ cat > "$SESSIONS_FILE" << 'TMPL'
57
+ # ~/.acp/sessions.yaml
58
+ # Managed by acp.sessions.sh — do not edit manually
59
+
60
+ sessions: []
61
+
62
+ last_updated: null
63
+ TMPL
64
+ fi
65
+ fi
66
+ }
67
+
68
+ # Get current timestamp in epoch seconds
69
+ get_epoch() {
70
+ date +%s
71
+ }
72
+
73
+ # Convert ISO timestamp to epoch seconds
74
+ iso_to_epoch() {
75
+ local ts="$1"
76
+ if [ -z "$ts" ] || [ "$ts" = "null" ]; then
77
+ echo "0"
78
+ return
79
+ fi
80
+ date -d "$ts" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +%s 2>/dev/null || echo "0"
81
+ }
82
+
83
+ # Format seconds as relative time (e.g., "2m ago", "1h ago")
84
+ format_relative_time() {
85
+ local seconds="$1"
86
+ if [ "$seconds" -lt 60 ]; then
87
+ echo "now"
88
+ elif [ "$seconds" -lt 3600 ]; then
89
+ echo "$((seconds / 60))m ago"
90
+ elif [ "$seconds" -lt 86400 ]; then
91
+ echo "$((seconds / 3600))h ago"
92
+ else
93
+ echo "$((seconds / 86400))d ago"
94
+ fi
95
+ }
96
+
97
+ # Get session count from parsed YAML
98
+ get_session_count() {
99
+ local count
100
+ count=$(yaml_get_array "$SESSIONS_FILE" "sessions" 2>/dev/null || echo "0")
101
+ echo "$count"
102
+ }
103
+
104
+ # Write sessions.yaml directly from session data arrays
105
+ # This bypasses yaml_delete limitations with array indices
106
+ # Args: space-separated list of indices to EXCLUDE
107
+ write_sessions_excluding() {
108
+ local exclude_indices="$1"
109
+ local now
110
+ now=$(get_timestamp)
111
+
112
+ # Read current session count (AST must already be loaded)
113
+ local count
114
+ count=$(yaml_get_array "$SESSIONS_FILE" "sessions" 2>/dev/null || echo "0")
115
+
116
+ # Build output file
117
+ local tmp_file="${SESSIONS_FILE}.tmp"
118
+ {
119
+ echo "# ~/.acp/sessions.yaml"
120
+ echo "# Managed by acp.sessions.sh — do not edit manually"
121
+ echo ""
122
+
123
+ local has_sessions=false
124
+ local i=0
125
+ while [ "$i" -lt "$count" ]; do
126
+ # Check if this index should be excluded
127
+ local skip=false
128
+ for ex in $exclude_indices; do
129
+ if [ "$ex" = "$i" ]; then
130
+ skip=true
131
+ break
132
+ fi
133
+ done
134
+
135
+ if [ "$skip" = "false" ]; then
136
+ if [ "$has_sessions" = "false" ]; then
137
+ echo "sessions:"
138
+ has_sessions=true
139
+ fi
140
+
141
+ local id proj desc started last_activity status milestone task pid terminal remote_url
142
+ id=$(yaml_query ".sessions[${i}].id" 2>/dev/null || echo "")
143
+ proj=$(yaml_query ".sessions[${i}].project" 2>/dev/null || echo "")
144
+ desc=$(yaml_query ".sessions[${i}].description" 2>/dev/null || echo "")
145
+ started=$(yaml_query ".sessions[${i}].started" 2>/dev/null || echo "")
146
+ last_activity=$(yaml_query ".sessions[${i}].last_activity" 2>/dev/null || echo "")
147
+ status=$(yaml_query ".sessions[${i}].status" 2>/dev/null || echo "active")
148
+ milestone=$(yaml_query ".sessions[${i}].current_milestone" 2>/dev/null || echo "")
149
+ task=$(yaml_query ".sessions[${i}].current_task" 2>/dev/null || echo "")
150
+ pid=$(yaml_query ".sessions[${i}].pid" 2>/dev/null || echo "")
151
+ terminal=$(yaml_query ".sessions[${i}].terminal" 2>/dev/null || echo "")
152
+ remote_url=$(yaml_query ".sessions[${i}].remote_url" 2>/dev/null || echo "")
153
+
154
+ # Normalize null values
155
+ [ "$milestone" = "null" ] && milestone=""
156
+ [ "$task" = "null" ] && task=""
157
+ [ "$remote_url" = "null" ] && remote_url=""
158
+ [ "$desc" = "null" ] && desc=""
159
+
160
+ echo " - id: ${id}"
161
+ echo " project: ${proj}"
162
+ echo " description: ${desc}"
163
+ echo " started: ${started}"
164
+ echo " last_activity: ${last_activity}"
165
+ echo " status: ${status}"
166
+ echo " current_milestone: ${milestone}"
167
+ echo " current_task: ${task}"
168
+ echo " pid: ${pid}"
169
+ echo " terminal: ${terminal}"
170
+ echo " remote_url: ${remote_url}"
171
+ fi
172
+
173
+ i=$((i + 1))
174
+ done
175
+
176
+ if [ "$has_sessions" = "false" ]; then
177
+ echo "sessions: []"
178
+ fi
179
+
180
+ echo ""
181
+ echo "last_updated: ${now}"
182
+ } > "$tmp_file"
183
+
184
+ mv "$tmp_file" "$SESSIONS_FILE"
185
+ }
186
+
187
+ # ============================================================================
188
+ # CLEAN SUBCOMMAND
189
+ # ============================================================================
190
+
191
+ # Remove stale sessions (dead PID or timed out)
192
+ do_clean() {
193
+ local verbose="${1:-false}"
194
+ ensure_sessions_file
195
+
196
+ yaml_parse "$SESSIONS_FILE" || return 1
197
+
198
+ local count
199
+ count=$(get_session_count)
200
+
201
+ if [ "$count" = "0" ] || [ -z "$count" ]; then
202
+ if [ "$verbose" = "true" ]; then
203
+ echo "No sessions to clean."
204
+ fi
205
+ return 0
206
+ fi
207
+
208
+ local cleaned=0
209
+ local now_epoch
210
+ now_epoch=$(get_epoch)
211
+ local indices_to_remove=""
212
+ local idle_indices=""
213
+
214
+ # Check each session
215
+ local i=0
216
+ while [ "$i" -lt "$count" ]; do
217
+ local pid last_activity status
218
+
219
+ pid=$(yaml_query ".sessions[${i}].pid" 2>/dev/null || echo "")
220
+ last_activity=$(yaml_query ".sessions[${i}].last_activity" 2>/dev/null || echo "")
221
+ status=$(yaml_query ".sessions[${i}].status" 2>/dev/null || echo "active")
222
+
223
+ local should_remove=false
224
+
225
+ # Check 1: PID is dead
226
+ if [ -n "$pid" ] && [ "$pid" != "null" ] && [ "$pid" != "0" ]; then
227
+ if ! kill -0 "$pid" 2>/dev/null; then
228
+ should_remove=true
229
+ if [ "$verbose" = "true" ]; then
230
+ local proj sid
231
+ proj=$(yaml_query ".sessions[${i}].project" 2>/dev/null || echo "unknown")
232
+ sid=$(yaml_query ".sessions[${i}].id" 2>/dev/null || echo "unknown")
233
+ echo " ${sid} ${proj} (PID ${pid} not running)"
234
+ fi
235
+ fi
236
+ fi
237
+
238
+ # Check 2: Timeout
239
+ if [ "$should_remove" = "false" ] && [ -n "$last_activity" ] && [ "$last_activity" != "null" ]; then
240
+ local activity_epoch elapsed
241
+ activity_epoch=$(iso_to_epoch "$last_activity")
242
+ elapsed=$((now_epoch - activity_epoch))
243
+
244
+ if [ "$elapsed" -gt "$REMOVE_THRESHOLD" ]; then
245
+ should_remove=true
246
+ if [ "$verbose" = "true" ]; then
247
+ local proj sid
248
+ proj=$(yaml_query ".sessions[${i}].project" 2>/dev/null || echo "unknown")
249
+ sid=$(yaml_query ".sessions[${i}].id" 2>/dev/null || echo "unknown")
250
+ echo " ${sid} ${proj} (inactive for $(format_relative_time "$elapsed"))"
251
+ fi
252
+ elif [ "$elapsed" -gt "$IDLE_THRESHOLD" ]; then
253
+ idle_indices="${idle_indices} ${i}"
254
+ fi
255
+ fi
256
+
257
+ if [ "$should_remove" = "true" ]; then
258
+ indices_to_remove="${indices_to_remove} ${i}"
259
+ cleaned=$((cleaned + 1))
260
+ fi
261
+
262
+ i=$((i + 1))
263
+ done
264
+
265
+ # Mark idle sessions (update in-place via yaml_set before rebuild)
266
+ for idx in $idle_indices; do
267
+ yaml_set ".sessions[${idx}].status" "idle"
268
+ done
269
+
270
+ # Remove flagged sessions by rebuilding file
271
+ if [ -n "$indices_to_remove" ]; then
272
+ write_sessions_excluding "$indices_to_remove"
273
+ elif [ -n "$idle_indices" ]; then
274
+ # If we only marked idle (no removals), persist the status changes
275
+ yaml_set ".last_updated" "$(get_timestamp)"
276
+ yaml_write "$SESSIONS_FILE"
277
+ fi
278
+
279
+ if [ "$verbose" = "true" ]; then
280
+ if [ "$cleaned" -gt 0 ]; then
281
+ echo ""
282
+ local remaining=$((count - cleaned))
283
+ echo "Active sessions remaining: ${remaining}"
284
+ else
285
+ echo "No stale sessions found."
286
+ fi
287
+ else
288
+ echo "$cleaned"
289
+ fi
290
+ }
291
+
292
+ # ============================================================================
293
+ # REGISTER SUBCOMMAND
294
+ # ============================================================================
295
+
296
+ do_register() {
297
+ local project="" description="" remote_url="" explicit_pid=""
298
+
299
+ while [ $# -gt 0 ]; do
300
+ case "$1" in
301
+ --project) project="$2"; shift 2 ;;
302
+ --description) description="$2"; shift 2 ;;
303
+ --remote-url) remote_url="$2"; shift 2 ;;
304
+ --pid) explicit_pid="$2"; shift 2 ;;
305
+ *) shift ;;
306
+ esac
307
+ done
308
+
309
+ if [ -z "$project" ]; then
310
+ echo "Error: --project is required" >&2
311
+ echo "Usage: $0 register --project <name> [--description <desc>] [--remote-url <url>] [--pid <pid>]" >&2
312
+ return 1
313
+ fi
314
+
315
+ ensure_sessions_file
316
+
317
+ # Clean stale sessions first
318
+ do_clean "false" >/dev/null 2>&1 || true
319
+
320
+ # Re-parse after clean
321
+ yaml_parse "$SESSIONS_FILE" || return 1
322
+
323
+ local session_id
324
+ session_id=$(generate_session_id)
325
+
326
+ local pid="${explicit_pid:-$PPID}"
327
+ local terminal
328
+ terminal=$(tty 2>/dev/null || echo "unknown")
329
+ local now
330
+ now=$(get_timestamp)
331
+
332
+ # Auto-infer description if not provided
333
+ if [ -z "$description" ]; then
334
+ description="Working on ${project}"
335
+ fi
336
+
337
+ # Append new session object to sessions array
338
+ local node_id
339
+ node_id=$(yaml_array_append_object ".sessions")
340
+ yaml_object_set "$node_id" "id" "$session_id" >/dev/null
341
+ yaml_object_set "$node_id" "project" "$project" >/dev/null
342
+ yaml_object_set "$node_id" "description" "$description" >/dev/null
343
+ yaml_object_set "$node_id" "started" "$now" >/dev/null
344
+ yaml_object_set "$node_id" "last_activity" "$now" >/dev/null
345
+ yaml_object_set "$node_id" "status" "active" >/dev/null
346
+ yaml_object_set "$node_id" "current_milestone" "" >/dev/null
347
+ yaml_object_set "$node_id" "current_task" "" >/dev/null
348
+ yaml_object_set "$node_id" "pid" "$pid" >/dev/null
349
+ yaml_object_set "$node_id" "terminal" "$terminal" >/dev/null
350
+ yaml_object_set "$node_id" "remote_url" "${remote_url:-}" >/dev/null
351
+
352
+ yaml_set ".last_updated" "$now"
353
+ yaml_write "$SESSIONS_FILE"
354
+
355
+ echo "Session ${session_id} registered for ${project}."
356
+ echo "$session_id"
357
+ }
358
+
359
+ # ============================================================================
360
+ # DEREGISTER SUBCOMMAND
361
+ # ============================================================================
362
+
363
+ do_deregister() {
364
+ local target_id=""
365
+
366
+ while [ $# -gt 0 ]; do
367
+ case "$1" in
368
+ --id) target_id="$2"; shift 2 ;;
369
+ *) shift ;;
370
+ esac
371
+ done
372
+
373
+ ensure_sessions_file
374
+ yaml_parse "$SESSIONS_FILE" || return 1
375
+
376
+ local count
377
+ count=$(get_session_count)
378
+
379
+ if [ "$count" = "0" ] || [ -z "$count" ]; then
380
+ echo "No active sessions."
381
+ return 0
382
+ fi
383
+
384
+ # Auto-detect by PID if no --id
385
+ if [ -z "$target_id" ]; then
386
+ local current_pid=$PPID
387
+ local i=0
388
+ while [ "$i" -lt "$count" ]; do
389
+ local pid
390
+ pid=$(yaml_query ".sessions[${i}].pid" 2>/dev/null || echo "")
391
+ if [ "$pid" = "$current_pid" ]; then
392
+ target_id=$(yaml_query ".sessions[${i}].id" 2>/dev/null || echo "")
393
+ break
394
+ fi
395
+ i=$((i + 1))
396
+ done
397
+ fi
398
+
399
+ if [ -z "$target_id" ]; then
400
+ echo "No session found for current process (PPID: $PPID)."
401
+ return 1
402
+ fi
403
+
404
+ # Find the session index by ID
405
+ local found_idx=""
406
+ local i=0
407
+ while [ "$i" -lt "$count" ]; do
408
+ local sid
409
+ sid=$(yaml_query ".sessions[${i}].id" 2>/dev/null || echo "")
410
+ if [ "$sid" = "$target_id" ]; then
411
+ found_idx="$i"
412
+ break
413
+ fi
414
+ i=$((i + 1))
415
+ done
416
+
417
+ if [ -n "$found_idx" ]; then
418
+ write_sessions_excluding "$found_idx"
419
+ local remaining=$((count - 1))
420
+ echo "Session ${target_id} deregistered."
421
+ echo "Active sessions remaining: ${remaining}"
422
+ else
423
+ echo "Session ${target_id} not found."
424
+ return 1
425
+ fi
426
+ }
427
+
428
+ # ============================================================================
429
+ # LIST SUBCOMMAND
430
+ # ============================================================================
431
+
432
+ do_list() {
433
+ local filter_project=""
434
+
435
+ while [ $# -gt 0 ]; do
436
+ case "$1" in
437
+ --project) filter_project="$2"; shift 2 ;;
438
+ *) shift ;;
439
+ esac
440
+ done
441
+
442
+ ensure_sessions_file
443
+
444
+ # Clean stale sessions first
445
+ do_clean "false" >/dev/null 2>&1 || true
446
+
447
+ # Re-parse after clean
448
+ yaml_parse "$SESSIONS_FILE" || return 1
449
+
450
+ local count
451
+ count=$(get_session_count)
452
+
453
+ if [ "$count" = "0" ] || [ -z "$count" ]; then
454
+ echo "No active sessions."
455
+ return 0
456
+ fi
457
+
458
+ local now_epoch
459
+ now_epoch=$(get_epoch)
460
+ local current_pid=$PPID
461
+
462
+ # Count matching sessions
463
+ local total_matching=0
464
+ local i=0
465
+ while [ "$i" -lt "$count" ]; do
466
+ local proj
467
+ proj=$(yaml_query ".sessions[${i}].project" 2>/dev/null || echo "")
468
+ if [ -z "$filter_project" ] || [ "$proj" = "$filter_project" ]; then
469
+ total_matching=$((total_matching + 1))
470
+ fi
471
+ i=$((i + 1))
472
+ done
473
+
474
+ if [ "$total_matching" = "0" ]; then
475
+ if [ -n "$filter_project" ]; then
476
+ echo "No active sessions for project: ${filter_project}"
477
+ else
478
+ echo "No active sessions."
479
+ fi
480
+ return 0
481
+ fi
482
+
483
+ echo "Active Sessions (${total_matching}):"
484
+ echo ""
485
+
486
+ i=0
487
+ while [ "$i" -lt "$count" ]; do
488
+ local proj sid desc started last_activity pid status
489
+
490
+ proj=$(yaml_query ".sessions[${i}].project" 2>/dev/null || echo "")
491
+
492
+ if [ -n "$filter_project" ] && [ "$proj" != "$filter_project" ]; then
493
+ i=$((i + 1))
494
+ continue
495
+ fi
496
+
497
+ sid=$(yaml_query ".sessions[${i}].id" 2>/dev/null || echo "")
498
+ desc=$(yaml_query ".sessions[${i}].description" 2>/dev/null || echo "")
499
+ started=$(yaml_query ".sessions[${i}].started" 2>/dev/null || echo "")
500
+ last_activity=$(yaml_query ".sessions[${i}].last_activity" 2>/dev/null || echo "")
501
+ pid=$(yaml_query ".sessions[${i}].pid" 2>/dev/null || echo "")
502
+ status=$(yaml_query ".sessions[${i}].status" 2>/dev/null || echo "active")
503
+
504
+ local started_ago="" active_ago=""
505
+ if [ -n "$started" ] && [ "$started" != "null" ]; then
506
+ local started_secs=$((now_epoch - $(iso_to_epoch "$started")))
507
+ started_ago="Started $(format_relative_time "$started_secs")"
508
+ fi
509
+ if [ -n "$last_activity" ] && [ "$last_activity" != "null" ]; then
510
+ local active_secs=$((now_epoch - $(iso_to_epoch "$last_activity")))
511
+ active_ago="last active $(format_relative_time "$active_secs")"
512
+ fi
513
+
514
+ local indicator=""
515
+ if [ "$pid" = "$current_pid" ]; then
516
+ indicator=" (this session)"
517
+ fi
518
+
519
+ local status_marker=""
520
+ if [ "$status" = "idle" ]; then
521
+ status_marker=" [idle]"
522
+ fi
523
+
524
+ echo " ${sid} ${proj}${indicator}${status_marker}"
525
+ if [ -n "$desc" ] && [ "$desc" != "null" ]; then
526
+ echo " ${desc}"
527
+ fi
528
+ if [ -n "$started_ago" ] || [ -n "$active_ago" ]; then
529
+ local time_line=""
530
+ [ -n "$started_ago" ] && time_line="$started_ago"
531
+ if [ -n "$active_ago" ]; then
532
+ [ -n "$time_line" ] && time_line="${time_line}, "
533
+ time_line="${time_line}${active_ago}"
534
+ fi
535
+ echo " ${time_line}"
536
+ fi
537
+ echo ""
538
+
539
+ i=$((i + 1))
540
+ done
541
+ }
542
+
543
+ # ============================================================================
544
+ # HEARTBEAT SUBCOMMAND
545
+ # ============================================================================
546
+
547
+ do_heartbeat() {
548
+ local target_id="" new_task="" new_description=""
549
+
550
+ while [ $# -gt 0 ]; do
551
+ case "$1" in
552
+ --id) target_id="$2"; shift 2 ;;
553
+ --task) new_task="$2"; shift 2 ;;
554
+ --description) new_description="$2"; shift 2 ;;
555
+ *) shift ;;
556
+ esac
557
+ done
558
+
559
+ ensure_sessions_file
560
+ yaml_parse "$SESSIONS_FILE" || return 1
561
+
562
+ local count
563
+ count=$(get_session_count)
564
+
565
+ if [ "$count" = "0" ] || [ -z "$count" ]; then
566
+ echo "No active sessions."
567
+ return 1
568
+ fi
569
+
570
+ # Auto-detect by PID if no --id
571
+ if [ -z "$target_id" ]; then
572
+ local current_pid=$PPID
573
+ local i=0
574
+ while [ "$i" -lt "$count" ]; do
575
+ local pid
576
+ pid=$(yaml_query ".sessions[${i}].pid" 2>/dev/null || echo "")
577
+ if [ "$pid" = "$current_pid" ]; then
578
+ target_id=$(yaml_query ".sessions[${i}].id" 2>/dev/null || echo "")
579
+ break
580
+ fi
581
+ i=$((i + 1))
582
+ done
583
+ fi
584
+
585
+ if [ -z "$target_id" ]; then
586
+ echo "No session found for current process (PPID: $PPID)."
587
+ return 1
588
+ fi
589
+
590
+ # Find the session and rebuild with updated fields
591
+ local now
592
+ now=$(get_timestamp)
593
+ local found=false
594
+
595
+ local tmp_file="${SESSIONS_FILE}.tmp"
596
+ {
597
+ echo "# ~/.acp/sessions.yaml"
598
+ echo "# Managed by acp.sessions.sh — do not edit manually"
599
+ echo ""
600
+ echo "sessions:"
601
+
602
+ local i=0
603
+ while [ "$i" -lt "$count" ]; do
604
+ local id proj desc started last_activity status milestone task pid terminal remote_url
605
+ id=$(yaml_query ".sessions[${i}].id" 2>/dev/null || echo "")
606
+ proj=$(yaml_query ".sessions[${i}].project" 2>/dev/null || echo "")
607
+ desc=$(yaml_query ".sessions[${i}].description" 2>/dev/null || echo "")
608
+ started=$(yaml_query ".sessions[${i}].started" 2>/dev/null || echo "")
609
+ last_activity=$(yaml_query ".sessions[${i}].last_activity" 2>/dev/null || echo "")
610
+ status=$(yaml_query ".sessions[${i}].status" 2>/dev/null || echo "active")
611
+ milestone=$(yaml_query ".sessions[${i}].current_milestone" 2>/dev/null || echo "")
612
+ task=$(yaml_query ".sessions[${i}].current_task" 2>/dev/null || echo "")
613
+ pid=$(yaml_query ".sessions[${i}].pid" 2>/dev/null || echo "")
614
+ terminal=$(yaml_query ".sessions[${i}].terminal" 2>/dev/null || echo "")
615
+ remote_url=$(yaml_query ".sessions[${i}].remote_url" 2>/dev/null || echo "")
616
+
617
+ # Normalize nulls
618
+ [ "$milestone" = "null" ] && milestone=""
619
+ [ "$task" = "null" ] && task=""
620
+ [ "$remote_url" = "null" ] && remote_url=""
621
+ [ "$desc" = "null" ] && desc=""
622
+
623
+ # Apply updates to the target session
624
+ if [ "$id" = "$target_id" ]; then
625
+ found=true
626
+ last_activity="$now"
627
+ status="active"
628
+ [ -n "$new_task" ] && task="$new_task"
629
+ [ -n "$new_description" ] && desc="$new_description"
630
+ fi
631
+
632
+ echo " - id: ${id}"
633
+ echo " project: ${proj}"
634
+ echo " description: ${desc}"
635
+ echo " started: ${started}"
636
+ echo " last_activity: ${last_activity}"
637
+ echo " status: ${status}"
638
+ echo " current_milestone: ${milestone}"
639
+ echo " current_task: ${task}"
640
+ echo " pid: ${pid}"
641
+ echo " terminal: ${terminal}"
642
+ echo " remote_url: ${remote_url}"
643
+
644
+ i=$((i + 1))
645
+ done
646
+
647
+ echo ""
648
+ echo "last_updated: ${now}"
649
+ } > "$tmp_file"
650
+
651
+ if [ "$found" = "true" ]; then
652
+ mv "$tmp_file" "$SESSIONS_FILE"
653
+ echo "Session ${target_id} updated."
654
+ return 0
655
+ else
656
+ rm -f "$tmp_file"
657
+ echo "Session ${target_id} not found."
658
+ return 1
659
+ fi
660
+ }
661
+
662
+ # ============================================================================
663
+ # COUNT SUBCOMMAND
664
+ # ============================================================================
665
+
666
+ do_count() {
667
+ ensure_sessions_file
668
+
669
+ # Clean stale sessions first
670
+ do_clean "false" >/dev/null 2>&1 || true
671
+
672
+ # Re-parse after clean
673
+ yaml_parse "$SESSIONS_FILE" || return 1
674
+
675
+ local count
676
+ count=$(get_session_count)
677
+ echo "${count:-0}"
678
+ }
679
+
680
+ # ============================================================================
681
+ # MAIN DISPATCH
682
+ # ============================================================================
683
+
684
+ main() {
685
+ local subcommand="${1:-}"
686
+
687
+ if [ -z "$subcommand" ]; then
688
+ echo "Usage: $0 <subcommand> [options]"
689
+ echo ""
690
+ echo "Subcommands:"
691
+ echo " register Register a new session"
692
+ echo " deregister Remove a session"
693
+ echo " list List active sessions"
694
+ echo " clean Remove stale sessions"
695
+ echo " heartbeat Update session activity"
696
+ echo " count Output count of active sessions"
697
+ echo ""
698
+ echo "Options:"
699
+ echo " --project <name> Project name (register, list)"
700
+ echo " --description <desc> Session description (register, heartbeat)"
701
+ echo " --remote-url <url> Remote session URL (register)"
702
+ echo " --id <session-id> Target session (deregister, heartbeat)"
703
+ echo " --task <task-id> Current task (heartbeat)"
704
+ echo " --pid <pid> Explicit PID for stale detection (register)"
705
+ return 1
706
+ fi
707
+
708
+ shift
709
+
710
+ case "$subcommand" in
711
+ register) do_register "$@" ;;
712
+ deregister) do_deregister "$@" ;;
713
+ list) do_list "$@" ;;
714
+ clean) do_clean "true" ;;
715
+ heartbeat) do_heartbeat "$@" ;;
716
+ count) do_count ;;
717
+ *)
718
+ echo "Error: Unknown subcommand '${subcommand}'" >&2
719
+ echo "Run '$0' without arguments for usage." >&2
720
+ return 1
721
+ ;;
722
+ esac
723
+ }
724
+
725
+ main "$@"