@smilintux/skcapstone 0.9.0 → 0.12.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.
- package/.env.example +10 -4
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +9 -2
- package/.openclaw-workspace.json +2 -2
- package/CLAUDE.md +37 -0
- package/MISSION.md +17 -2
- package/README.md +282 -3
- package/docker/Dockerfile +7 -7
- package/docker/compose-templates/dev-team.yml +12 -12
- package/docker/compose-templates/mini-team.yml +9 -9
- package/docker/compose-templates/ops-team.yml +10 -10
- package/docker/compose-templates/research-team.yml +10 -10
- package/docker/entrypoint.sh +4 -4
- package/docs/ADR-optional-integration-backbone.md +181 -0
- package/docs/ARCHITECTURE.md +186 -43
- package/docs/BOND_WITH_GROK.md +6 -6
- package/docs/CUSTOM_AGENT.md +278 -1
- package/docs/DREAMING.md +70 -0
- package/docs/GETTING_STARTED.md +10 -7
- package/docs/QUICKSTART.md +10 -6
- package/docs/SKJOULE_ARCHITECTURE.md +3 -3
- package/docs/SOUL_SWAPPER.md +5 -5
- package/docs/hammertime-audit.md +402 -0
- package/docs/sk-integration-HANDOFF.md +117 -0
- package/docs/skscheduler.md +155 -0
- package/docs/superpowers/examples/jobs.yaml +31 -0
- package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
- package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
- package/examples/custom-bond-template.json +1 -1
- package/examples/grok-feb.json +1 -1
- package/examples/queen-ava-feb.json +1 -1
- package/launchd/com.skcapstone.daemon.plist +52 -0
- package/launchd/com.skcapstone.memory-compress.plist +45 -0
- package/launchd/com.skcapstone.skcomms-heartbeat.plist +33 -0
- package/launchd/com.skcapstone.skcomms-queue-drain.plist +34 -0
- package/launchd/install-launchd.sh +156 -0
- package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
- package/package.json +1 -1
- package/pyproject.toml +16 -10
- package/scripts/archive-sessions.sh +95 -0
- package/scripts/check-updates.py +4 -4
- package/scripts/install-bundle.sh +8 -8
- package/scripts/install.ps1 +12 -11
- package/scripts/install.sh +196 -11
- package/scripts/model-fallback-monitor.sh +102 -0
- package/scripts/notion-api.py +259 -0
- package/scripts/nvidia-proxy.mjs +908 -0
- package/scripts/proxy-monitor.sh +89 -0
- package/scripts/refresh-anthropic-token.sh +172 -0
- package/scripts/release.sh +98 -0
- package/scripts/session-to-memory.py +219 -0
- package/scripts/skgateway.mjs +856 -0
- package/scripts/telegram-catchup-all.sh +147 -0
- package/scripts/verify_install.sh +2 -2
- package/scripts/wargov-ufo-capture/README.md +43 -0
- package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
- package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
- package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
- package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
- package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
- package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
- package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
- package/scripts/watch-anthropic-token.sh +212 -0
- package/scripts/windows/install-tasks.ps1 +7 -7
- package/scripts/windows/skcapstone-task.xml +1 -1
- package/src/skcapstone/__init__.py +45 -3
- package/src/skcapstone/_cli_monolith.py +20 -15
- package/src/skcapstone/activity.py +5 -1
- package/src/skcapstone/agent_card.py +3 -2
- package/src/skcapstone/api.py +41 -40
- package/src/skcapstone/auction.py +14 -11
- package/src/skcapstone/backup.py +2 -1
- package/src/skcapstone/blueprint_registry.py +4 -3
- package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
- package/src/skcapstone/brain_first.py +238 -0
- package/src/skcapstone/changelog.py +1 -1
- package/src/skcapstone/chat.py +22 -17
- package/src/skcapstone/cli/__init__.py +9 -1
- package/src/skcapstone/cli/_common.py +1 -0
- package/src/skcapstone/cli/agents_spawner.py +5 -2
- package/src/skcapstone/cli/alerts.py +25 -4
- package/src/skcapstone/cli/bench.py +15 -15
- package/src/skcapstone/cli/chat.py +7 -4
- package/src/skcapstone/cli/consciousness.py +5 -2
- package/src/skcapstone/cli/context_cmd.py +18 -4
- package/src/skcapstone/cli/daemon.py +121 -42
- package/src/skcapstone/cli/gtd.py +26 -1
- package/src/skcapstone/cli/housekeeping.py +3 -3
- package/src/skcapstone/cli/identity_cmd.py +378 -0
- package/src/skcapstone/cli/joule_cmd.py +7 -3
- package/src/skcapstone/cli/memory.py +8 -6
- package/src/skcapstone/cli/peers_dir.py +1 -1
- package/src/skcapstone/cli/register_cmd.py +29 -3
- package/src/skcapstone/cli/scheduler_cmd.py +167 -0
- package/src/skcapstone/cli/session.py +25 -0
- package/src/skcapstone/cli/setup.py +96 -29
- package/src/skcapstone/cli/shell_cmd.py +53 -1
- package/src/skcapstone/cli/skills_cmd.py +2 -2
- package/src/skcapstone/cli/soul.py +8 -5
- package/src/skcapstone/cli/status.py +37 -11
- package/src/skcapstone/cli/telegram.py +21 -0
- package/src/skcapstone/cli/test_cmd.py +5 -5
- package/src/skcapstone/cli/test_connection.py +2 -2
- package/src/skcapstone/cli/upgrade_cmd.py +23 -14
- package/src/skcapstone/cli/version_cmd.py +1 -1
- package/src/skcapstone/cli/watch_cmd.py +9 -6
- package/src/skcapstone/cloud9_bridge.py +14 -14
- package/src/skcapstone/codex_setup.py +255 -0
- package/src/skcapstone/config_validator.py +7 -4
- package/src/skcapstone/consciousness_config.py +5 -1
- package/src/skcapstone/consciousness_loop.py +313 -273
- package/src/skcapstone/context_loader.py +121 -0
- package/src/skcapstone/coord_federation.py +2 -1
- package/src/skcapstone/coordination.py +23 -6
- package/src/skcapstone/crush_integration.py +2 -1
- package/src/skcapstone/daemon.py +151 -88
- package/src/skcapstone/dashboard.py +10 -10
- package/src/skcapstone/data/sk-agent-picker.sh +421 -0
- package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
- package/src/skcapstone/data/systemd/skcapstone.service +37 -0
- package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
- package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
- package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
- package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
- package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
- package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
- package/src/skcapstone/defaults/claude/settings.json +74 -0
- package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
- package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
- package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
- package/src/skcapstone/defaults/unhinged.json +13 -0
- package/src/skcapstone/discovery.py +43 -20
- package/src/skcapstone/doctor.py +941 -22
- package/src/skcapstone/dreaming.py +1183 -109
- package/src/skcapstone/emotion_tracker.py +2 -2
- package/src/skcapstone/export.py +4 -3
- package/src/skcapstone/fuse_mount.py +35 -25
- package/src/skcapstone/gui_installer.py +2 -2
- package/src/skcapstone/heartbeat.py +34 -30
- package/src/skcapstone/housekeeping.py +14 -14
- package/src/skcapstone/install_wizard.py +209 -7
- package/src/skcapstone/itil.py +13 -4
- package/src/skcapstone/kms_scheduler.py +10 -8
- package/src/skcapstone/launchd.py +426 -0
- package/src/skcapstone/mcp_launcher.py +15 -1
- package/src/skcapstone/mcp_server.py +341 -49
- package/src/skcapstone/mcp_tools/__init__.py +2 -0
- package/src/skcapstone/mcp_tools/_helpers.py +2 -2
- package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
- package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
- package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
- package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
- package/src/skcapstone/mcp_tools/did_tools.py +11 -8
- package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
- package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
- package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
- package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
- package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
- package/src/skcapstone/mdns_discovery.py +2 -2
- package/src/skcapstone/memory_curator.py +1 -1
- package/src/skcapstone/memory_engine.py +10 -3
- package/src/skcapstone/metrics.py +30 -16
- package/src/skcapstone/migrate_memories.py +4 -3
- package/src/skcapstone/migrate_multi_agent.py +8 -7
- package/src/skcapstone/models.py +47 -5
- package/src/skcapstone/notifications.py +42 -18
- package/src/skcapstone/onboard.py +1000 -126
- package/src/skcapstone/operator_link.py +170 -0
- package/src/skcapstone/peer_directory.py +4 -4
- package/src/skcapstone/peers.py +19 -19
- package/src/skcapstone/pillars/__init__.py +7 -5
- package/src/skcapstone/pillars/consciousness.py +191 -0
- package/src/skcapstone/pillars/identity.py +51 -7
- package/src/skcapstone/pillars/memory.py +9 -3
- package/src/skcapstone/pillars/sync.py +2 -2
- package/src/skcapstone/preflight.py +3 -3
- package/src/skcapstone/providers/docker.py +28 -28
- package/src/skcapstone/register.py +6 -6
- package/src/skcapstone/registry_client.py +5 -4
- package/src/skcapstone/runtime.py +14 -3
- package/src/skcapstone/scheduled_tasks.py +254 -19
- package/src/skcapstone/scheduler_jobs.py +456 -0
- package/src/skcapstone/scheduler_runner.py +239 -0
- package/src/skcapstone/scheduler_state.py +162 -0
- package/src/skcapstone/sdk.py +310 -0
- package/src/skcapstone/service_health.py +279 -39
- package/src/skcapstone/session_briefing.py +108 -0
- package/src/skcapstone/session_capture.py +1 -1
- package/src/skcapstone/shell.py +7 -1
- package/src/skcapstone/soul.py +3 -1
- package/src/skcapstone/soul_switch.py +3 -1
- package/src/skcapstone/summary.py +6 -6
- package/src/skcapstone/sync_engine.py +15 -15
- package/src/skcapstone/sync_watcher.py +2 -2
- package/src/skcapstone/systemd.py +72 -21
- package/src/skcapstone/team_comms.py +8 -8
- package/src/skcapstone/team_engine.py +1 -1
- package/src/skcapstone/testrunner.py +3 -3
- package/src/skcapstone/trust_graph.py +40 -5
- package/src/skcapstone/unified_search.py +15 -6
- package/src/skcapstone/uninstall_wizard.py +11 -3
- package/src/skcapstone/version_check.py +8 -4
- package/src/skcapstone/warmth_anchor.py +4 -2
- package/src/skcapstone/whoami.py +4 -4
- package/systemd/skcapstone.service +4 -6
- package/systemd/skcapstone@.service +7 -8
- package/systemd/skcomms-heartbeat.service +21 -0
- package/systemd/skcomms-heartbeat.timer +12 -0
- package/systemd/skcomms-queue-drain.service +17 -0
- package/systemd/skcomms-queue-drain.timer +12 -0
- package/tests/conftest.py +39 -0
- package/tests/integration/test_consciousness_e2e.py +39 -39
- package/tests/test_agent_card.py +1 -1
- package/tests/test_agent_home_scaffold.py +34 -0
- package/tests/test_alerts_consumer_topics.py +27 -0
- package/tests/test_backup.py +2 -1
- package/tests/test_chat.py +6 -6
- package/tests/test_claude_md.py +2 -2
- package/tests/test_cli_skills.py +10 -10
- package/tests/test_cli_test_cmd.py +4 -4
- package/tests/test_cli_test_connection.py +1 -1
- package/tests/test_cloud9_bridge.py +6 -6
- package/tests/test_consciousness_e2e.py +1 -1
- package/tests/test_consciousness_loop.py +10 -10
- package/tests/test_coordination.py +25 -0
- package/tests/test_cross_package.py +21 -21
- package/tests/test_daemon.py +4 -4
- package/tests/test_daemon_shutdown.py +1 -1
- package/tests/test_docker_provider.py +29 -29
- package/tests/test_doctor.py +400 -0
- package/tests/test_doctor_skscheduler.py +50 -0
- package/tests/test_dreaming_engine.py +147 -0
- package/tests/test_dreaming_gtd_capture.py +35 -0
- package/tests/test_e2e_automated.py +8 -5
- package/tests/test_fuse_mount.py +10 -10
- package/tests/test_gtd_brief.py +46 -0
- package/tests/test_gtd_malformed_tolerance.py +31 -0
- package/tests/test_housekeeping.py +15 -15
- package/tests/test_identity_migrate.py +251 -0
- package/tests/test_integration_backbone.py +598 -0
- package/tests/test_itil_gtd_lifecycle.py +37 -0
- package/tests/test_jobs_dropins.py +84 -0
- package/tests/test_mcp_server.py +82 -37
- package/tests/test_models.py +48 -4
- package/tests/test_multi_agent.py +31 -29
- package/tests/test_notifications.py +122 -32
- package/tests/test_onboard.py +63 -75
- package/tests/test_operator_link.py +78 -0
- package/tests/test_peers.py +14 -14
- package/tests/test_pillars.py +98 -0
- package/tests/test_preflight.py +3 -3
- package/tests/test_runtime.py +21 -0
- package/tests/test_scheduled_tasks.py +11 -6
- package/tests/test_scheduler_cli.py +47 -0
- package/tests/test_scheduler_features.py +133 -0
- package/tests/test_scheduler_integration.py +87 -0
- package/tests/test_scheduler_jobs.py +155 -0
- package/tests/test_scheduler_runner.py +64 -0
- package/tests/test_scheduler_state.py +57 -0
- package/tests/test_sdk.py +70 -0
- package/tests/test_service_health_incidents.py +34 -0
- package/tests/test_service_registry.py +52 -0
- package/tests/test_session_briefing.py +130 -0
- package/tests/test_snapshots.py +4 -4
- package/tests/test_sync_pipeline.py +26 -26
- package/tests/test_team_comms.py +2 -2
- package/tests/test_testrunner.py +2 -2
- package/tests/test_trust_graph.py +18 -0
- package/tests/test_unified_search.py +2 -2
- package/tests/test_version_check.py +10 -0
- package/tests/test_version_cmd.py +8 -8
- package/tests/test_whoami.py +1 -1
- package/systemd/skcomm-heartbeat.service +0 -18
- package/systemd/skcomm-queue-drain.service +0 -17
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# telegram-catchup-all.sh — Import all configured Telegram groups into SKMemory
|
|
3
|
+
#
|
|
4
|
+
# Reads groups from ~/.skcapstone/agents/lumina/config/telegram.yaml
|
|
5
|
+
# and runs `skcapstone telegram catchup` for each enabled group.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash scripts/telegram-catchup-all.sh [--since YYYY-MM-DD] [--limit N] [--group NAME]
|
|
9
|
+
#
|
|
10
|
+
# Examples:
|
|
11
|
+
# bash scripts/telegram-catchup-all.sh # All groups, last 2000 msgs
|
|
12
|
+
# bash scripts/telegram-catchup-all.sh --since 2026-03-01 # All groups since March 1
|
|
13
|
+
# bash scripts/telegram-catchup-all.sh --group brother-john # Just one group
|
|
14
|
+
#
|
|
15
|
+
# Requires:
|
|
16
|
+
# - TELEGRAM_API_ID and TELEGRAM_API_HASH environment variables
|
|
17
|
+
# - ~/.skenv/bin/skcapstone on PATH
|
|
18
|
+
# - Telethon installed in ~/.skenv/
|
|
19
|
+
|
|
20
|
+
set -uo pipefail # no -e: individual group failures shouldn't stop the batch
|
|
21
|
+
|
|
22
|
+
SKENV="${HOME}/.skenv/bin"
|
|
23
|
+
SKCAPSTONE="${SKENV}/skcapstone"
|
|
24
|
+
CONFIG="${HOME}/.skcapstone/agents/lumina/config/telegram.yaml"
|
|
25
|
+
export SKAGENT="${SKAGENT:-lumina}"
|
|
26
|
+
export SKCAPSTONE_AGENT="${SKAGENT}"
|
|
27
|
+
export PATH="${SKENV}:${PATH}"
|
|
28
|
+
|
|
29
|
+
# Parse args
|
|
30
|
+
SINCE=""
|
|
31
|
+
LIMIT="2000"
|
|
32
|
+
ONLY_GROUP=""
|
|
33
|
+
|
|
34
|
+
while [[ $# -gt 0 ]]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--since) SINCE="$2"; shift 2 ;;
|
|
37
|
+
--limit) LIMIT="$2"; shift 2 ;;
|
|
38
|
+
--group) ONLY_GROUP="$2"; shift 2 ;;
|
|
39
|
+
*) echo "Unknown arg: $1"; exit 1 ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# Resolve relative date keywords (downstream CLI only accepts YYYY-MM-DD).
|
|
44
|
+
# Anything else is passed through untouched so explicit dates still work.
|
|
45
|
+
if [[ -n "$SINCE" ]]; then
|
|
46
|
+
case "$SINCE" in
|
|
47
|
+
yesterday) SINCE="$(date -u -d 'yesterday' +%Y-%m-%d)" ;;
|
|
48
|
+
today) SINCE="$(date -u +%Y-%m-%d)" ;;
|
|
49
|
+
[0-9]*d) SINCE="$(date -u -d "${SINCE%d} days ago" +%Y-%m-%d)" ;;
|
|
50
|
+
esac
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Check prerequisites
|
|
54
|
+
if [[ -z "${TELEGRAM_API_ID:-}" || -z "${TELEGRAM_API_HASH:-}" ]]; then
|
|
55
|
+
echo "ERROR: TELEGRAM_API_ID and TELEGRAM_API_HASH must be set."
|
|
56
|
+
echo "Get them from https://my.telegram.org"
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [[ ! -f "$CONFIG" ]]; then
|
|
61
|
+
echo "ERROR: Config not found: $CONFIG"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Parse groups from YAML (simple grep — no yq dependency)
|
|
66
|
+
echo "=== Telegram Catch-Up All ==="
|
|
67
|
+
echo "Config: $CONFIG"
|
|
68
|
+
echo "Agent: $SKCAPSTONE_AGENT"
|
|
69
|
+
echo "Limit: $LIMIT"
|
|
70
|
+
[[ -n "$SINCE" ]] && echo "Since: $SINCE"
|
|
71
|
+
[[ -n "$ONLY_GROUP" ]] && echo "Only group: $ONLY_GROUP"
|
|
72
|
+
echo ""
|
|
73
|
+
|
|
74
|
+
# Extract group entries: name, chat ID, tags, enabled status
|
|
75
|
+
SUCCESS=0
|
|
76
|
+
FAILED=0
|
|
77
|
+
SKIPPED=0
|
|
78
|
+
|
|
79
|
+
current_name=""
|
|
80
|
+
current_chat=""
|
|
81
|
+
current_tags=""
|
|
82
|
+
current_enabled=""
|
|
83
|
+
|
|
84
|
+
process_group() {
|
|
85
|
+
local name="$1" chat="$2" tags="$3" enabled="$4"
|
|
86
|
+
|
|
87
|
+
if [[ "$enabled" != "true" ]]; then
|
|
88
|
+
echo " SKIP $name (disabled)"
|
|
89
|
+
SKIPPED=$((SKIPPED + 1))
|
|
90
|
+
return
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if [[ -n "$ONLY_GROUP" && "$name" != *"$ONLY_GROUP"* ]]; then
|
|
94
|
+
SKIPPED=$((SKIPPED + 1))
|
|
95
|
+
return
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
echo -n " IMPORTING $name (chat: $chat) ... "
|
|
99
|
+
|
|
100
|
+
local cmd="$SKCAPSTONE telegram catchup $chat --limit $LIMIT --min-length 20"
|
|
101
|
+
[[ -n "$SINCE" ]] && cmd="$cmd --since $SINCE"
|
|
102
|
+
[[ -n "$tags" ]] && cmd="$cmd --tags $tags"
|
|
103
|
+
|
|
104
|
+
if eval "$cmd" > /tmp/telegram-catchup-$name.log 2>&1; then
|
|
105
|
+
echo "OK"
|
|
106
|
+
SUCCESS=$((SUCCESS + 1))
|
|
107
|
+
else
|
|
108
|
+
echo "FAILED (see /tmp/telegram-catchup-$name.log)"
|
|
109
|
+
FAILED=$((FAILED + 1))
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# Rate limit — avoid hitting Telegram flood control
|
|
113
|
+
sleep 3
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Parse the YAML manually
|
|
117
|
+
while IFS= read -r line; do
|
|
118
|
+
# Detect new group entry
|
|
119
|
+
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.*) ]]; then
|
|
120
|
+
# Process previous group if we have one
|
|
121
|
+
if [[ -n "$current_name" ]]; then
|
|
122
|
+
process_group "$current_name" "$current_chat" "$current_tags" "$current_enabled"
|
|
123
|
+
fi
|
|
124
|
+
current_name="${BASH_REMATCH[1]}"
|
|
125
|
+
current_chat=""
|
|
126
|
+
current_tags=""
|
|
127
|
+
current_enabled="true"
|
|
128
|
+
elif [[ "$line" =~ ^[[:space:]]*chat:[[:space:]]*\"?([0-9]+)\"? ]]; then
|
|
129
|
+
current_chat="${BASH_REMATCH[1]}"
|
|
130
|
+
elif [[ "$line" =~ ^[[:space:]]*tags:[[:space:]]*\[(.*)\] ]]; then
|
|
131
|
+
# Convert YAML list to comma-separated
|
|
132
|
+
current_tags=$(echo "${BASH_REMATCH[1]}" | sed 's/,/ /g' | tr -s ' ' ',' | sed 's/^,//;s/,$//')
|
|
133
|
+
elif [[ "$line" =~ ^[[:space:]]*enabled:[[:space:]]*(.*) ]]; then
|
|
134
|
+
current_enabled="${BASH_REMATCH[1]}"
|
|
135
|
+
fi
|
|
136
|
+
done < "$CONFIG"
|
|
137
|
+
|
|
138
|
+
# Process last group
|
|
139
|
+
if [[ -n "$current_name" ]]; then
|
|
140
|
+
process_group "$current_name" "$current_chat" "$current_tags" "$current_enabled"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
echo ""
|
|
144
|
+
echo "=== Done ==="
|
|
145
|
+
echo " Success: $SUCCESS"
|
|
146
|
+
echo " Failed: $FAILED"
|
|
147
|
+
echo " Skipped: $SKIPPED"
|
|
@@ -130,10 +130,10 @@ else
|
|
|
130
130
|
fail "pip install skcapstone[dev]"
|
|
131
131
|
fi
|
|
132
132
|
|
|
133
|
-
# ── 5. Local sibling packages (skseed,
|
|
133
|
+
# ── 5. Local sibling packages (skseed, skcomms) ─────────────────────────────
|
|
134
134
|
echo
|
|
135
135
|
echo "Step 5: local sibling packages (if present)"
|
|
136
|
-
for pkg_dir in "$REPO_ROOT/../skseed" "$REPO_ROOT/../
|
|
136
|
+
for pkg_dir in "$REPO_ROOT/../skseed" "$REPO_ROOT/../skcomms" "$REPO_ROOT/../skmemory" "$REPO_ROOT/../skskills"; do
|
|
137
137
|
pkg_name=$(basename "$pkg_dir")
|
|
138
138
|
if [[ -f "$pkg_dir/pyproject.toml" ]] || [[ -f "$pkg_dir/setup.py" ]]; then
|
|
139
139
|
if "$PIP" install "${PIP_OPTS[@]}" -e "$pkg_dir" 2>&1; then
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# war.gov/UFO/ PURSUE capture scripts
|
|
2
|
+
|
|
3
|
+
Lumina-built CDP scripts for capturing war.gov/UFO/ PURSUE releases via the Lumina Chrome browser harness (port 9222). Akamai TLS-fingerprint gates direct curl, so capture has to drive a real browser session.
|
|
4
|
+
|
|
5
|
+
## Quick reference
|
|
6
|
+
|
|
7
|
+
| Script | What it does |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `cdp_probe.py` | Open a tab on `war.gov/UFO/`, inspect the page meta + inline scripts to find the current CSV URL and any new release ZIP bundle. Run this first when a new release lands. |
|
|
10
|
+
| `cdp_capture_release2.py` | Reference implementation of a full-release capture. Pulls the new CSV, downloads the ZIP bundle, and saves the press-release HTML. Per-release: edit `ZIP_URL`, `CSV_URL`, `PRESS_URL`, `DOC_DIR` constants. |
|
|
11
|
+
| `parse_csv.py` | Parses the merged `uap-data.csv`, splits records by `Release Date`, produces `release-02-records.json` + `release-02-urls.json`. Run after CSV fetch. |
|
|
12
|
+
| `pull_dvids.sh` | xargs-parallel-4 batch DVIDS puller. Reads `release-02-records.json`, fetches each DVIDS page, greps mp4 CDN URL, downloads. **Note:** DVIDS classifies UAP audio records as `/video/<id>` pages — always use `/video/` prefix, NOT `/audio/`. |
|
|
13
|
+
| `cdp_finish.py` | Tail-end CDP script: re-extract press release `innerText`, page-context-fetch the 6 thumbnails (Akamai-gated to curl), and grab FBI Vault Part 15. |
|
|
14
|
+
|
|
15
|
+
## Capture flow for a new release (Release 03 etc.)
|
|
16
|
+
|
|
17
|
+
1. Confirm Lumina Chrome is up at :9222 (`~/bin/lumina-x-browser` to start).
|
|
18
|
+
2. `python3 cdp_probe.py` — confirms the data source URL and looks for a ZIP-bundle path.
|
|
19
|
+
3. Edit `cdp_capture_release2.py` constants for the new release: `ZIP_URL`, `CSV_URL`, `PRESS_URL`, `DOC_DIR`.
|
|
20
|
+
4. `python3 cdp_capture_release2.py` — pulls CSV + ZIP + press-release HTML.
|
|
21
|
+
5. `python3 parse_csv.py` — generates per-record inventory + URL set + DVIDS-only list.
|
|
22
|
+
6. `bash pull_dvids.sh` — pulls all DVIDS media in parallel-4.
|
|
23
|
+
7. `python3 cdp_finish.py` — re-extract press text (it picks the wrong selector on first pass), thumbnails, and any FBI Vault references.
|
|
24
|
+
8. Update README.md at `~/nextcloud/cbrd21-share/reference/war-gov-UFO-PURSUE-2026/`.
|
|
25
|
+
|
|
26
|
+
## Gotchas
|
|
27
|
+
|
|
28
|
+
- **DVIDS audio → use `/video/` URL.** "Audio" UAP records are served from `dvidshub.net/video/<id>` with a static image. The `/audio/` path returns 404 for these IDs.
|
|
29
|
+
- **Some DVIDS IDs are shared across records.** E.g., Release 02 ID `1007720` covers both DOW-UAP-PR057a and DOW-UAP-PR057b. Inventory may show N records but only N-1 unique IDs.
|
|
30
|
+
- **Akamai bm tokens are per-session.** Cookies extracted from the browser will NOT work with curl — Akamai checks TLS fingerprint, not cookies. Stay in the browser.
|
|
31
|
+
- **CSV name can change.** Release 01 used `uap-csv.csv`; when Release 02 shipped, it was renamed to `uap-data.csv`. Re-probe the inline scripts to find the current name.
|
|
32
|
+
- **The ZIP bundle is PDFs only.** Don't skip DVIDS pulls just because you got the ZIP — videos/audio are not in the bundle.
|
|
33
|
+
|
|
34
|
+
## Naming convention
|
|
35
|
+
|
|
36
|
+
Match Release 01: `dvids-<id>-<UAP-PR-code>.mp4` (e.g. `dvids-1007706-DOW-UAP-PR050.mp4`). Cleaner than verbose slugs and matches existing on-disk corpus.
|
|
37
|
+
|
|
38
|
+
## Output location
|
|
39
|
+
|
|
40
|
+
`~/nextcloud/cbrd21-share/reference/war-gov-UFO-PURSUE-2026/`
|
|
41
|
+
- `release-NN/` for media
|
|
42
|
+
- `docs/release-NN/` for press release + CSV + manifest + thumbnails
|
|
43
|
+
- `release-NN-zip/` for the official ZIP bundle if provided
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Capture war.gov/UFO/ Release 02 via Lumina Chrome CDP.
|
|
3
|
+
|
|
4
|
+
Strategy (Release 02 is bundled into one ZIP, plus a fresh CSV + press release):
|
|
5
|
+
1. Open a tab on war.gov/UFO/ to seed Akamai cookies in the Chrome session.
|
|
6
|
+
2. Set Page.setDownloadBehavior to allow downloads to our target dir.
|
|
7
|
+
3. Trigger ZIP download by injecting <a download href=...> and clicking it.
|
|
8
|
+
4. Poll for .crdownload to drain and the final file to appear.
|
|
9
|
+
5. Also fetch the new CSV in-page (text response — simpler than download).
|
|
10
|
+
6. Fetch the press release HTML the same way.
|
|
11
|
+
|
|
12
|
+
Output → ~/nextcloud/cbrd21-share/reference/war-gov-UFO-PURSUE-2026/{docs/release-02, release-02-zip}/
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
import urllib.request
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import websocket
|
|
24
|
+
|
|
25
|
+
CDP_HTTP = "http://127.0.0.1:9222"
|
|
26
|
+
SEED_URL = "https://www.war.gov/UFO/"
|
|
27
|
+
|
|
28
|
+
ZIP_URL = "https://www.war.gov/medialink/ufo/052226/release_02/release_02_document_bundle.zip"
|
|
29
|
+
CSV_URL = "https://www.war.gov/Portals/1/Interactive/2026/UFO/uap-data.csv"
|
|
30
|
+
PRESS_URL = "https://www.war.gov/News/Releases/Release/Article/4499305/department-of-war-publishes-second-release-of-unidentified-anomalous-phenomena/"
|
|
31
|
+
|
|
32
|
+
BASE = Path("/home/cbrd21/nextcloud/cbrd21-share/reference/war-gov-UFO-PURSUE-2026")
|
|
33
|
+
DOC_DIR = BASE / "docs" / "release-02"
|
|
34
|
+
ZIP_DIR = BASE / "release-02-zip"
|
|
35
|
+
DOC_DIR.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
ZIP_DIR.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cdp_get(path: str) -> dict | list:
|
|
40
|
+
with urllib.request.urlopen(f"{CDP_HTTP}{path}") as r:
|
|
41
|
+
return json.loads(r.read())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def open_tab(url: str) -> dict:
|
|
45
|
+
req = urllib.request.Request(f"{CDP_HTTP}/json/new?{url}", method="PUT")
|
|
46
|
+
with urllib.request.urlopen(req, timeout=10) as r:
|
|
47
|
+
return json.loads(r.read())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def close_tab(target_id: str) -> None:
|
|
51
|
+
try:
|
|
52
|
+
with urllib.request.urlopen(f"{CDP_HTTP}/json/close/{target_id}", timeout=5):
|
|
53
|
+
pass
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CDP:
|
|
59
|
+
def __init__(self, ws_url: str):
|
|
60
|
+
self.ws = websocket.create_connection(ws_url, timeout=120)
|
|
61
|
+
self.mid = 0
|
|
62
|
+
|
|
63
|
+
def call(self, method: str, params: dict | None = None, timeout: float = 60.0) -> dict:
|
|
64
|
+
self.mid += 1
|
|
65
|
+
msg_id = self.mid
|
|
66
|
+
self.ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
|
|
67
|
+
self.ws.settimeout(timeout)
|
|
68
|
+
while True:
|
|
69
|
+
raw = self.ws.recv()
|
|
70
|
+
msg = json.loads(raw)
|
|
71
|
+
if msg.get("id") == msg_id:
|
|
72
|
+
if "error" in msg:
|
|
73
|
+
raise RuntimeError(f"{method}: {msg['error']}")
|
|
74
|
+
return msg.get("result", {})
|
|
75
|
+
|
|
76
|
+
def wait_event(self, name: str, timeout: float = 30.0) -> dict:
|
|
77
|
+
deadline = time.time() + timeout
|
|
78
|
+
while time.time() < deadline:
|
|
79
|
+
self.ws.settimeout(max(0.1, deadline - time.time()))
|
|
80
|
+
try:
|
|
81
|
+
raw = self.ws.recv()
|
|
82
|
+
except websocket.WebSocketTimeoutException:
|
|
83
|
+
continue
|
|
84
|
+
msg = json.loads(raw)
|
|
85
|
+
if msg.get("method") == name:
|
|
86
|
+
return msg.get("params", {})
|
|
87
|
+
raise TimeoutError(f"event {name} did not fire within {timeout}s")
|
|
88
|
+
|
|
89
|
+
def close(self) -> None:
|
|
90
|
+
try:
|
|
91
|
+
self.ws.close()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def fetch_text_in_page(cdp: CDP, url: str) -> tuple[int, str]:
|
|
97
|
+
expr = (
|
|
98
|
+
f"(async () => {{"
|
|
99
|
+
f" const r = await fetch({json.dumps(url)}, {{credentials: 'include', cache: 'no-store'}});"
|
|
100
|
+
f" return {{status: r.status, text: await r.text()}};"
|
|
101
|
+
f"}})()"
|
|
102
|
+
)
|
|
103
|
+
res = cdp.call("Runtime.evaluate", {
|
|
104
|
+
"expression": expr,
|
|
105
|
+
"awaitPromise": True,
|
|
106
|
+
"returnByValue": True,
|
|
107
|
+
}, timeout=180)
|
|
108
|
+
val = res.get("result", {}).get("value", {}) or {}
|
|
109
|
+
return val.get("status", 0), val.get("text", "")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def trigger_download(cdp: CDP, url: str) -> None:
|
|
113
|
+
expr = (
|
|
114
|
+
f"(() => {{"
|
|
115
|
+
f" const a = document.createElement('a');"
|
|
116
|
+
f" a.href = {json.dumps(url)};"
|
|
117
|
+
f" a.download = '';"
|
|
118
|
+
f" document.body.appendChild(a);"
|
|
119
|
+
f" a.click();"
|
|
120
|
+
f" a.remove();"
|
|
121
|
+
f" return 'click-triggered';"
|
|
122
|
+
f"}})()"
|
|
123
|
+
)
|
|
124
|
+
cdp.call("Runtime.evaluate", {"expression": expr, "returnByValue": True})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def wait_for_file(path: Path, partial_glob: str, timeout: float = 1800.0, idle_threshold: float = 5.0) -> Path | None:
|
|
128
|
+
"""Wait until a file matching the final name shows up + a quiet period after .crdownload drains."""
|
|
129
|
+
deadline = time.time() + timeout
|
|
130
|
+
last_size = -1
|
|
131
|
+
last_change = time.time()
|
|
132
|
+
while time.time() < deadline:
|
|
133
|
+
# Find .crdownload first
|
|
134
|
+
crfiles = list(path.glob("*.crdownload"))
|
|
135
|
+
finished = [p for p in path.glob(partial_glob) if not p.name.endswith(".crdownload")]
|
|
136
|
+
if crfiles:
|
|
137
|
+
size = sum(f.stat().st_size for f in crfiles)
|
|
138
|
+
if size != last_size:
|
|
139
|
+
last_size = size
|
|
140
|
+
last_change = time.time()
|
|
141
|
+
print(f"[download] in-progress {size/1e6:.1f} MB", flush=True)
|
|
142
|
+
time.sleep(2.0)
|
|
143
|
+
elif finished:
|
|
144
|
+
# No crdownload, file is there. Need idle period to ensure stable.
|
|
145
|
+
f = finished[0]
|
|
146
|
+
size = f.stat().st_size
|
|
147
|
+
if size != last_size:
|
|
148
|
+
last_size = size
|
|
149
|
+
last_change = time.time()
|
|
150
|
+
if time.time() - last_change >= idle_threshold:
|
|
151
|
+
return f
|
|
152
|
+
time.sleep(1.0)
|
|
153
|
+
else:
|
|
154
|
+
time.sleep(2.0)
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def sha256_file(p: Path) -> str:
|
|
159
|
+
h = hashlib.sha256()
|
|
160
|
+
with p.open("rb") as f:
|
|
161
|
+
while chunk := f.read(8 * 1024 * 1024):
|
|
162
|
+
h.update(chunk)
|
|
163
|
+
return h.hexdigest()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main() -> int:
|
|
167
|
+
print(f"[capture] seeding tab → {SEED_URL}", flush=True)
|
|
168
|
+
tab = open_tab(SEED_URL)
|
|
169
|
+
target_id = tab["id"]
|
|
170
|
+
ws_url = tab["webSocketDebuggerUrl"]
|
|
171
|
+
cdp = CDP(ws_url)
|
|
172
|
+
try:
|
|
173
|
+
cdp.call("Page.enable")
|
|
174
|
+
cdp.call("Runtime.enable")
|
|
175
|
+
cdp.call("Network.enable", {"maxPostDataSize": 0})
|
|
176
|
+
cdp.call("Page.navigate", {"url": SEED_URL})
|
|
177
|
+
try:
|
|
178
|
+
cdp.wait_event("Page.loadEventFired", timeout=30.0)
|
|
179
|
+
except TimeoutError:
|
|
180
|
+
pass
|
|
181
|
+
time.sleep(3.0) # let Vue settle and cookies stick
|
|
182
|
+
|
|
183
|
+
# ---- 1. Fetch CSV (small, text)
|
|
184
|
+
print(f"[capture] fetching CSV → {CSV_URL}", flush=True)
|
|
185
|
+
status, text = fetch_text_in_page(cdp, CSV_URL)
|
|
186
|
+
print(f"[capture] CSV status={status} len={len(text)}", flush=True)
|
|
187
|
+
if status == 200 and text:
|
|
188
|
+
(DOC_DIR / "uap-data.csv").write_text(text)
|
|
189
|
+
else:
|
|
190
|
+
(DOC_DIR / "uap-data-error.json").write_text(json.dumps({"status": status, "preview": text[:1000]}, indent=2))
|
|
191
|
+
|
|
192
|
+
# ---- 2. Fetch press release HTML
|
|
193
|
+
print(f"[capture] fetching press release → {PRESS_URL}", flush=True)
|
|
194
|
+
status, text = fetch_text_in_page(cdp, PRESS_URL)
|
|
195
|
+
print(f"[capture] press release status={status} len={len(text)}", flush=True)
|
|
196
|
+
if status == 200 and text:
|
|
197
|
+
(DOC_DIR / "press-release-2026-05-22.html").write_text(text)
|
|
198
|
+
# Try to extract clean text via DOM
|
|
199
|
+
txt_expr = (
|
|
200
|
+
f"(async () => {{"
|
|
201
|
+
f" const r = await fetch({json.dumps(PRESS_URL)}, {{credentials: 'include'}});"
|
|
202
|
+
f" const html = await r.text();"
|
|
203
|
+
f" const doc = new DOMParser().parseFromString(html, 'text/html');"
|
|
204
|
+
f" const article = doc.querySelector('.body-text') || doc.querySelector('article') || doc.querySelector('main') || doc.body;"
|
|
205
|
+
f" return article ? article.innerText : '';"
|
|
206
|
+
f"}})()"
|
|
207
|
+
)
|
|
208
|
+
res = cdp.call("Runtime.evaluate", {
|
|
209
|
+
"expression": txt_expr,
|
|
210
|
+
"awaitPromise": True,
|
|
211
|
+
"returnByValue": True,
|
|
212
|
+
}, timeout=60)
|
|
213
|
+
article_text = res.get("result", {}).get("value", "") or ""
|
|
214
|
+
if article_text:
|
|
215
|
+
(DOC_DIR / "press-release-2026-05-22.txt").write_text(article_text)
|
|
216
|
+
print(f"[capture] extracted {len(article_text)} chars of article text", flush=True)
|
|
217
|
+
|
|
218
|
+
# ---- 3. Download the ZIP bundle via download behavior + <a download> click
|
|
219
|
+
print(f"[capture] setting download dir → {ZIP_DIR}", flush=True)
|
|
220
|
+
cdp.call("Page.setDownloadBehavior", {
|
|
221
|
+
"behavior": "allow",
|
|
222
|
+
"downloadPath": str(ZIP_DIR),
|
|
223
|
+
})
|
|
224
|
+
# Also try Browser.setDownloadBehavior which is the newer API
|
|
225
|
+
try:
|
|
226
|
+
cdp.call("Browser.setDownloadBehavior", {
|
|
227
|
+
"behavior": "allow",
|
|
228
|
+
"downloadPath": str(ZIP_DIR),
|
|
229
|
+
"eventsEnabled": True,
|
|
230
|
+
})
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"[capture] Browser.setDownloadBehavior not supported: {e}", flush=True)
|
|
233
|
+
|
|
234
|
+
print(f"[capture] triggering ZIP download → {ZIP_URL}", flush=True)
|
|
235
|
+
trigger_download(cdp, ZIP_URL)
|
|
236
|
+
|
|
237
|
+
# Poll for completion
|
|
238
|
+
zip_file = wait_for_file(ZIP_DIR, "release_02_document_bundle*.zip", timeout=1800.0, idle_threshold=5.0)
|
|
239
|
+
if not zip_file:
|
|
240
|
+
print("[capture] ZIP download did NOT complete in 30 min — check ZIP_DIR manually", flush=True)
|
|
241
|
+
# Diagnostic: list what's in there
|
|
242
|
+
for f in ZIP_DIR.iterdir():
|
|
243
|
+
print(f" {f.name} {f.stat().st_size}", flush=True)
|
|
244
|
+
return 2
|
|
245
|
+
|
|
246
|
+
size_mb = zip_file.stat().st_size / 1e6
|
|
247
|
+
sha = sha256_file(zip_file)
|
|
248
|
+
print(f"[capture] ZIP done: {zip_file.name} {size_mb:.1f} MB sha256={sha}", flush=True)
|
|
249
|
+
|
|
250
|
+
# Write manifest
|
|
251
|
+
manifest = {
|
|
252
|
+
"release": "02",
|
|
253
|
+
"release_date": "2026-05-22",
|
|
254
|
+
"captured_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
255
|
+
"zip_url": ZIP_URL,
|
|
256
|
+
"zip_path": str(zip_file),
|
|
257
|
+
"zip_size_bytes": zip_file.stat().st_size,
|
|
258
|
+
"zip_sha256": sha,
|
|
259
|
+
"csv_url": CSV_URL,
|
|
260
|
+
"press_url": PRESS_URL,
|
|
261
|
+
"capture_method": "Lumina Chrome CDP (port 9222) — page-context fetch + <a download> click",
|
|
262
|
+
}
|
|
263
|
+
(DOC_DIR / "release-02-manifest.json").write_text(json.dumps(manifest, indent=2))
|
|
264
|
+
print(f"[capture] manifest written", flush=True)
|
|
265
|
+
|
|
266
|
+
return 0
|
|
267
|
+
finally:
|
|
268
|
+
cdp.close()
|
|
269
|
+
close_tab(target_id)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
sys.exit(main())
|