skillwiki 0.9.63 → 0.10.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 (36) hide show
  1. package/dist/chunk-5TBIMLTZ.js +343 -0
  2. package/dist/chunk-C5OLZRRM.js +357 -0
  3. package/dist/{chunk-TUFQZ5K4.js → chunk-DR7KFHNH.js} +792 -1983
  4. package/dist/chunk-IZABIE44.js +647 -0
  5. package/dist/chunk-S5ABQCXQ.js +580 -0
  6. package/dist/cli.js +1540 -550
  7. package/dist/index-projection-ERFX76U5.js +10 -0
  8. package/dist/managed-write-preflight-SILEUQEV.js +11 -0
  9. package/dist/skillwiki-mcp.js +4 -1
  10. package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
  11. package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
  12. package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
  13. package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
  14. package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
  15. package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
  16. package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
  17. package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
  18. package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
  19. package/dist/vault-sync/scripts/lib/platform.sh +184 -0
  20. package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
  21. package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
  22. package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
  23. package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
  24. package/dist/vault-sync/scripts/wiki-push.sh +364 -0
  25. package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
  26. package/package.json +2 -2
  27. package/skills/.claude-plugin/plugin.json +1 -1
  28. package/skills/.codex-plugin/plugin.json +1 -1
  29. package/skills/README.md +13 -0
  30. package/skills/package.json +1 -1
  31. package/skills/proj-work/SKILL.md +3 -0
  32. package/skills/skills/proj-work/SKILL.md +3 -0
  33. package/skills/skills/using-skillwiki/SKILL.md +13 -0
  34. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  35. package/skills/using-skillwiki/SKILL.md +13 -0
  36. package/skills/wiki-crystallize/SKILL.md +3 -0
@@ -0,0 +1,364 @@
1
+ #!/bin/bash
2
+ # wiki-push.sh — one-way push from ~/wiki to SeaweedFS S3 via rclone copy.
3
+ #
4
+ # Push-only (rclone copy, NOT sync) — NEVER deletes files on the remote.
5
+ # Pull side is handled separately by:
6
+ # - Obsidian Remotely Save (option 5: Incremental Pull And Delete), or
7
+ # - wiki-fetch-notify.sh (opt-in WIKI_FETCH_PULL_ON_DELTA=1 performs
8
+ # git pull --rebase on positive delta), or
9
+ # - manual `skillwiki sync` / `wiki-sync` skill.
10
+ #
11
+ # This script is the SOLE macOS → S3 transport. It does NOT touch git —
12
+ # single-writer-git is enforced: only sg01's wiki-snapshot.sh pushes to
13
+ # GitHub (concepts/vault-write-authority-model.md D2, D3, D6). macOS
14
+ # consumes sg01 Snapshot commits via the pull mechanisms above.
15
+ #
16
+ # Lockfile prevents overlapping runs if a push takes longer than the launchd interval.
17
+ # Safe by default: no flag = real run (no dry-run mode — meant to be unattended).
18
+
19
+ set -u
20
+
21
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]:-$0}" )" && pwd )"
22
+ . "$SCRIPT_DIR/lib/platform.sh"
23
+ . "$SCRIPT_DIR/lib/lockfile.sh"
24
+ . "$SCRIPT_DIR/lib/git-case.sh"
25
+ . "$SCRIPT_DIR/lib/conflict-markers.sh"
26
+ # Optional: tombstone helpers (present in package installs after 2026-07-14)
27
+ if [ -f "$SCRIPT_DIR/lib/delete-intent.sh" ]; then
28
+ # shellcheck source=lib/delete-intent.sh
29
+ . "$SCRIPT_DIR/lib/delete-intent.sh"
30
+ fi
31
+ platform_detect_os
32
+
33
+ WIKI_DIR="${WIKI_DIR:-$HOME/wiki}"
34
+ REMOTE="${WIKI_REMOTE:-seaweed-wiki:cloud/wiki}"
35
+ FILTERS="${WIKI_PUSH_FILTERS:-$(platform_rclone_config_dir)/wiki-push-filters.txt}"
36
+ LOCK_FILE="$(platform_cache_dir)/wiki-push.lock"
37
+ LOG_FILE="$(platform_log_dir)/wiki-push.log"
38
+ LOG_MAX_SIZE=1048576 # 1 MB
39
+ LOG_KEEP=5
40
+
41
+ mkdir -p "$(dirname "$LOCK_FILE")" "$(dirname "$LOG_FILE")"
42
+
43
+ log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG_FILE"; }
44
+
45
+ conflict_marker_guard() {
46
+ local findings
47
+ findings="$(mktemp)" || { log "FAIL could not create conflict-marker scan temp file"; return 1; }
48
+ if ! vault_sync_scan_conflict_markers "$WIKI_DIR" "$findings"; then
49
+ log "FAIL conflict marker blocks present; refusing S3 push"
50
+ vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
51
+ rm -f "$findings"
52
+ return 1
53
+ fi
54
+ rm -f "$findings"
55
+ return 0
56
+ }
57
+
58
+ # Parse WIKI_PUSH_MAX_REMOTE_DELETES into global _VS_MAX_REMOTE_DELETES (echo for callers).
59
+ remote_prune_resolve_max_deletes() {
60
+ local max_remote_deletes="${WIKI_PUSH_MAX_REMOTE_DELETES:-10}"
61
+ case "$max_remote_deletes" in
62
+ ''|*[!0-9]*)
63
+ log "FAIL remote prune invalid WIKI_PUSH_MAX_REMOTE_DELETES=$max_remote_deletes"
64
+ return 1
65
+ ;;
66
+ esac
67
+ if [ "$max_remote_deletes" -lt 1 ]; then
68
+ log "FAIL remote prune invalid WIKI_PUSH_MAX_REMOTE_DELETES=$max_remote_deletes"
69
+ return 1
70
+ fi
71
+ printf '%s\n' "$max_remote_deletes"
72
+ }
73
+
74
+ remote_prune_archived_source_paths() {
75
+ local max_remote_deletes
76
+ max_remote_deletes="$(remote_prune_resolve_max_deletes)" || return 1
77
+
78
+ local tmp_dir archive_pairs remote_paths plan_file
79
+ tmp_dir="$(mktemp -d)"
80
+ archive_pairs="$tmp_dir/archive-pairs.tsv"
81
+ remote_paths="$tmp_dir/remote-paths.txt"
82
+ plan_file="$tmp_dir/plan.txt"
83
+
84
+ if [ ! -d "$WIKI_DIR/_archive" ]; then
85
+ rm -rf "$tmp_dir"
86
+ return 0
87
+ fi
88
+
89
+ (
90
+ cd "$WIKI_DIR" || exit 1
91
+ find _archive -type f | while IFS= read -r archive_path; do
92
+ local source_path="${archive_path#_archive/}"
93
+ [ "$source_path" = "$archive_path" ] && continue
94
+ [ -e "$source_path" ] && continue
95
+ printf '%s\t%s\n' "$source_path" "$archive_path"
96
+ done | LC_ALL=C sort -u
97
+ ) > "$archive_pairs" || {
98
+ log "FAIL remote prune could not enumerate _archive pairs"
99
+ rm -rf "$tmp_dir"
100
+ return 1
101
+ }
102
+
103
+ if [ ! -s "$archive_pairs" ]; then
104
+ rm -rf "$tmp_dir"
105
+ return 0
106
+ fi
107
+
108
+ if ! rclone lsf "$REMOTE" --recursive --files-only 2>>"$LOG_FILE" | LC_ALL=C sort -u > "$remote_paths"; then
109
+ log "FAIL remote prune could not list remote paths"
110
+ rm -rf "$tmp_dir"
111
+ return 1
112
+ fi
113
+
114
+ while IFS="$(printf '\t')" read -r source_path archive_path; do
115
+ [ -z "$source_path" ] && continue
116
+ if grep -Fxq -- "$source_path" "$remote_paths" && grep -Fxq -- "$archive_path" "$remote_paths"; then
117
+ printf '%s\n' "$source_path"
118
+ fi
119
+ done < "$archive_pairs" | LC_ALL=C sort -u > "$plan_file"
120
+
121
+ local planned_count
122
+ planned_count="$(wc -l < "$plan_file" | tr -d ' ')"
123
+ if [ "$planned_count" = "0" ]; then
124
+ rm -rf "$tmp_dir"
125
+ return 0
126
+ fi
127
+ if [ "$planned_count" -gt "$max_remote_deletes" ]; then
128
+ log "FAIL remote prune cap exceeded: $planned_count > $max_remote_deletes"
129
+ sed 's/^/remote prune skipped: /' "$plan_file" >> "$LOG_FILE"
130
+ rm -rf "$tmp_dir"
131
+ return 1
132
+ fi
133
+
134
+ local rel_path remote_path
135
+ while IFS= read -r rel_path; do
136
+ [ -z "$rel_path" ] && continue
137
+ remote_path="${REMOTE%/}/$rel_path"
138
+ if rclone deletefile "$remote_path" >>"$LOG_FILE" 2>&1; then
139
+ log "OK remote pruned archived source $remote_path"
140
+ else
141
+ log "FAIL remote prune deletefile failed for $remote_path"
142
+ rm -rf "$tmp_dir"
143
+ return 1
144
+ fi
145
+ done < "$plan_file"
146
+
147
+ rm -rf "$tmp_dir"
148
+ return 0
149
+ }
150
+
151
+ # Prune S3 objects listed in active meta/delete-intents tombstones.
152
+ # Combined with archive prune under the same WIKI_PUSH_MAX_REMOTE_DELETES budget
153
+ # only for this function's own plan (archive prune uses its own budget per call).
154
+ remote_prune_tombstoned_paths() {
155
+ if ! command -v delete_intent_list_active_paths >/dev/null 2>&1; then
156
+ return 0
157
+ fi
158
+
159
+ local max_remote_deletes
160
+ max_remote_deletes="$(remote_prune_resolve_max_deletes)" || return 1
161
+
162
+ local tmp_dir remote_paths plan_file intent_paths
163
+ tmp_dir="$(mktemp -d)"
164
+ remote_paths="$tmp_dir/remote-paths.txt"
165
+ plan_file="$tmp_dir/plan.txt"
166
+ intent_paths="$tmp_dir/intents.txt"
167
+
168
+ delete_intent_list_active_paths "$WIKI_DIR" > "$intent_paths" || true
169
+ if [ ! -s "$intent_paths" ]; then
170
+ rm -rf "$tmp_dir"
171
+ return 0
172
+ fi
173
+
174
+ if ! rclone lsf "$REMOTE" --recursive --files-only 2>>"$LOG_FILE" | LC_ALL=C sort -u > "$remote_paths"; then
175
+ log "FAIL tombstone prune could not list remote paths"
176
+ rm -rf "$tmp_dir"
177
+ return 1
178
+ fi
179
+
180
+ : > "$plan_file"
181
+ while IFS= read -r rel_path; do
182
+ [ -z "$rel_path" ] && continue
183
+ if grep -Fxq -- "$rel_path" "$remote_paths"; then
184
+ printf '%s\n' "$rel_path" >> "$plan_file"
185
+ fi
186
+ done < "$intent_paths"
187
+
188
+ local planned_count
189
+ planned_count="$(wc -l < "$plan_file" | tr -d ' ')"
190
+ if [ "$planned_count" = "0" ]; then
191
+ rm -rf "$tmp_dir"
192
+ return 0
193
+ fi
194
+ if [ "$planned_count" -gt "$max_remote_deletes" ]; then
195
+ log "FAIL tombstone prune cap exceeded: $planned_count > $max_remote_deletes"
196
+ sed 's/^/tombstone prune skipped: /' "$plan_file" >> "$LOG_FILE"
197
+ rm -rf "$tmp_dir"
198
+ return 1
199
+ fi
200
+
201
+ local rel_path remote_path
202
+ while IFS= read -r rel_path; do
203
+ [ -z "$rel_path" ] && continue
204
+ remote_path="${REMOTE%/}/$rel_path"
205
+ if rclone deletefile "$remote_path" >>"$LOG_FILE" 2>&1; then
206
+ log "OK remote pruned tombstone $remote_path"
207
+ else
208
+ log "FAIL tombstone prune deletefile failed for $remote_path"
209
+ rm -rf "$tmp_dir"
210
+ return 1
211
+ fi
212
+ done < "$plan_file"
213
+
214
+ rm -rf "$tmp_dir"
215
+ return 0
216
+ }
217
+
218
+ # Rotate log if oversized.
219
+ if [ -f "$LOG_FILE" ] && [ "$(platform_stat_size "$LOG_FILE")" -gt "$LOG_MAX_SIZE" ]; then
220
+ for i in $(seq $((LOG_KEEP - 1)) -1 1); do
221
+ [ -f "$LOG_FILE.$i" ] && mv "$LOG_FILE.$i" "$LOG_FILE.$((i + 1))"
222
+ done
223
+ mv "$LOG_FILE" "$LOG_FILE.1"
224
+ fi
225
+
226
+ # Guard: wiki dir present.
227
+ if [ ! -d "$WIKI_DIR" ]; then
228
+ log "ERROR: WIKI_DIR not found at $WIKI_DIR"
229
+ exit 0
230
+ fi
231
+
232
+ # Guard: filter file present (it's not strictly required, but missing it would push .git/).
233
+ if [ ! -f "$FILTERS" ]; then
234
+ log "ERROR: filter file missing at $FILTERS — refusing to push without exclusions"
235
+ exit 0
236
+ fi
237
+ # Projection-authority transport: non-authority leaves must not push root aggregates.
238
+ if ! grep -qxF -- '- /index.md' "$FILTERS" || ! grep -qxF -- '- /log.md' "$FILTERS"; then
239
+ log "ERROR: filter file missing root aggregate exclusions (- /index.md and - /log.md)"
240
+ exit 1
241
+ fi
242
+
243
+ # Acquire lockfile (non-blocking).
244
+ LOCK_RC=0
245
+ lockfile_acquire "$LOCK_FILE" 600 || LOCK_RC=$?
246
+ if [ "$LOCK_RC" -eq 1 ]; then
247
+ log "skip: previous run still in flight"
248
+ exit 0
249
+ elif [ "$LOCK_RC" -eq 2 ]; then
250
+ log "stale lock reclaimed"
251
+ fi
252
+
253
+ START=$(date +%s)
254
+
255
+ if ! CASE_CONFLICTS=$(cd "$WIKI_DIR" && git_case_conflicts); then
256
+ log "ERROR: case-only path collision detected — refusing rclone push"
257
+ printf '%s\n' "$CASE_CONFLICTS" >>"$LOG_FILE"
258
+ exit 0
259
+ fi
260
+
261
+ # Fix Windows-hostile markdown paths before publishing to S3.
262
+ # The skillwiki CLI owns the rename and citation-rewire semantics; this script
263
+ # only gates the unattended push pipeline.
264
+ if command -v skillwiki >/dev/null 2>&1; then
265
+ PATH_FIX_OUTPUT=$(skillwiki lint "$WIKI_DIR" --only path_too_long --fix 2>&1)
266
+ PATH_FIX_RC=$?
267
+ printf '%s\n' "$PATH_FIX_OUTPUT" >>"$LOG_FILE"
268
+ if [ "$PATH_FIX_RC" -ne 0 ]; then
269
+ log "ERROR: path_too_long fix failed exit=$PATH_FIX_RC — refusing rclone push"
270
+ exit 0
271
+ fi
272
+ else
273
+ log "ERROR: skillwiki CLI not found — refusing push because path_too_long guard cannot run"
274
+ exit 0
275
+ fi
276
+
277
+ if ! conflict_marker_guard; then
278
+ exit 1
279
+ fi
280
+
281
+ # Lint delta gate: block S3 publication only on newly introduced lint errors.
282
+ # Inherited debt is logged. Malformed/missing delta evidence fails closed.
283
+ if command -v skillwiki >/dev/null 2>&1; then
284
+ DELTA_OUT=$(skillwiki sync lint-delta "$WIKI_DIR" --base-ref origin/main 2>&1) || true
285
+ printf '%s
286
+ ' "$DELTA_OUT" >>"$LOG_FILE"
287
+ eval "$(printf '%s\n' "$DELTA_OUT" | python3 -c '
288
+ import json,sys
289
+ try:
290
+ d=json.load(sys.stdin)
291
+ data=d.get("data") or {}
292
+ print("DELTA_FULL=%s" % int(data.get("full_errors", -1)))
293
+ print("DELTA_BASE=%s" % int(data.get("base_errors", -1)))
294
+ print("DELTA_NEW=%s" % int(data.get("new_errors", -1)))
295
+ print("DELTA_RESOLVED=%s" % int(data.get("resolved_errors", -1)))
296
+ except Exception:
297
+ print("DELTA_FULL=-1")
298
+ print("DELTA_BASE=-1")
299
+ print("DELTA_NEW=-1")
300
+ print("DELTA_RESOLVED=-1")
301
+ ' 2>/dev/null)" || { DELTA_FULL=-1; DELTA_BASE=-1; DELTA_NEW=-1; DELTA_RESOLVED=-1; }
302
+ if [ "${DELTA_NEW:--1}" = "-1" ] || [ "${DELTA_FULL:--1}" = "-1" ]; then
303
+ log "FAIL lint-delta evidence missing or malformed — refusing S3 push"
304
+ exit 1
305
+ fi
306
+ log "LINT-DELTA full=$DELTA_FULL base=$DELTA_BASE new=$DELTA_NEW resolved=$DELTA_RESOLVED"
307
+ if [ "$DELTA_NEW" -gt 0 ]; then
308
+ log "FAIL lint-delta new_errors=$DELTA_NEW blocks S3 push (full=$DELTA_FULL)"
309
+ exit 1
310
+ fi
311
+ if [ "$DELTA_FULL" -gt 0 ]; then
312
+ log "WARN inherited lint debt full_errors=$DELTA_FULL (new_errors=0) — push allowed"
313
+ fi
314
+ else
315
+ log "FAIL skillwiki CLI not found — lint-delta gate cannot run; refusing S3 push"
316
+ exit 1
317
+ fi
318
+
319
+ # rclone copy (NOT sync) → never bulk-deletes on remote.
320
+ # After a successful copy, prune only those stale live paths whose matching
321
+ # _archive/<path> object is now present on the remote and whose live path is
322
+ # absent locally. This keeps archive moves from leaving stale live note paths
323
+ # on S3 without reintroducing git pushes on macOS.
324
+ # --update : only newer source files overwrite remote (mod-time + size based)
325
+ # --filter-from : reuse the bisync filter list
326
+ # --transfers 4 : modest parallelism for small files
327
+ # --checkers 8 : default-ish
328
+ # --low-level-retries 10 --retries 3 : tolerate transient network blips
329
+ OUTPUT=$(rclone copy "$WIKI_DIR" "$REMOTE" \
330
+ --filter-from "$FILTERS" \
331
+ --update \
332
+ --transfers 4 \
333
+ --checkers 8 \
334
+ --low-level-retries 10 \
335
+ --retries 3 \
336
+ --stats-one-line \
337
+ --stats 60s 2>&1)
338
+ RC=$?
339
+
340
+ DURATION=$(( $(date +%s) - START ))
341
+
342
+ if [ "$RC" -eq 0 ]; then
343
+ # rclone with --stats-one-line emits a "Transferred: N / N, 100%, ..." line on completion.
344
+ STATS_LINE=$(printf '%s\n' "$OUTPUT" | grep -E '^Transferred:.*[0-9]+ B' | tail -1)
345
+ if [ -n "$STATS_LINE" ]; then
346
+ log "OK push duration=${DURATION}s | $STATS_LINE"
347
+ else
348
+ log "OK push (no changes) duration=${DURATION}s"
349
+ fi
350
+ else
351
+ log "FAIL rclone exit=$RC duration=${DURATION}s"
352
+ printf '%s\n' "$OUTPUT" >>"$LOG_FILE"
353
+ fi
354
+
355
+ if [ "$RC" -eq 0 ]; then
356
+ if ! remote_prune_archived_source_paths; then
357
+ log "FAIL remote prune failed after rclone copy"
358
+ fi
359
+ if ! remote_prune_tombstoned_paths; then
360
+ log "FAIL tombstone prune failed after rclone copy"
361
+ fi
362
+ fi
363
+
364
+ exit 0