@prmichaelsen/remember-mcp 2.7.11 → 3.0.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 (101) hide show
  1. package/.env.example +6 -0
  2. package/AGENT.md +224 -21
  3. package/CHANGELOG.md +155 -915
  4. package/README.md +130 -1
  5. package/agent/commands/acp.command-create.md +372 -0
  6. package/agent/commands/acp.design-create.md +224 -0
  7. package/agent/commands/acp.init.md +39 -5
  8. package/agent/commands/acp.package-create.md +894 -0
  9. package/agent/commands/acp.package-info.md +211 -0
  10. package/agent/commands/acp.package-install.md +206 -33
  11. package/agent/commands/acp.package-list.md +279 -0
  12. package/agent/commands/acp.package-publish.md +540 -0
  13. package/agent/commands/acp.package-remove.md +292 -0
  14. package/agent/commands/acp.package-search.md +306 -0
  15. package/agent/commands/acp.package-update.md +360 -0
  16. package/agent/commands/acp.package-validate.md +539 -0
  17. package/agent/commands/acp.pattern-create.md +326 -0
  18. package/agent/commands/acp.plan.md +552 -0
  19. package/agent/commands/acp.proceed.md +111 -86
  20. package/agent/commands/acp.project-create.md +672 -0
  21. package/agent/commands/acp.project-list.md +224 -0
  22. package/agent/commands/acp.project-set.md +226 -0
  23. package/agent/commands/acp.report.md +2 -0
  24. package/agent/commands/acp.resume.md +237 -0
  25. package/agent/commands/acp.sync.md +55 -15
  26. package/agent/commands/acp.task-create.md +390 -0
  27. package/agent/commands/acp.validate.md +61 -10
  28. package/agent/commands/acp.version-check-for-updates.md +5 -5
  29. package/agent/commands/acp.version-check.md +6 -6
  30. package/agent/commands/acp.version-update.md +6 -6
  31. package/agent/commands/command.template.md +43 -0
  32. package/agent/commands/git.commit.md +5 -3
  33. package/agent/design/soft-delete-system.md +291 -0
  34. package/agent/manifest.template.yaml +13 -0
  35. package/agent/milestones/milestone-13-soft-delete-system.md +306 -0
  36. package/agent/package.template.yaml +36 -0
  37. package/agent/progress.template.yaml +3 -0
  38. package/agent/progress.yaml +238 -6
  39. package/agent/scripts/acp.common.sh +1536 -0
  40. package/agent/scripts/{install.sh → acp.install.sh} +82 -26
  41. package/agent/scripts/acp.package-create.sh +925 -0
  42. package/agent/scripts/acp.package-info.sh +270 -0
  43. package/agent/scripts/acp.package-install.sh +596 -0
  44. package/agent/scripts/acp.package-list.sh +263 -0
  45. package/agent/scripts/acp.package-publish.sh +420 -0
  46. package/agent/scripts/acp.package-remove.sh +272 -0
  47. package/agent/scripts/acp.package-search.sh +156 -0
  48. package/agent/scripts/acp.package-update.sh +438 -0
  49. package/agent/scripts/acp.package-validate.sh +855 -0
  50. package/agent/scripts/acp.project-list.sh +121 -0
  51. package/agent/scripts/acp.project-set.sh +138 -0
  52. package/agent/scripts/{uninstall.sh → acp.uninstall.sh} +25 -15
  53. package/agent/scripts/{check-for-updates.sh → acp.version-check-for-updates.sh} +24 -14
  54. package/agent/scripts/{version.sh → acp.version-check.sh} +20 -8
  55. package/agent/scripts/{update.sh → acp.version-update.sh} +44 -25
  56. package/agent/scripts/acp.yaml-parser.sh +853 -0
  57. package/agent/scripts/acp.yaml-validate.sh +205 -0
  58. package/agent/tasks/task-68-fix-missing-space-properties.md +192 -0
  59. package/agent/tasks/task-69-add-comprehensive-tool-debugging.md +454 -0
  60. package/agent/tasks/task-70-add-soft-delete-schema-fields.md +165 -0
  61. package/agent/tasks/task-71-implement-delete-confirmation-flow.md +257 -0
  62. package/agent/tasks/task-72-add-deleted-filter-to-search-tools.md +18 -0
  63. package/agent/tasks/task-73-update-relationship-handling.md +18 -0
  64. package/agent/tasks/task-74-add-unit-tests-soft-delete.md +18 -0
  65. package/agent/tasks/task-75-update-documentation-changelog.md +26 -0
  66. package/dist/config.d.ts +18 -0
  67. package/dist/server-factory.js +788 -355
  68. package/dist/server.js +788 -355
  69. package/dist/tools/delete-memory.d.ts +5 -30
  70. package/dist/tools/find-similar.d.ts +8 -1
  71. package/dist/tools/query-memory.d.ts +8 -1
  72. package/dist/tools/search-memory.d.ts +6 -0
  73. package/dist/tools/search-relationship.d.ts +8 -1
  74. package/dist/types/memory.d.ts +8 -0
  75. package/dist/types/space-memory.d.ts +3 -0
  76. package/dist/utils/debug.d.ts +52 -0
  77. package/dist/utils/debug.spec.d.ts +5 -0
  78. package/dist/utils/weaviate-filters.d.ts +19 -0
  79. package/dist/weaviate/client.d.ts +1 -1
  80. package/package.json +1 -1
  81. package/src/config.ts +33 -0
  82. package/src/tools/confirm.ts +113 -8
  83. package/src/tools/create-relationship.ts +14 -1
  84. package/src/tools/delete-memory.ts +91 -63
  85. package/src/tools/find-similar.ts +30 -5
  86. package/src/tools/publish.ts +19 -1
  87. package/src/tools/query-memory.ts +18 -5
  88. package/src/tools/query-space.ts +36 -3
  89. package/src/tools/search-memory.ts +18 -5
  90. package/src/tools/search-relationship.ts +19 -5
  91. package/src/tools/search-space.ts +36 -3
  92. package/src/tools/update-memory.ts +8 -0
  93. package/src/types/memory.ts +11 -0
  94. package/src/types/space-memory.ts +5 -0
  95. package/src/utils/debug.spec.ts +257 -0
  96. package/src/utils/debug.ts +138 -0
  97. package/src/utils/weaviate-filters.ts +28 -1
  98. package/src/weaviate/client.ts +47 -3
  99. package/src/weaviate/schema.ts +17 -0
  100. package/src/weaviate/space-schema.spec.ts +5 -2
  101. package/src/weaviate/space-schema.ts +17 -5
@@ -0,0 +1,853 @@
1
+ #!/bin/bash
2
+ # Generic YAML Parser with AST
3
+ # Pure POSIX shell implementation
4
+ # Version: 1.0.0
5
+ # Created: 2026-02-21
6
+
7
+ # ============================================================================
8
+ # GLOBAL STATE
9
+ # ============================================================================
10
+
11
+ # Prevent variable reset on re-sourcing
12
+ if [ -z "${YAML_PARSER_LOADED:-}" ]; then
13
+ YAML_PARSER_LOADED=1
14
+ AST_FILE=""
15
+ AST_ROOT_ID=0
16
+ YAML_CURRENT_FILE=""
17
+ fi
18
+
19
+ # ============================================================================
20
+ # UTILITY FUNCTIONS
21
+ # ============================================================================
22
+
23
+ init_ast() {
24
+ AST_FILE=$(mktemp)
25
+ echo "0|map||root|-1|" > "$AST_FILE"
26
+ AST_ROOT_ID=0
27
+ }
28
+
29
+ cleanup_ast() {
30
+ if [ -n "$AST_FILE" ] && [ -f "$AST_FILE" ]; then
31
+ rm -f "$AST_FILE"
32
+ fi
33
+ }
34
+
35
+ get_next_node_id() {
36
+ wc -l < "$AST_FILE"
37
+ }
38
+
39
+ create_node() {
40
+ local type="$1"
41
+ local key="$2"
42
+ local value="$3"
43
+ local parent_id="$4"
44
+
45
+ key=$(echo "$key" | sed 's/|/\\|/g')
46
+ value=$(echo "$value" | sed 's/|/\\|/g')
47
+
48
+ local node_id
49
+ node_id=$(get_next_node_id)
50
+
51
+ echo "$node_id|$type|$key|$value|$parent_id|" >> "$AST_FILE"
52
+ echo "$node_id"
53
+ }
54
+
55
+ get_node() {
56
+ local node_id="$1"
57
+ sed -n "$((node_id + 1))p" "$AST_FILE"
58
+ }
59
+
60
+ get_node_field() {
61
+ local node_id="$1"
62
+ local field_num="$2"
63
+ get_node "$node_id" | cut -d'|' -f"$field_num"
64
+ }
65
+
66
+ add_child() {
67
+ local parent_id="$1"
68
+ local child_id="$2"
69
+
70
+ local node
71
+ node=$(get_node "$parent_id")
72
+
73
+ local id type key value parent children
74
+ id=$(echo "$node" | cut -d'|' -f1)
75
+ type=$(echo "$node" | cut -d'|' -f2)
76
+ key=$(echo "$node" | cut -d'|' -f3)
77
+ value=$(echo "$node" | cut -d'|' -f4)
78
+ parent=$(echo "$node" | cut -d'|' -f5)
79
+ children=$(echo "$node" | cut -d'|' -f6)
80
+
81
+ if [ -z "$children" ]; then
82
+ children="$child_id"
83
+ else
84
+ children="$children,$child_id"
85
+ fi
86
+
87
+ local updated="$id|$type|$key|$value|$parent|$children"
88
+ sed -i "$((parent_id + 1))s@.*@$updated@" "$AST_FILE"
89
+ }
90
+
91
+ update_node_type() {
92
+ local node_id="$1"
93
+ local new_type="$2"
94
+
95
+ local node
96
+ node=$(get_node "$node_id")
97
+
98
+ local id type key value parent children
99
+ id=$(echo "$node" | cut -d'|' -f1)
100
+ key=$(echo "$node" | cut -d'|' -f3)
101
+ value=$(echo "$node" | cut -d'|' -f4)
102
+ parent=$(echo "$node" | cut -d'|' -f5)
103
+ children=$(echo "$node" | cut -d'|' -f6)
104
+
105
+ local updated="$id|$new_type|$key|$value|$parent|$children"
106
+ sed -i "$((node_id + 1))s@.*@$updated@" "$AST_FILE"
107
+ }
108
+
109
+ # ============================================================================
110
+ # PARSER
111
+ # ============================================================================
112
+
113
+ yaml_parse() {
114
+ local file="$1"
115
+
116
+ if [ ! -f "$file" ]; then
117
+ echo "Error: File not found: $file" >&2
118
+ return 1
119
+ fi
120
+
121
+ cleanup_ast
122
+ init_ast
123
+ YAML_CURRENT_FILE="$file"
124
+
125
+ # State tracking
126
+ local parent_stack="0"
127
+ local indent_stack="-1"
128
+ local current_parent=0
129
+ local prev_indent=-1
130
+ local last_key_node=-1
131
+
132
+ while IFS= read -r line || [ -n "$line" ]; do
133
+ # Skip empty lines
134
+ [ -z "$line" ] && continue
135
+
136
+ # Skip comment lines
137
+ case "$line" in \#*) continue ;; esac
138
+
139
+ # Strip inline comments
140
+ line=$(echo "$line" | sed 's/#.*$//')
141
+
142
+ # Calculate indentation
143
+ local indent=0
144
+ local trimmed="$line"
145
+ while [ "$trimmed" != "${trimmed# }" ]; do
146
+ indent=$((indent + 1))
147
+ trimmed="${trimmed# }"
148
+ done
149
+
150
+ # Skip empty after trim
151
+ [ -z "$trimmed" ] && continue
152
+
153
+ # Handle dedent - pop stack
154
+ while [ "$prev_indent" -ge 0 ] && [ "$indent" -le "$prev_indent" ]; do
155
+ # Pop one level
156
+ parent_stack=$(echo "$parent_stack" | sed 's/,[^,]*$//')
157
+ indent_stack=$(echo "$indent_stack" | sed 's/,[^,]*$//')
158
+
159
+ # Get new current parent
160
+ current_parent=$(echo "$parent_stack" | awk -F',' '{print $NF}')
161
+ prev_indent=$(echo "$indent_stack" | awk -F',' '{print $NF}')
162
+
163
+ # Handle empty stack
164
+ [ -z "$current_parent" ] && current_parent=0
165
+ [ -z "$prev_indent" ] && prev_indent=-1
166
+
167
+ last_key_node=-1
168
+ done
169
+
170
+ # Parse line content
171
+ if echo "$trimmed" | grep -q '^-[[:space:]]'; then
172
+ # Array item
173
+ local item_content
174
+ item_content=$(echo "$trimmed" | sed 's/^-[[:space:]]*//')
175
+
176
+ # Convert last key node to array if needed
177
+ if [ "$last_key_node" -ge 0 ]; then
178
+ update_node_type "$last_key_node" "array"
179
+ current_parent="$last_key_node"
180
+ last_key_node=-1
181
+ fi
182
+
183
+ # Check if inline object (has colon on same line)
184
+ if echo "$item_content" | grep -q ':'; then
185
+ # Inline object: - name: value
186
+ local obj_node
187
+ obj_node=$(create_node "map" "" "" "$current_parent")
188
+ add_child "$current_parent" "$obj_node"
189
+
190
+ # Parse first field
191
+ local key value
192
+ key=$(echo "$item_content" | cut -d':' -f1 | sed 's/[[:space:]]*$//')
193
+ value=$(echo "$item_content" | cut -d':' -f2- | sed 's/^[[:space:]]*//')
194
+
195
+ local field_node
196
+ field_node=$(create_node "scalar" "$key" "$value" "$obj_node")
197
+ add_child "$obj_node" "$field_node"
198
+
199
+ # Push object onto stack for potential additional fields
200
+ parent_stack="$parent_stack,$obj_node"
201
+ indent_stack="$indent_stack,$indent"
202
+ current_parent="$obj_node"
203
+ prev_indent="$indent"
204
+ else
205
+ # Simple array item: - value
206
+ local item_node
207
+ item_node=$(create_node "scalar" "" "$item_content" "$current_parent")
208
+ add_child "$current_parent" "$item_node"
209
+ fi
210
+ elif echo "$trimmed" | grep -q ':'; then
211
+ # Key-value pair
212
+ local key value
213
+ key=$(echo "$trimmed" | cut -d':' -f1 | sed 's/[[:space:]]*$//')
214
+ value=$(echo "$trimmed" | cut -d':' -f2- | sed 's/^[[:space:]]*//')
215
+
216
+ if [ -z "$value" ]; then
217
+ # Key with no value - map or array follows
218
+ local node_id
219
+ node_id=$(create_node "map" "$key" "" "$current_parent")
220
+ add_child "$current_parent" "$node_id"
221
+
222
+ # Push onto stack
223
+ parent_stack="$parent_stack,$node_id"
224
+ indent_stack="$indent_stack,$indent"
225
+ current_parent="$node_id"
226
+ prev_indent="$indent"
227
+ last_key_node="$node_id"
228
+ else
229
+ # Scalar value
230
+ local node_id
231
+ node_id=$(create_node "scalar" "$key" "$value" "$current_parent")
232
+ add_child "$current_parent" "$node_id"
233
+ fi
234
+ fi
235
+ done < "$file"
236
+
237
+ return 0
238
+ }
239
+
240
+ # ============================================================================
241
+ # QUERY ENGINE
242
+ # ============================================================================
243
+
244
+ find_child_by_key() {
245
+ local parent_id="$1"
246
+ local key="$2"
247
+
248
+ local children
249
+ children=$(get_node_field "$parent_id" 6)
250
+
251
+ [ -z "$children" ] && return 1
252
+
253
+ local IFS=','
254
+ for child_id in $children; do
255
+ local child_key
256
+ child_key=$(get_node_field "$child_id" 3)
257
+
258
+ if [ "$child_key" = "$key" ]; then
259
+ echo "$child_id"
260
+ return 0
261
+ fi
262
+ done
263
+
264
+ return 1
265
+ }
266
+
267
+ find_child_by_index() {
268
+ local parent_id="$1"
269
+ local index="$2"
270
+
271
+ local children
272
+ children=$(get_node_field "$parent_id" 6)
273
+
274
+ [ -z "$children" ] && return 1
275
+
276
+ local child_id
277
+ child_id=$(echo "$children" | tr ',' '\n' | sed -n "$((index + 1))p")
278
+
279
+ if [ -n "$child_id" ]; then
280
+ echo "$child_id"
281
+ return 0
282
+ fi
283
+
284
+ return 1
285
+ }
286
+
287
+ yaml_query() {
288
+ local path="$1"
289
+
290
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
291
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
292
+ return 1
293
+ fi
294
+
295
+ path=$(echo "$path" | sed 's/^\.//')
296
+
297
+ local current_node="$AST_ROOT_ID"
298
+
299
+ local IFS='.'
300
+ for segment in $path; do
301
+ if echo "$segment" | grep -q '\['; then
302
+ local key index
303
+ key=$(echo "$segment" | sed 's/\[.*//')
304
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
305
+
306
+ current_node=$(find_child_by_key "$current_node" "$key")
307
+ [ -z "$current_node" ] && return 1
308
+
309
+ current_node=$(find_child_by_index "$current_node" "$index")
310
+ [ -z "$current_node" ] && return 1
311
+ else
312
+ current_node=$(find_child_by_key "$current_node" "$segment")
313
+ [ -z "$current_node" ] && return 1
314
+ fi
315
+ done
316
+
317
+ # Check node type
318
+ local node_type
319
+ node_type=$(get_node_field "$current_node" 2)
320
+
321
+ # For map or array nodes, return children keys in YAML format
322
+ if [ "$node_type" = "map" ] || [ "$node_type" = "array" ]; then
323
+ local children
324
+ children=$(get_node_field "$current_node" 6)
325
+
326
+ if [ -n "$children" ]; then
327
+ # Split by comma and iterate
328
+ local IFS=','
329
+ for child_id in $children; do
330
+ local child_key
331
+ child_key=$(get_node_field "$child_id" 3)
332
+ echo "${child_key}:"
333
+ done
334
+ fi
335
+ else
336
+ # For scalar nodes, return the value
337
+ get_node_field "$current_node" 4
338
+ fi
339
+ }
340
+
341
+ # Create a new node in the AST (used by yaml_set for auto-creation)
342
+ # Usage: node_id=$(create_node_and_link "type" "key" "value" "parent_id")
343
+ # Returns: new node ID
344
+ # NOTE: This version adds the node as a child of parent (for yaml_set)
345
+ create_node_and_link() {
346
+ local type="$1"
347
+ local key="$2"
348
+ local value="$3"
349
+ local parent_id="$4"
350
+
351
+ # Get next node ID
352
+ local next_id
353
+ next_id=$(wc -l < "$AST_FILE")
354
+
355
+ # Create node: id|type|key|value|parent|children
356
+ echo "${next_id}|${type}|${key}|${value}|${parent_id}|" >> "$AST_FILE"
357
+
358
+ # Add this node to parent's children list
359
+ if [ "$parent_id" != "-1" ]; then
360
+ # Read current parent node
361
+ local parent_line
362
+ parent_line=$(sed -n "$((parent_id + 1))p" "$AST_FILE")
363
+
364
+ # Extract parent fields
365
+ local parent_children
366
+ parent_children=$(echo "$parent_line" | cut -d'|' -f6)
367
+
368
+ # Append new child ID
369
+ if [ -z "$parent_children" ]; then
370
+ parent_children="$next_id"
371
+ else
372
+ parent_children="${parent_children},${next_id}"
373
+ fi
374
+
375
+ # Update parent node with new children list
376
+ local parent_prefix
377
+ parent_prefix=$(echo "$parent_line" | cut -d'|' -f1-5)
378
+ sed -i "$((parent_id + 1))s@.*@${parent_prefix}|${parent_children}@" "$AST_FILE"
379
+ fi
380
+
381
+ echo "$next_id"
382
+ }
383
+
384
+ # Original create_node for backward compatibility (does NOT link to parent)
385
+ create_node() {
386
+ local type="$1"
387
+ local key="$2"
388
+ local value="$3"
389
+ local parent_id="$4"
390
+
391
+ # Get next node ID
392
+ local next_id
393
+ next_id=$(wc -l < "$AST_FILE")
394
+
395
+ # Create node: id|type|key|value|parent|children
396
+ echo "${next_id}|${type}|${key}|${value}|${parent_id}|" >> "$AST_FILE"
397
+
398
+ echo "$next_id"
399
+ }
400
+
401
+ yaml_set() {
402
+ local path="$1"
403
+ local new_value="$2"
404
+
405
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
406
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
407
+ return 1
408
+ fi
409
+
410
+ path=$(echo "$path" | sed 's/^\.//')
411
+
412
+ local current_node="$AST_ROOT_ID"
413
+ local IFS='.'
414
+ local segments=($path)
415
+ local last_index=$((${#segments[@]} - 1))
416
+
417
+ # Traverse path, creating missing nodes
418
+ local i=0
419
+ for segment in "${segments[@]}"; do
420
+ local is_last=$((i == last_index))
421
+
422
+ if echo "$segment" | grep -q '\['; then
423
+ local key index
424
+ key=$(echo "$segment" | sed 's/\[.*//')
425
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
426
+
427
+ local child_node
428
+ child_node=$(find_child_by_key "$current_node" "$key")
429
+ if [ -z "$child_node" ]; then
430
+ # Create missing array node
431
+ child_node=$(create_node_and_link "array" "$key" "" "$current_node")
432
+ fi
433
+ current_node="$child_node"
434
+
435
+ child_node=$(find_child_by_index "$current_node" "$index")
436
+ if [ -z "$child_node" ]; then
437
+ echo "Error: Cannot create array index $index (not supported yet)" >&2
438
+ return 1
439
+ fi
440
+ current_node="$child_node"
441
+ else
442
+ local child_node
443
+ child_node=$(find_child_by_key "$current_node" "$segment")
444
+
445
+ if [ -z "$child_node" ]; then
446
+ # Create missing node
447
+ if [ "$is_last" -eq 1 ]; then
448
+ # Last segment - create scalar with value
449
+ child_node=$(create_node_and_link "scalar" "$segment" "$new_value" "$current_node")
450
+ return 0
451
+ else
452
+ # Intermediate segment - create map
453
+ child_node=$(create_node_and_link "map" "$segment" "" "$current_node")
454
+ fi
455
+ fi
456
+ current_node="$child_node"
457
+ fi
458
+
459
+ i=$((i + 1))
460
+ done
461
+
462
+ # Update existing node value
463
+ local node
464
+ node=$(get_node "$current_node")
465
+
466
+ local id type key value parent children
467
+ id=$(echo "$node" | cut -d'|' -f1)
468
+ type=$(echo "$node" | cut -d'|' -f2)
469
+ key=$(echo "$node" | cut -d'|' -f3)
470
+ parent=$(echo "$node" | cut -d'|' -f5)
471
+ children=$(echo "$node" | cut -d'|' -f6)
472
+
473
+ new_value=$(echo "$new_value" | sed 's/|/\\|/g')
474
+
475
+ local updated="$id|$type|$key|$new_value|$parent|$children"
476
+ sed -i "$((current_node + 1))s@.*@$updated@" "$AST_FILE"
477
+ }
478
+
479
+ yaml_write() {
480
+ local output_file="$1"
481
+
482
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
483
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
484
+ return 1
485
+ fi
486
+
487
+ serialize_node "$AST_ROOT_ID" 0 > "$output_file"
488
+ }
489
+
490
+ serialize_node() {
491
+ local node_id="$1"
492
+ local indent_level="$2"
493
+ local parent_type="${3:-}"
494
+
495
+ local node
496
+ node=$(get_node "$node_id")
497
+
498
+ local type key value children parent_id
499
+ type=$(echo "$node" | cut -d'|' -f2)
500
+ key=$(echo "$node" | cut -d'|' -f3)
501
+ value=$(echo "$node" | cut -d'|' -f4)
502
+ parent_id=$(echo "$node" | cut -d'|' -f5)
503
+ children=$(echo "$node" | cut -d'|' -f6)
504
+
505
+ # Determine parent type if not provided
506
+ if [ -z "$parent_type" ] && [ "$parent_id" -ge 0 ]; then
507
+ parent_type=$(get_node_field "$parent_id" 2)
508
+ fi
509
+
510
+ local indent=""
511
+ local i=0
512
+ while [ "$i" -lt "$indent_level" ]; do
513
+ indent="$indent "
514
+ i=$((i + 1))
515
+ done
516
+
517
+ case "$type" in
518
+ scalar)
519
+ if [ -n "$key" ]; then
520
+ echo "$indent$key: $value"
521
+ else
522
+ echo "$indent- $value"
523
+ fi
524
+ ;;
525
+
526
+ map)
527
+ # If this map is in an array, first child gets dash prefix
528
+ local is_first_child=true
529
+
530
+ if [ "$node_id" -ne 0 ] && [ -n "$key" ]; then
531
+ echo "$indent$key:"
532
+ fi
533
+
534
+ if [ -n "$children" ]; then
535
+ local IFS=','
536
+ local next_indent
537
+ # Root node (id=0) doesn't add indentation
538
+ if [ "$node_id" -eq 0 ]; then
539
+ next_indent="$indent_level"
540
+ else
541
+ next_indent="$((indent_level + 1))"
542
+ fi
543
+
544
+ for child_id in $children; do
545
+ # If parent is array and this is first child, use dash
546
+ if [ "$parent_type" = "array" ] && [ "$is_first_child" = true ]; then
547
+ # Serialize first field with dash
548
+ local child_node
549
+ child_node=$(get_node "$child_id")
550
+ local child_type child_key child_value
551
+ child_type=$(echo "$child_node" | cut -d'|' -f2)
552
+ child_key=$(echo "$child_node" | cut -d'|' -f3)
553
+ child_value=$(echo "$child_node" | cut -d'|' -f4)
554
+
555
+ if [ "$child_type" = "scalar" ] && [ -n "$child_key" ]; then
556
+ echo "$indent- $child_key: $child_value"
557
+ fi
558
+ is_first_child=false
559
+ else
560
+ serialize_node "$child_id" "$next_indent" "$type"
561
+ fi
562
+ done
563
+ fi
564
+ ;;
565
+
566
+ array)
567
+ if [ -n "$key" ]; then
568
+ echo "$indent$key:"
569
+ fi
570
+
571
+ if [ -n "$children" ]; then
572
+ local IFS=','
573
+ # Array children need to be indented
574
+ for child_id in $children; do
575
+ serialize_node "$child_id" "$((indent_level + 1))" "array"
576
+ done
577
+ fi
578
+ ;;
579
+ esac
580
+ }
581
+
582
+ # ============================================================================
583
+ # BACKWARD COMPATIBILITY
584
+ # ============================================================================
585
+
586
+ yaml_get() {
587
+ local file="$1"
588
+ local key="$2"
589
+
590
+ if [ "$YAML_CURRENT_FILE" != "$file" ]; then
591
+ yaml_parse "$file" || return 1
592
+ fi
593
+
594
+ yaml_query ".$key"
595
+ }
596
+
597
+ yaml_get_nested() {
598
+ local file="$1"
599
+ local path="$2"
600
+
601
+ if [ "$YAML_CURRENT_FILE" != "$file" ]; then
602
+ yaml_parse "$file" || return 1
603
+ fi
604
+
605
+ yaml_query ".$path"
606
+ }
607
+
608
+ # Check if key exists (checks if node exists, not if it has a value)
609
+ yaml_has_key() {
610
+ local file="$1"
611
+ local key="$2"
612
+
613
+ if [ "$YAML_CURRENT_FILE" != "$file" ]; then
614
+ yaml_parse "$file" || return 1
615
+ fi
616
+
617
+ # Try to find the node (returns empty string on failure, but exit code tells us)
618
+ path=$(echo "$key" | sed 's/^\.//')
619
+ local current_node="$AST_ROOT_ID"
620
+
621
+ local IFS='.'
622
+ for segment in $path; do
623
+ if echo "$segment" | grep -q '\['; then
624
+ local k index
625
+ k=$(echo "$segment" | sed 's/\[.*//')
626
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
627
+
628
+ current_node=$(find_child_by_key "$current_node" "$k" 2>/dev/null)
629
+ [ -z "$current_node" ] && return 1
630
+
631
+ current_node=$(find_child_by_index "$current_node" "$index" 2>/dev/null)
632
+ [ -z "$current_node" ] && return 1
633
+ else
634
+ current_node=$(find_child_by_key "$current_node" "$segment" 2>/dev/null)
635
+ [ -z "$current_node" ] && return 1
636
+ fi
637
+ done
638
+
639
+ # Node exists
640
+ return 0
641
+ }
642
+
643
+ # Get array count (for object arrays)
644
+ # Usage: yaml_get_array file.yaml "contents.commands"
645
+ # Returns: count of array elements
646
+ yaml_get_array() {
647
+ local file="$1"
648
+ local path="$2"
649
+
650
+ if [ "$YAML_CURRENT_FILE" != "$file" ]; then
651
+ yaml_parse "$file" || return 1
652
+ fi
653
+
654
+ # Find the array node
655
+ path=$(echo "$path" | sed 's/^\.//')
656
+ local current_node="$AST_ROOT_ID"
657
+
658
+ local IFS='.'
659
+ for segment in $path; do
660
+ current_node=$(find_child_by_key "$current_node" "$segment")
661
+ [ -z "$current_node" ] && return 1
662
+ done
663
+
664
+ # Get children count
665
+ local children
666
+ children=$(get_node_field "$current_node" 6)
667
+
668
+ if [ -z "$children" ]; then
669
+ echo "0"
670
+ else
671
+ echo "$children" | tr ',' '\n' | wc -l
672
+ fi
673
+ }
674
+
675
+ # Append scalar item to array
676
+ # Usage: yaml_array_append ".path.to.array" "value"
677
+ # Returns: node_id of new item
678
+ yaml_array_append() {
679
+ local path="$1"
680
+ local value="$2"
681
+
682
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
683
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
684
+ return 1
685
+ fi
686
+
687
+ # Find the array node
688
+ path=$(echo "$path" | sed 's/^\.//')
689
+ local current_node="$AST_ROOT_ID"
690
+
691
+ local IFS='.'
692
+ for segment in $path; do
693
+ if echo "$segment" | grep -q '\['; then
694
+ local key index
695
+ key=$(echo "$segment" | sed 's/\[.*//')
696
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
697
+
698
+ current_node=$(find_child_by_key "$current_node" "$key")
699
+ [ -z "$current_node" ] && return 1
700
+
701
+ current_node=$(find_child_by_index "$current_node" "$index")
702
+ [ -z "$current_node" ] && return 1
703
+ else
704
+ current_node=$(find_child_by_key "$current_node" "$segment")
705
+ [ -z "$current_node" ] && return 1
706
+ fi
707
+ done
708
+
709
+ # Verify it's an array
710
+ local node_type
711
+ node_type=$(get_node_field "$current_node" 2)
712
+
713
+ if [ "$node_type" != "array" ]; then
714
+ echo "Error: Path does not point to an array" >&2
715
+ return 1
716
+ fi
717
+
718
+ # Create new scalar node
719
+ local new_node
720
+ new_node=$(create_node "scalar" "" "$value" "$current_node")
721
+
722
+ # Add as child
723
+ add_child "$current_node" "$new_node"
724
+
725
+ echo "$new_node"
726
+ }
727
+
728
+ # Append object to array
729
+ # Usage: yaml_array_append_object ".path.to.array"
730
+ # Returns: node_id of new object (use yaml_object_set to add fields)
731
+ yaml_array_append_object() {
732
+ local path="$1"
733
+
734
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
735
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
736
+ return 1
737
+ fi
738
+
739
+ # Find the node
740
+ path=$(echo "$path" | sed 's/^\.//')
741
+ local current_node="$AST_ROOT_ID"
742
+
743
+ local IFS='.'
744
+ for segment in $path; do
745
+ if echo "$segment" | grep -q '\['; then
746
+ local key index
747
+ key=$(echo "$segment" | sed 's/\[.*//')
748
+ index=$(echo "$segment" | sed 's/.*\[\([0-9]*\)\].*/\1/')
749
+
750
+ current_node=$(find_child_by_key "$current_node" "$key")
751
+ [ -z "$current_node" ] && return 1
752
+
753
+ current_node=$(find_child_by_index "$current_node" "$index")
754
+ [ -z "$current_node" ] && return 1
755
+ else
756
+ current_node=$(find_child_by_key "$current_node" "$segment")
757
+ [ -z "$current_node" ] && return 1
758
+ fi
759
+ done
760
+
761
+ # Check node type
762
+ local node_type
763
+ node_type=$(get_node_field "$current_node" 2)
764
+
765
+ # If it's a map with no children, convert to array
766
+ if [ "$node_type" = "map" ]; then
767
+ local children
768
+ children=$(get_node_field "$current_node" 6)
769
+ if [ -z "$children" ]; then
770
+ # Empty map - convert to array
771
+ update_node_type "$current_node" "array"
772
+ else
773
+ echo "Error: Path points to non-empty map, not array" >&2
774
+ return 1
775
+ fi
776
+ elif [ "$node_type" != "array" ]; then
777
+ echo "Error: Path does not point to an array" >&2
778
+ return 1
779
+ fi
780
+
781
+ # Create new map node (object)
782
+ local new_node
783
+ new_node=$(create_node "map" "" "" "$current_node")
784
+
785
+ # Add as child
786
+ add_child "$current_node" "$new_node"
787
+
788
+ echo "$new_node"
789
+ }
790
+
791
+ # Set field on object (for building objects in arrays)
792
+ # Usage: yaml_object_set node_id "field_name" "value"
793
+ yaml_object_set() {
794
+ local object_node="$1"
795
+ local field_name="$2"
796
+ local field_value="$3"
797
+
798
+ if [ -z "$AST_FILE" ] || [ ! -f "$AST_FILE" ]; then
799
+ echo "Error: No AST loaded. Call yaml_parse first." >&2
800
+ return 1
801
+ fi
802
+
803
+ # Create scalar field
804
+ local field_node
805
+ field_node=$(create_node "scalar" "$field_name" "$field_value" "$object_node")
806
+
807
+ # Add as child
808
+ add_child "$object_node" "$field_node"
809
+
810
+ echo "$field_node"
811
+ }
812
+
813
+ # ============================================================================
814
+ # MAIN
815
+ # ============================================================================
816
+
817
+ trap cleanup_ast EXIT INT TERM
818
+
819
+ # Only run main if script is executed directly (not sourced)
820
+ if [ -n "${1:-}" ] && [ "${1:-}" != "-" ] && [ "${BASH_SOURCE[0]}" = "${0}" ]; then
821
+ case "$1" in
822
+ parse)
823
+ yaml_parse "$2"
824
+ echo "✓ Parsed $2 ($(get_next_node_id) nodes)"
825
+ ;;
826
+ query)
827
+ yaml_parse "$2"
828
+ yaml_query "$3"
829
+ ;;
830
+ set)
831
+ yaml_parse "$2"
832
+ yaml_set "$3" "$4"
833
+ yaml_write "$2"
834
+ echo "✓ Updated $2"
835
+ ;;
836
+ debug)
837
+ yaml_parse "$2"
838
+ echo "AST Contents:"
839
+ cat "$AST_FILE"
840
+ ;;
841
+ *)
842
+ echo "Usage: $0 {parse|query|set|debug} file.yaml [path] [value]"
843
+ echo ""
844
+ echo "Examples:"
845
+ echo " $0 parse file.yaml"
846
+ echo " $0 query file.yaml .name"
847
+ echo " $0 query file.yaml .tags[0]"
848
+ echo " $0 set file.yaml .version 2.0.0"
849
+ echo " $0 debug file.yaml"
850
+ exit 1
851
+ ;;
852
+ esac
853
+ fi