Huawei Cloud Agent Integration (OpenViking Long-Term Memory) Integrate / unbind OpenViking long-term memory for coding agents running in bwrap sandboxes ( ). Every agent uses its native mechanism (MCP, HTTP memory provider, TS extension hooks, settings.json), so integration survives agent upgrades. Writes are template-level persistent : config is injected into , so sandbox preserves the integration. Supported Agents | Agent | Mechanism | |-------|-----------| | CodeArts CLI | → | | OpenCode | (npm mirror → GitHub fallback) | | OpenClaw | + slot (disables hook, denies ) | | Hermes | Built-in (…

&& ! _ov_openclaw slot-owner \"$cfg_file\" 2>/dev/null | grep -q 'openviking'; then\n log_warn \"plugins.slots.contextEngine is currently owned by another plugin\"\n if [[ \"${AUTO_YES:-false}\" == \"true\" ]]; then\n force_slot=1\n else\n read -p \"Allow --force-slot to replace it (yes/no)? \" ans\n [[ \"$ans\" == \"yes\" || \"$ans\" == \"y\" ]] && force_slot=1\n fi\n fi\n done\n local allow_offline=1 # dev-mode local server; safe default\n require_confirmation \"Integrate OpenViking (Official ClawHub Plugin)\" \"openclaw\" \"Install @openviking/openclaw-plugin (ClawHub → domestic npm mirror → on-demand source build fallback) + openviking setup --json into template start.sh (contextEngine slot, auto-recall + auto-capture)\" || return 1\n if dry_run_msg \"Would add OpenViking plugin install to $tpl\"; then return 0; fi\n backup_file \"$tpl\"\n # Insert official plugin install block before the gateway start step\n _ov_openclaw inject \"$tpl\" \"$OV_ENDPOINT\" \"$force_slot\" \"$allow_offline\" \"$OV_NPM_REGISTRY_DEFAULT\" \"$OV_SHARED_DIR\" \"$OV_RUNTIME_DIR\" \"$OV_TEMPLATE_DIR\"\n log_ok \"OpenClaw template updated with official OpenViking plugin install\"\n # Sync to sandbox workspace — ISSUE-013: path join must use a slash, and a\n # missing process_dir must not be reported as synced\n local sandbox_dir=\"\"\n sandbox_dir=$(find_sandbox \"openclaw\")\n if [[ -n \"$sandbox_dir\" ]]; then\n ov_sync_template_to_sandbox \"$tpl\" \"$sandbox_dir\" process_dir\n fi\n # Clean up legacy direct-config-write injection and old MCP artifacts\n local cleaned=false\n if grep -q \"OpenViking plugin config\" \"$tpl\" 2>/dev/null; then\n _ov_openclaw clean-legacy-cfg \"$tpl\"\n cleaned=true\n fi\n if grep -q \"OpenViking MCP server injected\" \"$tpl\" 2>/dev/null; then\n _ov_openclaw clean-legacy-mcp \"$tpl\"\n cleaned=true\n fi\n for ext_dir in \"$OV_HOME/.openclaw/extensions/openviking\" \"$OV_RUNTIME_DIR/openclaw/state/extensions/openviking\"; do\n if [[ -d \"$ext_dir\" ]]; then\n ov_safe_rm \"$ext_dir\" 2>/dev/null || true\n cleaned=true\n fi\n done\n for cfg_file in \"$OV_HOME/.openclaw/openclaw.json\" \"$OV_RUNTIME_DIR/openclaw/state/openclaw.json\"; do\n if [[ -f \"$cfg_file\" ]] && _ov_openclaw has-mcp-server \"$cfg_file\" 2>/dev/null; then\n _ov_openclaw remove-mcp-server \"$cfg_file\" 2>/dev/null\n cleaned=true\n fi\n done\n [[ \"$cleaned\" == \"true\" ]] && log_ok \"Legacy direct-config-write, MCP injection, and old artifacts cleaned\"\n log_ok \"OpenClaw integrated with OpenViking (official plugin via ClawHub → domestic npm → on-demand source build)\"\n ov_log_info \"重启 OpenClaw 以使更改生效\" \"Restart OpenClaw for changes to take effect\"\n}\n\nagent_openclaw_unbind() {\n local tpl=\"${AGENT_META[template_path]}\"\n local tpl_has_ov=false\n # Detect OpenViking injection in template (all known formats)\n if [[ -f \"$tpl\" ]] && grep -qE '# ── Step 5(\\.[0-9]+)?:.*[Oo]pen[Vv]iking|ov-openclaw-init\\.sh' \"$tpl\" 2>/dev/null; then\n tpl_has_ov=true\n fi\n local has_cfg=false\n for cfg_file in \"$OV_HOME/.openclaw/openclaw.json\" \"$OV_RUNTIME_DIR/openclaw/state/openclaw.json\"; do\n if [[ -f \"$cfg_file\" ]] && _ov_openclaw has-config \"$cfg_file\" 2>/dev/null; then\n has_cfg=true\n fi\n done\n local has_ext=false\n for ext_dir in \"$OV_HOME/.openclaw/extensions/openviking\" \"$OV_RUNTIME_DIR/openclaw/state/extensions/openviking\"; do\n [[ -d \"$ext_dir\" ]] && has_ext=true\n done\n if [[ \"$tpl_has_ov\" == \"false\" && \"$has_cfg\" == \"false\" && \"$has_ext\" == \"false\" ]]; then\n log_ok \"OpenClaw not integrated (nothing to remove)\"; return 0\n fi\n require_confirmation \"UNBIND OpenViking\" \"openclaw\" \"Remove OpenViking plugin install from template start.sh and clean up config files\" \"$RED\" || return 1\n if dry_run_msg \"Would remove OpenViking plugin config and legacy artifacts\"; then return 0; fi\n # Remove ALL OpenViking-related Step 5.x blocks from template start.sh\n if [[ \"$tpl_has_ov\" == \"true\" ]]; then\n backup_file \"$tpl\"\n _ov_openclaw unbind \"$tpl\" \"$OV_SHARED_DIR\"\n local remove_result=$?\n if [[ $remove_result -eq 0 ]]; then\n log_ok \"OpenViking injection blocks removed from template start.sh\"\n else\n log_warn \"Template removal completed with issues\"\n fi\n rm -f \"$OV_SHARED_DIR/ov-openclaw-init.sh\" && log_ok \"Removed standalone ov-openclaw-init.sh\"\n # Post-removal verification\n local residual_count\n residual_count=$(grep -ciE 'openviking' \"$tpl\" 2>/dev/null || true)\n if [[ \"$residual_count\" -gt 0 ]]; then\n log_warn \"WARNING: $residual_count residual OpenViking reference(s) still in template start.sh — manual review needed\"\n grep -niE 'openviking' \"$tpl\" 2>/dev/null | head -10 | while read -r line; do\n log_warn \" $line\"\n done\n else\n log_ok \"Verified: no OpenViking references remain in template start.sh\"\n fi\n # Sync to sandbox workspace — ISSUE-013: slash-safe sync (matches integrate)\n local sandbox_dir=\"\"\n for d in \"$OV_SANDBOX_DIR\"/openclaw-*/; do\n if [[ -d \"${d}process_dir\" ]]; then\n sandbox_dir=\"${d%/}\"\n break\n fi\n done\n if [[ -n \"$sandbox_dir\" ]]; then\n ov_sync_template_to_sandbox \"$tpl\" \"$sandbox_dir\" process_dir\n fi\n fi\n # Clean up config files (plugin entries, MCP servers, tool policy)\n local cleaned=false\n for ext_dir in \"$OV_HOME/.openclaw/extensions/openviking\" \"$OV_RUNTIME_DIR/openclaw/state/extensions/openviking\"; do\n if [[ -d \"$ext_dir\" ]]; then\n ov_safe_rm \"$ext_dir\" 2>/dev/null || true\n cleaned=true\n fi\n done\n for cfg_file in \"$OV_HOME/.openclaw/openclaw.json\" \"$OV_RUNTIME_DIR/openclaw/state/openclaw.json\"; do\n [[ -f \"$cfg_file\" ]] || continue\n _ov_openclaw clean-config \"$cfg_file\" 2>/dev/null | grep -q \"cleaned\" && cleaned=true\n done\n [[ \"$cleaned\" == \"true\" ]] && log_ok \"Config files cleaned (plugin entries, MCP servers, tool policy)\"\n # Remove on-demand plugin source + AGENTS.md\n if [[ -d \"$OV_RUNTIME_DIR/openclaw/openviking-plugin-source\" ]]; then\n ov_safe_rm \"$OV_RUNTIME_DIR/openclaw/openviking-plugin-source\" 2>/dev/null || true\n log_ok \"On-demand plugin source removed from persistent runtime location\"\n fi\n if [[ -d \"$OV_RUNTIME_DIR/openclaw/openviking-plugin-built\" ]]; then\n ov_safe_rm \"$OV_RUNTIME_DIR/openclaw/openviking-plugin-built\" 2>/dev/null || true\n log_ok \"Pre-built plugin cache removed from persistent runtime location\"\n fi\n # Revert OPENCLAW_STATE_DIR to default /tmp (only for old hardcoded format)\n # Skip revert if start.sh uses dynamic derivation (${OPENCLAW_DIR}/state) —\n # persistent path is the new default, reverting to /tmp would reintroduce the\n # TUI config-not-found bug.\n local _esc_rt=\"${OV_RUNTIME_DIR//|/\\\\|}\"\n if grep -q 'OPENCLAW_STATE_DIR.*\\${OPENCLAW_DIR}/state' \"$tpl\" 2>/dev/null; then\n log_info \"start.sh uses dynamic state dir — keeping persistent path (not reverting to /tmp)\"\n elif grep -q \"OPENCLAW_STATE_DIR.*${OV_RUNTIME_DIR}/openclaw/state\" \"$tpl\" 2>/dev/null; then\n sed -i \"s|OPENCLAW_STATE_DIR:-${_esc_rt}/openclaw/state|OPENCLAW_STATE_DIR:-/tmp/.openclaw|\" \"$tpl\"\n sed -i 's|# ── State dir: persistent (survives sandbox restarts) ──|# ── State dir: inside sandbox, not mounted outside ──|' \"$tpl\"\n log_ok \"Reverted OPENCLAW_STATE_DIR to default /tmp/.openclaw\"\n # Revert env.yaml too (only for old hardcoded format)\n local env_yaml=\"$OV_TEMPLATE_DIR/openclaw/env.yaml\"\n if [[ -f \"$env_yaml\" ]] && grep -q \"OPENCLAW_STATE_DIR.*${OV_RUNTIME_DIR}/openclaw/state\" \"$env_yaml\" 2>/dev/null; then\n sed -i \"s|OPENCLAW_STATE_DIR: \\\"${_esc_rt}/openclaw/state\\\"|OPENCLAW_STATE_DIR: \\\"/tmp/.openclaw\\\"|\" \"$env_yaml\"\n log_ok \"Reverted env.yaml OPENCLAW_STATE_DIR to /tmp/.openclaw\"\n fi\n fi\n # Clean up persistent state dir\n if [[ -d \"$OV_RUNTIME_DIR/openclaw/state\" ]]; then\n ov_safe_rm \"$OV_RUNTIME_DIR/openclaw/state\" 2>/dev/null || true\n log_ok \"Persistent state dir removed\"\n fi\n for agents_md in \"$OV_HOME/.openclaw/workspace/AGENTS.md\" \"$OV_RUNTIME_DIR/openclaw/state/workspace/AGENTS.md\"; do\n if [[ -f \"$agents_md\" ]] && grep -q \"OpenViking\" \"$agents_md\" 2>/dev/null; then\n rm -f \"$agents_md\"\n cleaned=true\n fi\n done\n log_ok \"OpenViking removed from OpenClaw\"\n ov_log_info \"重启 OpenClaw 以使更改生效\" \"Restart OpenClaw for changes to take effect\"\n}\n\nagent_openclaw_status() {\n local tpl=\"${AGENT_META[template_path]}\"\n\n local has_plugin=false\n if [[ -f \"$tpl\" ]] && grep -qE \"# ── Step 5(\\.[0-9]+)?:.*[Oo]pen[Vv]iking|ov-openclaw-init\\.sh\" \"$tpl\" 2>/dev/null; then\n has_plugin=true\n fi\n local has_legacy_cfg=false\n if [[ -f \"$tpl\" ]] && grep -q \"OpenViking plugin config\" \"$tpl\" 2>/dev/null; then\n has_legacy_cfg=true\n fi\n local has_mcp=false\n if [[ -f \"$tpl\" ]] && grep -q \"OpenViking MCP server injected\" \"$tpl\" 2>/dev/null; then\n has_mcp=true\n fi\n local has_legacy=false\n for ext_dir in \"$OV_HOME/.openclaw/extensions/openviking\" \"$OV_RUNTIME_DIR/openclaw/state/extensions/openviking\"; do\n [[ -d \"$ext_dir\" ]] && has_legacy=true\n done\n local tpl_has=\"$has_plugin\"\n local live_has=false\n [[ \"$has_plugin\" == \"true\" || \"$has_legacy_cfg\" == \"true\" || \"$has_mcp\" == \"true\" || \"$has_legacy\" == \"true\" ]] && live_has=true\n local d_integrated=\"\"\n if [[ \"$has_plugin\" == \"true\" ]]; then\n local src_desc=\"ClawHub\"\n grep -q \"clawhub:@openviking/openclaw-plugin\" \"$tpl\" 2>/dev/null || src_desc=\"npm mirror\"\n if [[ -d \"$OV_RUNTIME_DIR/openclaw/openviking-plugin-source\" ]]; then\n src_desc=\"${src_desc} + on-demand source fallback\"\n fi\n if [[ -d \"$OV_RUNTIME_DIR/openclaw/openviking-plugin-built\" ]]; then\n src_desc=\"${src_desc} + pre-built cache (fast recovery)\"\n fi\n if grep -q \"openviking setup .*--json\" \"$tpl\" 2>/dev/null; then\n d_integrated=\"Official plugin install in start.sh (${src_desc}, contextEngine slot, setup --json contract) ✓\"\n else\n d_integrated=\"Official plugin install in start.sh (${src_desc}, contextEngine slot) ✓\"\n fi\n fi\n local d_live=\"\"\n if [[ \"$has_legacy_cfg\" == \"true\" ]]; then\n d_live=\"Legacy direct-config-write found — run integrate to upgrade to official plugin install\"\n elif [[ \"$has_mcp\" == \"true\" ]]; then\n d_live=\"Legacy MCP injection found — run integrate to upgrade to official plugin install\"\n elif [[ \"$has_legacy\" == \"true\" ]]; then\n d_live=\"Legacy plugin artifacts found — run unbind to clean\"\n fi\n agent::report_status \"${AGENT_META[name]}\" \"$tpl_has\" \"$live_has\" \\\n \"$d_integrated\" \"\" \"$d_live\" \"No OpenViking integration found\"\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":16656,"content_sha256":"2a4a042c9b255b7b25da9784f9971bd8c77c78e5d1ba5d798dd804f0bcdf6edc"},{"filename":"scripts/agents/opencode.sh","content":"#!/bin/bash\n# agents/opencode.sh — OpenCode agent subclass (Plugin + openviking-config.json)\n# Inherits from lib/base.sh; overrides: integrate, unbind, status\nagent_opencode_register() {\n agent::set_meta name \"opencode\"\n agent::set_meta display_name \"OpenCode\"\n agent::set_meta sandbox_pattern \"opencode-*\"\n agent::set_meta template_path \"$OV_TEMPLATE_DIR/opencode/start.sh\"\n agent::set_meta config_path \"$OV_SANDBOX_DIR/opencode-*/.config/opencode/opencode.json\"\n agent::set_meta mechanism \"Plugin + openviking-config.json\"\n registry_add \"opencode\"\n}\n_ov_opencode() { \"$OV_PY\" \"$OV_PY_DIR/agents/ov_opencode.py\" \"$@\"; }\n# Pre-filling skips the ~60s npm install on undeploy+deploy.\n_ov_prepopulate_cache() {\n local home_dir=\"$1\"\n local plugin_dst=\"${home_dir}/.config/opencode/node_modules/@openviking/opencode-plugin\"\n local cache_pkg=\"${home_dir}/.cache/opencode/packages/@openviking/opencode-plugin@latest\"\n if [[ ! -d \"$plugin_dst\" || ! -f \"$plugin_dst/package.json\" ]]; then return 0; fi\n if [[ -d \"$cache_pkg/node_modules/@openviking/opencode-plugin\" ]]; then return 0; fi\n mkdir -p \"$cache_pkg/node_modules/@openviking\"\n cp -a \"$plugin_dst\" \"$cache_pkg/node_modules/@openviking/opencode-plugin\"\n cat > \"$cache_pkg/package.json\" \u003c\u003c'OVCACHEPKG'\n{\"dependencies\":{\"@openviking/opencode-plugin\":\"__OV_OPENCODE_PLUGIN_VER__\"}}\nOVCACHEPKG\n cat > \"$cache_pkg/package-lock.json\" \u003c\u003c'OVCACHELOCK'\n{\"name\":\"@openviking/opencode-plugin@latest\",\"lockfileVersion\":3,\"requires\":true,\"packages\":{\"\":{\"dependencies\":{\"@openviking/opencode-plugin\":\"__OV_OPENCODE_PLUGIN_VER__\"}},\"node_modules/@openviking/opencode-plugin\":{\"version\":\"__OV_OPENCODE_PLUGIN_VER__\",\"resolved\":\"https://registry.npmjs.org/@openviking/opencode-plugin/-/opencode-plugin-__OV_OPENCODE_PLUGIN_VER__.tgz\",\"integrity\":\"sha512-jEPnxARNmP5lMNTrEQ42KP5SkKHdF3O5I99HnMHGHC0MyHWT9zn3FanAXT4cjaeC2d4Bzt7Nd9IL50SUZYkhGg==\",\"license\":\"Apache-2.0\"}}}\nOVCACHELOCK\n cat > \"$cache_pkg/node_modules/.package-lock.json\" \u003c\u003c'OVNMLOCK'\n{\"name\":\"@openviking/opencode-plugin@latest\",\"lockfileVersion\":3,\"requires\":true,\"packages\":{\"node_modules/@openviking/opencode-plugin\":{\"version\":\"__OV_OPENCODE_PLUGIN_VER__\",\"resolved\":\"https://registry.npmjs.org/@openviking/opencode-plugin/-/opencode-plugin-__OV_OPENCODE_PLUGIN_VER__.tgz\",\"integrity\":\"sha512-jEPnxARNmP5lMNTrEQ42KP5SkKHdF3O5I99HnMHGHC0MyHWT9zn3FanAXT4cjaeC2d4Bzt7Nd9IL50SUZYkhGg==\",\"license\":\"Apache-2.0\"}}}\nOVNMLOCK\n local _ver; _ver=$(ov_probe_package_version \"$plugin_dst/package.json\") || _ver=\"${OV_OPENCODE_PLUGIN_VER:-latest}\"\n sed -i \"s/__OV_OPENCODE_PLUGIN_VER__/$_ver/g\" \"$cache_pkg/package.json\" \"$cache_pkg/package-lock.json\" \"$cache_pkg/node_modules/.package-lock.json\"\n # Use the resolved real version (not the possibly-latest constant) for the\n # versioned cache dir — avoids self-copy when OV_OPENCODE_PLUGIN_VER=latest.\n local cache_ver=\"${home_dir}/.cache/opencode/packages/@openviking/opencode-plugin@${_ver}\"\n if [[ \"$_ver\" != \"latest\" && ! -d \"$cache_ver/node_modules/@openviking/opencode-plugin\" ]]; then\n cp -a \"$cache_pkg\" \"$cache_ver\"\n sed -i \"s/@openviking/opencode-plugin@latest/@openviking/opencode-plugin@${_ver}/g\" \"$cache_ver/package-lock.json\" \"$cache_ver/node_modules/.package-lock.json\" 2>/dev/null || true\n fi\n}\nagent_opencode_integrate() {\n local tpl=\"${AGENT_META[template_path]}\"\n [[ ! -f \"$tpl\" ]] && { log_error \"OpenCode template start.sh not found: $tpl\"; return 1; }\n local ov_pkg=\"@openviking/opencode-plugin\"\n # ISSUE-011: idempotency must verify template AND live — a template-only grep\n # falsely reported \"already integrated\" for partially-unbound/restored states\n local tpl_has=false live_has=false\n if grep -q \"ov-opencode-init.sh\" \"$tpl\" 2>/dev/null || { [[ -f \"$OV_SHARED_DIR/ov-opencode-init.sh\" ]] && grep -q \"OpenViking integration\" \"$tpl\" 2>/dev/null; }; then\n tpl_has=true\n fi\n local _sbx_once; _sbx_once=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n if [[ -n \"$_sbx_once\" && -f \"$_sbx_once/.config/opencode/opencode.json\" ]]; then\n check_json_mcp \"$_sbx_once/.config/opencode/opencode.json\" && live_has=true\n _ov_opencode check-plugin \"$_sbx_once/.config/opencode/opencode.json\" 2>/dev/null && live_has=true\n fi\n if [[ \"$tpl_has\" == \"true\" && \"$live_has\" == \"true\" ]]; then\n log_ok \"OpenCode already integrated with OpenViking (template + live)\"\n return 0\n fi\n require_confirmation \"Integrate OpenViking (npm + domestic mirror fallback)\" \"opencode\" \\\n \"Install @openviking/opencode-plugin (npm online first, on-demand GitHub mirror fallback) + plugin SDK + openviking-config.json to template start.sh (persistent)\" \\\n || return 1\n if dry_run_msg \"Would install OpenViking plugin (npm → on-demand GitHub fallback) to $tpl and live sandbox\"; then return 0; fi\n local sandbox; sandbox=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n if [[ -n \"$sandbox\" && -f \"${sandbox}/.config/opencode/opencode.json\" ]]; then\n local ov_npm_dir=\"${sandbox}/.config/opencode\"\n local NPM_REGISTRY; NPM_REGISTRY=$(ov_first_npm_registry)\n echo \"registry=${NPM_REGISTRY}\" > \"${ov_npm_dir}/.npmrc\"\n log_ok \".npmrc created ($NPM_REGISTRY)\"\n local _ov_ver; _ov_ver=$(ov_effective_plugin_ver \"${ov_npm_dir}/node_modules/@openviking/opencode-plugin/package.json\" \"$OV_OPENCODE_PLUGIN_VER\")\n ov_deploy_plugin_tiered \"$ov_pkg\" \"$ov_npm_dir\" \"$NPM_REGISTRY\" \"opencode-plugin\" \"@openviking/opencode-plugin\" \"$_ov_ver\" || true\n case \"$_ov_tiered_result\" in\n existing) log_ok \"Plugin already installed (cache hit)\" ;;\n cache) log_ok \"Plugin from shared cache\" ;;\n npm) log_ok \"Plugin from npm\" ;;\n provision) log_ok \"Plugin provisioned from GitHub\" ;;\n *)\n log_warn \"All plugin installation methods failed\"\n if [[ ! -d \"${ov_npm_dir}/node_modules/@openviking/opencode-plugin\" ]]; then\n log_error \"Plugin not installed and no existing copy — aborting integrate\"\n return 1\n fi\n ;;\n esac\n # Agent-specific: ensure @opencode-ai SDK is available (from cache or npm)\n if [[ ! -d \"${ov_npm_dir}/node_modules/@opencode-ai\" ]]; then\n local sdk_cache; sdk_cache=$(ov_plugin_cache_path \"@opencode-ai\")\n if [[ -d \"$sdk_cache\" ]]; then\n cp -a \"$sdk_cache\" \"${ov_npm_dir}/node_modules/@opencode-ai\"\n log_ok \"Plugin SDK from shared cache\"\n elif command -v npm &>/dev/null || command -v \"${OV_NPM:-npm}\" &>/dev/null; then\n (cd \"$ov_npm_dir\" && \"${OV_NPM:-npm}\" install \"@opencode-ai/plugin@$OV_PLUGIN_SDK_VER\" --registry=\"$NPM_REGISTRY\" --no-audit --no-fund 2>&1 | tail -3) && \\\n log_ok \"Plugin SDK from npm\"\n if [[ -d \"${ov_npm_dir}/node_modules/@opencode-ai\" ]]; then\n mkdir -p \"$(dirname \"$sdk_cache\")\"\n ov_safe_rm \"$sdk_cache\" 2>/dev/null || true\n cp -a \"${ov_npm_dir}/node_modules/@opencode-ai\" \"$sdk_cache\"\n fi\n fi\n fi\n local cf=\"${sandbox}/.config/opencode/opencode.json\"\n backup_file \"$cf\"\n _ov_opencode register-plugin \"$cf\" \"$OV_OPENCODE_PLUGIN_VER\"\n log_ok \"Plugin registered in opencode.json\"\n create_ov_config \"$sandbox\"\n _ov_prepopulate_cache \"$sandbox\" | sed 's/^/ /'\n local oc_deps_src=\"${sandbox}/.opencode\"\n local oc_deps_dst=\"${OV_PLUGIN_CACHE_DIR%/openviking-plugins}/opencode-deps\"\n if [[ -d \"$oc_deps_src/node_modules/@opencode-ai/plugin\" ]]; then\n mkdir -p \"$oc_deps_dst\"\n cp -a \"$oc_deps_src/node_modules\" \"$oc_deps_dst/node_modules\"\n cp -a \"$oc_deps_src/package.json\" \"$oc_deps_dst/package.json\" 2>/dev/null\n cp -a \"$oc_deps_src/package-lock.json\" \"$oc_deps_dst/package-lock.json\" 2>/dev/null\n log_ok \"OpenCode deps saved to shared cache\"\n fi\n local npm_cache_src=\"${sandbox}/.npm/_cacache\"\n local npm_cache_dst=\"${OV_PLUGIN_CACHE_DIR%/openviking-plugins}/opencode-npm-cache\"\n if [[ -d \"$npm_cache_src\" ]]; then\n mkdir -p \"$npm_cache_dst\"\n cp -a \"$npm_cache_src\" \"$npm_cache_dst/_cacache\"\n log_ok \"npm cache saved to shared cache\"\n fi\n else\n log_info \"No live OpenCode sandbox found; plugin will be installed on next start\"\n local _prov_dir; _prov_dir=$(mktemp -d /tmp/ov-prov.XXXXXX)\n ov_plugin_provision \"opencode-plugin\" \"$_prov_dir\" || { ov_safe_rm \"$_prov_dir\" 2>/dev/null || true; return 1; }\n ov_plugin_cache_put \"$ov_pkg\" \"$_prov_dir\" || log_warn \"ISSUE-019: cache write failed for $ov_pkg\"\n ov_safe_rm \"$_prov_dir\" 2>/dev/null || true\n log_ok \"opencode-plugin provisioned to shared cache\"\n fi\n local env_yaml=\"$OV_TEMPLATE_DIR/opencode/env.yaml\"\n if [[ -f \"$env_yaml\" ]]; then\n backup_file \"$env_yaml\"\n _ov_opencode fix-env-yaml \"$env_yaml\"\n log_ok \"OpenCode env.yaml updated (nodejs accessible in sandbox)\"\n fi\n backup_file \"$tpl\"\n _ov_opencode integrate-template \"$tpl\" \"$OV_PLUGIN_SDK_VER\" \"$OV_OPENCODE_PLUGIN_VER\" \"$OV_NPM_REGISTRY_DEFAULT\" \"$OV_PLUGIN_CACHE_DIR\" \"$OV_SHARED_DIR\" \"$OV_TEMPLATE_DIR\"\n log_ok \"OpenCode template updated with cache-first plugin deployment\"\n ov_log_info \"重启 OpenCode 以激活插件\" \"Restart OpenCode for plugin to activate\"\n}\nagent_opencode_unbind() {\n local tpl=\"${AGENT_META[template_path]}\"\n local tpl_has_ov=false\n if grep -q \"OpenViking integration\\|@openviking/opencode-plugin\\|openviking plugin\\|plugins/opencode\" \"$tpl\" 2>/dev/null; then\n tpl_has_ov=true\n else\n has_ov_injection \"$tpl\" 2>/dev/null && tpl_has_ov=true\n fi\n local sandbox; sandbox=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n local sandbox_has_ov=false\n if [[ -n \"$sandbox\" && -f \"${sandbox}/.config/opencode/opencode.json\" ]]; then\n check_json_mcp \"${sandbox}/.config/opencode/opencode.json\" && sandbox_has_ov=true\n _ov_opencode check-plugin \"${sandbox}/.config/opencode/opencode.json\" 2>/dev/null && sandbox_has_ov=true\n fi\n [[ \"$tpl_has_ov\" == \"false\" && \"$sandbox_has_ov\" == \"false\" ]] && { log_ok \"OpenCode not integrated (nothing to remove)\"; return 0; }\n require_confirmation \"UNBIND OpenViking\" \"opencode\" \"Remove OpenViking plugin + npm packages + config from template and sandbox\" \"$RED\" || return 1\n if dry_run_msg \"Would remove OpenViking integration from template and sandbox\"; then return 0; fi\n if [[ \"$tpl_has_ov\" == \"true\" ]]; then\n backup_file \"$tpl\"\n _ov_opencode unbind-template \"$tpl\"\n log_ok \"OpenViking integration block removed from template\"\n rm -f \"$OV_SHARED_DIR/ov-opencode-init.sh\" && log_ok \"Removed ov-opencode-init.sh\"\n fi\n local env_yaml=\"$OV_TEMPLATE_DIR/opencode/env.yaml\"\n if [[ -f \"$env_yaml\" ]]; then\n backup_file \"$env_yaml\"\n _ov_opencode restore-env-yaml \"$env_yaml\"\n log_ok \"OpenCode env.yaml restored\"\n fi\n if [[ -n \"$sandbox\" ]]; then\n local cf=\"${sandbox}/.config/opencode/opencode.json\"\n if [[ -f \"$cf\" ]]; then\n backup_file \"$cf\"\n _ov_opencode unbind-live-config \"$cf\"\n log_ok \"OpenViking plugin removed from live sandbox\"\n fi\n # ISSUE-009: only remove OpenViking-owned packages — never the whole node_modules\n # (users may have extra packages installed there)\n ov_safe_rm \"${sandbox}/.config/opencode/node_modules/@openviking\" 2>/dev/null || true\n ov_safe_rm \"${sandbox}/.config/opencode/node_modules/@opencode-ai\" 2>/dev/null || true\n log_ok \"OpenViking plugin packages removed (user packages in node_modules kept)\"\n # OV-owned files: openviking-config.json and the .npmrc written by integrate.sh\n rm -f \"${sandbox}/.config/opencode/.npmrc\" \\\n \"${sandbox}/.config/opencode/openviking-config.json\" 2>/dev/null\n ov_safe_rm \"${sandbox}/.config/opencode/plugins/openviking\" 2>/dev/null || true\n rm -f \"${sandbox}/.config/opencode/plugins/openviking.js\" 2>/dev/null\n ov_safe_rm \"${sandbox}/.opencode/node_modules/@openviking\" 2>/dev/null || true\n ov_safe_rm \"${sandbox}/.cache/opencode/packages/@openviking\" 2>/dev/null || true\n log_ok \"npm packages, .opencode/, .cache/ cleaned up\"\n fi\n local oc_deps_cache=\"${OV_PLUGIN_CACHE_DIR%/openviking-plugins}/opencode-deps\"\n if [[ -d \"$oc_deps_cache\" ]]; then\n ov_safe_rm \"$oc_deps_cache\" 2>/dev/null || true\n log_ok \"OpenCode deps shared cache cleaned up\"\n fi\n local npm_cache=\"${OV_PLUGIN_CACHE_DIR%/openviking-plugins}/opencode-npm-cache\"\n if [[ -d \"$npm_cache\" ]]; then\n ov_safe_rm \"$npm_cache\" 2>/dev/null || true\n log_ok \"npm cache shared cache cleaned up\"\n fi\n ov_log_info \"重启 OpenCode 以使更改完全生效\" \"Restart OpenCode for changes to take full effect\"\n}\nagent_opencode_status() {\n local tpl=\"${AGENT_META[template_path]}\"\n local tpl_has_ov=false\n if grep -q \"OpenViking integration\\|@openviking/opencode-plugin\\|openviking plugin\\|plugins/opencode\" \"$tpl\" 2>/dev/null; then\n tpl_has_ov=true\n else\n has_ov_injection \"$tpl\" 2>/dev/null && tpl_has_ov=true\n fi\n local sandbox; sandbox=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n [[ -z \"$sandbox\" ]] && { ov_status \"opencode\" \"unknown\" \"sandbox not found\"; return; }\n local cf=\"${sandbox}/.config/opencode/opencode.json\"\n local sandbox_has_ov=false\n if [[ -f \"$cf\" ]]; then\n if _ov_opencode check-plugin \"$cf\" 2>/dev/null; then\n sandbox_has_ov=true\n elif check_json_mcp \"$cf\" 2>/dev/null; then\n sandbox_has_ov=true\n fi\n fi\n local plugin_installed=false\n local plugin_source=\"\"\n if [[ -d \"${sandbox}/.cache/opencode/packages/@openviking/opencode-plugin@latest/node_modules/@openviking/opencode-plugin\" ]] || \\\n [[ -d \"${sandbox}/.cache/opencode/packages/@openviking/opencode-plugin@${OV_OPENCODE_PLUGIN_VER}/node_modules/@openviking/opencode-plugin\" ]]; then\n plugin_installed=true\n plugin_source=\"cache\"\n elif [[ -d \"${sandbox}/.config/opencode/node_modules/@openviking/opencode-plugin\" ]]; then\n plugin_installed=true\n plugin_source=\"runtime\"\n elif [[ -d \"${sandbox}/.config/opencode/node_modules/@opencode-ai\" ]]; then\n plugin_installed=true\n plugin_source=\"npm\"\n elif [[ -f \"${sandbox}/.config/opencode/plugins/openviking.js\" ]]; then\n plugin_installed=true\n plugin_source=\"legacy\"\n fi\n local d_integrated=\"\"\n if [[ \"$plugin_installed\" == \"true\" ]]; then\n if [[ \"$plugin_source\" == \"cache\" ]]; then\n d_integrated=\"Official @openviking/opencode-plugin (.cache pre-populated, template + live)\"\n else\n d_integrated=\"Official @openviking/opencode-plugin (runtime install, template + live)\"\n fi\n else\n d_integrated=\"MCP: $(get_json_mcp_url \"$cf\" 2>/dev/null || echo \"unknown\")\"\n fi\n local d_template=\"Plugin configured (template only, restart to activate)\"\n [[ \"$plugin_installed\" == \"true\" ]] && d_template=\"Plugin installed (template + live), restart to activate hooks\"\n agent::report_status \"${AGENT_META[name]}\" \"$tpl_has_ov\" \"$sandbox_has_ov\" \\\n \"$d_integrated\" \"$d_template\" \\\n \"Plugin/MCP in live only, lost on restart\" \\\n \"No OpenViking integration found\"\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":14859,"content_sha256":"bd65491e99d9508162107ea5bbf1c0028da4f9800b0823597620a8650750916b"},{"filename":"scripts/agents/prime_agent.sh","content":"#!/bin/bash\n# agents/prime_agent.sh — Prime Agent agent subclass (pi-coding-agent-extension)\n# Inherits from lib/base.sh; overrides integrate/unbind/status.\nagent_prime_agent_register() {\n agent::set_meta name \"prime_agent\"\n agent::set_meta display_name \"Prime Agent\"\n agent::set_meta sandbox_pattern \"prime-agent-*\"\n agent::set_meta template_path \"$OV_TEMPLATE_DIR/prime-agent/start.sh\"\n agent::set_meta config_path \"$OV_HOME/.prime-agent/config.json\"\n agent::set_meta mechanism \"pi-coding-agent-extension\"\n registry_add \"prime_agent\"\n}\n_ov_prime_agent() { \"$OV_PY\" \"$OV_PY_DIR/agents/ov_prime_agent.py\" \"$@\"; }\n\nagent_prime_agent_integrate() {\n local tpl=\"${AGENT_META[template_path]}\"\n local sandbox; sandbox=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n [[ -z \"$sandbox\" ]] && { log_error \"Prime Agent sandbox not found\"; return 1; }\n\n local pa_runtime=\"$OV_RUNTIME_DIR/prime-agent\"\n local ext_dst=\"${pa_runtime}/agent-data/extensions/openviking\"\n local persist_src=\"${pa_runtime}/openviking-extension\"\n\n ov_plugin_provision \"pi-coding-agent-extension\" \"$persist_src\" || return 1\n if [[ \"${DRY_RUN:-false}\" != \"true\" ]]; then\n touch \"$persist_src\" # Refresh TTL timestamp (cache valid for 24h)\n fi\n\n # Post-provision fix: upstream refactor (commit aa3ee2f) moved shared/ files out of\n # the extension to agent-plugins/servers/shared/ and examples/claude-code-memory-plugin/scripts/shared/.\n # The pack-time step that copies them back is not run by ov_plugin_provision, so we\n # download them here to prevent ERR_MODULE_NOT_FOUND at runtime.\n ov_sync_shared_files \"$persist_src\"\n local tpl_has_ov=false\n grep -q \"OpenViking memory extension\" \"$tpl\" 2>/dev/null && tpl_has_ov=true\n local live_has_ov=false\n [[ -f \"${ext_dst}/index.ts\" ]] && live_has_ov=true\n\n if [[ \"$tpl_has_ov\" == \"true\" && \"$live_has_ov\" == \"true\" ]]; then\n log_ok \"Prime Agent already integrated with OpenViking (pi-coding-agent-extension, template + live)\"\n return 0\n fi\n require_confirmation \"Integrate OpenViking\" \"prime-agent\" \"Install @openviking/pi-coding-agent-extension (TypeScript extension with native hooks: auto-recall, auto-capture, context takeover) + template start.sh\" || return 1\n if dry_run_msg \"Would install pi-coding-agent-extension to $ext_dst + persist at $persist_src + template $tpl\"; then return 0; fi\n log_ok \"Extension source installed on demand at persistent location: $persist_src\"\n # Live sandbox (immediate effect)\n mkdir -p \"$ext_dst\"\n cp -a \"$persist_src\"/* \"$ext_dst/\"\n log_ok \"Extension installed to live extensions directory: $ext_dst\"\n # Template start.sh (persistent)\n if [[ \"$tpl_has_ov\" == \"false\" ]]; then\n if [[ -f \"$tpl\" ]]; then\n backup_file \"$tpl\"\n _ov_prime_agent integrate \"$tpl\" \"$OV_SHARED_DIR\"\n if [[ $? -ne 0 ]]; then\n log_error \"Failed to inject OpenViking block into template start.sh (anchor not found)\"\n return 1\n fi\n log_ok \"OpenViking integration block injected into template start.sh\"\n tpl_has_ov=true\n else\n log_warn \"Template $tpl missing — skipping template injection (live only, lost on restart)\"\n fi\n fi\n # Sync template to sandbox so a restart preserves integration\n if [[ \"$tpl_has_ov\" == \"true\" ]]; then\n local proc_dir\n for proc_dir in \"${sandbox}/process_dir\" \"${sandbox}/.process_dir\"; do\n if [[ -f \"${proc_dir}/start.sh\" ]]; then\n cp \"$tpl\" \"${proc_dir}/start.sh\"\n log_ok \"Template start.sh synced to sandbox ${proc_dir}\"\n break\n fi\n done\n fi\n ov_log_info \"重启 Prime Agent 以激活扩展(自动召回 + 自动捕获 + 7 工具)\" \"Restart Prime Agent for extension to activate (auto-recall + auto-capture + 7 tools)\"\n}\n\nagent_prime_agent_unbind() {\n local tpl=\"${AGENT_META[template_path]}\"\n local sandbox; sandbox=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n local pa_runtime=\"$OV_RUNTIME_DIR/prime-agent\"\n local ext_dst=\"${pa_runtime}/agent-data/extensions/openviking\"\n local persist_src=\"${pa_runtime}/openviking-extension\"\n\n local tpl_has=false\n grep -q \"OpenViking memory extension\\|ov-prime-agent-init.sh\" \"$tpl\" 2>/dev/null && tpl_has=true\n local live_has=false\n [[ -f \"${ext_dst}/index.ts\" ]] && live_has=true\n [[ -n \"$sandbox\" && -d \"${sandbox}/agent-data/extensions/openviking\" ]] && live_has=true\n\n if [[ \"$tpl_has\" == \"false\" && \"$live_has\" == \"false\" ]]; then\n log_ok \"Prime Agent has no OpenViking integration to unbind\"\n return 0\n fi\n require_confirmation \"UNBIND OpenViking\" \"prime-agent\" \"Remove pi-coding-agent-extension from extensions dir + persistent source + template start.sh injection\" \"$RED\" || return 1\n if dry_run_msg \"Would remove OpenViking extension from $ext_dst + $persist_src + template $tpl\"; then return 0; fi\n if [[ -d \"$ext_dst\" ]]; then\n ov_safe_rm \"$ext_dst\" 2>/dev/null || true\n log_ok \"Extension removed from live extensions directory: $ext_dst\"\n fi\n if [[ -n \"$sandbox\" ]]; then\n local sbx_ext=\"${sandbox}/agent-data/extensions/openviking\"\n if [[ -d \"$sbx_ext\" ]]; then\n ov_safe_rm \"$sbx_ext\" 2>/dev/null || true\n log_ok \"Extension removed from sandbox extensions directory: $sbx_ext\"\n fi\n local sbx_state=\"${sandbox}/.openviking\"\n if [[ -d \"$sbx_state\" ]]; then\n ov_safe_rm \"$sbx_state\" 2>/dev/null || true\n log_ok \"OpenViking state directory removed from sandbox: $sbx_state\"\n fi\n fi\n # Preserve persistent source (cache for fast re-integration)\n if [[ -d \"$persist_src\" ]]; then\n log_ok \"Persistent extension source preserved at $persist_src (cache for fast re-integration)\"\n fi\n if [[ \"$tpl_has\" == \"true\" && -f \"$tpl\" ]]; then\n backup_file \"$tpl\"\n _ov_prime_agent unbind-template \"$tpl\" \"$OV_SHARED_DIR\"\n if ! grep -q \"ov-prime-agent-init.sh\\|OpenViking memory extension\\|OpenViking integration\" \"$tpl\" 2>/dev/null; then\n log_ok \"OpenViking block removed from template start.sh\"\n else\n log_error \"Failed to remove OpenViking block from template start.sh (block still present)\"\n return 1\n fi\n rm -f \"$OV_SHARED_DIR/ov-prime-agent-init.sh\" && log_ok \"Removed standalone ov-prime-agent-init.sh\"\n fi\n # Sync template to sandbox\n if [[ -n \"$sandbox\" ]]; then\n for proc_dir in \"${sandbox}/process_dir\" \"${sandbox}/.process_dir\"; do\n if [[ -f \"${proc_dir}/start.sh\" ]]; then\n cp \"$tpl\" \"${proc_dir}/start.sh\"\n log_ok \"Template synced to sandbox ${proc_dir}\"\n break\n fi\n done\n fi\n ov_log_info \"重启 Prime Agent 以使更改生效\" \"Restart Prime Agent for changes to take effect\"\n}\n\nagent_prime_agent_status() {\n local tpl=\"${AGENT_META[template_path]}\"\n local sandbox; sandbox=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n local pa_runtime=\"$OV_RUNTIME_DIR/prime-agent\"\n local ext_dst=\"${pa_runtime}/agent-data/extensions/openviking\"\n\n local tpl_has=false\n grep -q \"OpenViking memory extension\\|ov-prime-agent-init.sh\" \"$tpl\" 2>/dev/null && tpl_has=true\n local live_has=false\n [[ -f \"${ext_dst}/index.ts\" ]] && live_has=true\n # ISSUE-012: status detection must match unbind (sandbox extension dir counted there)\n if [[ \"$live_has\" == \"false\" && -n \"$sandbox\" && -d \"${sandbox}/agent-data/extensions/openviking\" ]]; then\n live_has=true\n fi\n\n agent::report_status \"${AGENT_META[name]}\" \"$tpl_has\" \"$live_has\" \\\n \"pi-coding-agent-extension (TypeScript extension, native hooks: auto-recall + auto-capture + context takeover, template + live)\" \\\n \"pi-coding-agent-extension configured (template only, restart to activate)\" \\\n \"pi-coding-agent-extension (live only, lost on restart)\" \\\n \"No OpenViking extension\"\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":7654,"content_sha256":"2016e07877f1d53dbb07217c211daa73090ec6b501c36c07e18ec2d19c7d97ba"},{"filename":"scripts/agents/workswarm.sh","content":"#!/bin/bash\n# agents/jiuwenswarm.sh — WorkSwarm agent subclass (dual-channel: provider + MCP)\nagent_workswarm_register() {\n agent::set_meta name \"workswarm\"\n agent::set_meta display_name \"WorkSwarm\"\n agent::set_meta sandbox_pattern \"jiuwenswarm-*\"\n agent::set_meta template_path \"$OV_TEMPLATE_DIR/jiuwenswarm/start.sh\"\n agent::set_meta config_path \"$OV_HOME/.jiuwenswarm/config.json\"\n agent::set_meta mechanism \"Dual-channel: provider + MCP\"\n registry_add \"workswarm\"\n}\n\n_ov_workswarm() { \"$OV_PY\" \"$OV_PY_DIR/agents/ov_workswarm.py\" \"$@\"; }\n\n_jw_agents_md() {\n cat \u003c\u003c'JWAGENTS'\n# Agent Instructions\n## OpenViking Long-Term Memory\nDual-channel: (1) **Native memory provider** (`memory.engine: external`, `is_proactive: true`): auto-recalls/stores context. (2) **MCP server** (13 tools): `search`, `recall`, `find`, `read`, `remember`, `add_resource`, `grep`, `glob`, `forget`, `health`, `list`, `list_watches`, `cancel_watch`.\n### Auto-Recall (conversation start)\nNative engine auto-prefetches. For deeper: `search` (mode=\"context\"), `recall` (type-quota), `find` (min_score=0).\n### Proactive Search (during tasks)\n1. `search` (mode=\"context\") — past knowledge, solutions, decisions. 2. `recall` — structured type-quota. 3. `find` — fast ranked list. 4. `read` — expand viking:// URIs. 5. `grep`/`glob` — exact text/filename.\n### Auto-Capture (after meaningful exchanges)\n1. `remember` — user shares preferences/facts/decisions or asks to remember. 2. `add_resource` — import files/URLs/repos. 3. Never echo credentials.\n### Repo Context\n`add_resource` repo path to index; `search` for prior work.\nJWAGENTS\n}\n\n_jw_mutate_config() {\n local action=\"$1\" cfg=\"$2\" mcp_url=\"${3:-}\"\n _ov_workswarm mutate-config \"$action\" \"$cfg\" \"$mcp_url\"\n}\n\n_jw_patch_iface_apply() {\n local file=\"$1\"\n _ov_workswarm patch-iface-apply \"$file\"\n}\n\n_jw_patch_iface_revert() {\n local file=\"$1\"\n _ov_workswarm patch-iface-revert \"$file\"\n}\n\n_jw_patch_prov_apply() {\n local file=\"$1\"\n _ov_workswarm patch-prov-apply \"$file\"\n}\n\n_jw_patch_prov_revert() {\n local file=\"$1\"\n _ov_workswarm patch-prov-revert \"$file\"\n}\n\n_jw_patch_session_apply() {\n local file=\"$1\"\n _ov_workswarm patch-session-apply \"$file\"\n}\n\n_jw_patch_session_revert() {\n local file=\"$1\"\n _ov_workswarm patch-session-revert \"$file\"\n}\n\n_jw_patch_commit_apply() {\n local file=\"$1\"\n _ov_workswarm patch-commit-apply \"$file\"\n}\n\n_jw_patch_commit_revert() {\n local file=\"$1\"\n _ov_workswarm patch-commit-revert \"$file\"\n}\n\nagent_workswarm_integrate() {\n local sandbox; sandbox=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n [[ -z \"$sandbox\" ]] && { log_error \"WorkSwarm sandbox not found\"; return 1; }\n local cf=\"${sandbox}/.jiuwenswarm/config/config.yaml\"\n [[ ! -f \"$cf\" ]] && { log_error \"Config not found: $cf\"; return 1; }\n local tpl=\"${AGENT_META[template_path]}\"\n local already=false\n if grep -q \"MEMORY_ENGINE:-both\\|MEMORY_ENGINE:-external\\|MEMORY_EXTERNAL_PROVIDER:-openviking\" \"$cf\" 2>/dev/null; then\n already=true\n elif [[ -f \"$tpl\" ]] && grep -q \"MEMORY_EXTERNAL_PROVIDER=openviking\" \"$tpl\" 2>/dev/null; then\n already=true\n fi\n if [[ \"$already\" == \"true\" ]]; then\n if [[ -f \"$tpl\" ]] && grep -q \"OpenViking native memory provider injection\\|ov-jiuwenswarm-init.sh\" \"$tpl\" 2>/dev/null; then\n if grep -q \"OPENVIKING_ACCOUNT:-root\" \"$cf\" 2>/dev/null; then\n # ISSUE-008: dry-run must never write — gate the account fixup too\n if [[ \"${DRY_RUN:-false}\" == \"true\" ]]; then\n log_info \"[DRY-RUN] would normalize OPENVIKING_ACCOUNT to default in $cf\"\n else\n sed -i \"s/OPENVIKING_ACCOUNT:-root/OPENVIKING_ACCOUNT:-default/\" \"$cf\"\n fi\n fi\n return 0\n fi\n fi\n require_confirmation \"Integrate OpenViking\" \"workswarm\" \"Add OpenViking native memory provider + MCP server to sandbox config + template start.sh\" || return 1\n if dry_run_msg \"Would add OpenViking native memory provider + MCP server to $cf and $tpl\"; then return 0; fi\n backup_file \"$cf\"\n _jw_mutate_config apply \"$cf\" \"$OV_MCP_URL\"\n if [[ -f \"$tpl\" ]] && ! grep -q \"OpenViking native memory provider injection\\|ov-jiuwenswarm-init.sh\" \"$tpl\" 2>/dev/null; then\n backup_file \"$tpl\"\n local _agents_tmp; _agents_tmp=$(mktemp)\n _jw_agents_md > \"$_agents_tmp\"\n _ov_workswarm inject-template \"$tpl\" \"$OV_ENDPOINT\" \"$OV_MCP_URL\" \"$_agents_tmp\" \"$OV_SHARED_DIR\" \"$OV_TEMPLATE_DIR\" \"$OV_RUNTIME_DIR\"\n rm -f \"$_agents_tmp\"\n fi\n mkdir -p \"${sandbox}/workspace\"\n _jw_agents_md > \"${sandbox}/workspace/AGENTS.md\"\n local jw_iface; jw_iface=$(ov_find_runtime_file \"*/jiuwenswarm*/agent_adapter/interface_code.py\")\n ov_patch_apply \"$jw_iface\" \"code-mode ExternalMemoryRail\" \\\n \"Code-mode ExternalMemoryRail patch\" _jw_patch_iface_apply || true\n local jw_prov; jw_prov=$(ov_find_runtime_file \"*/jiuwenswarm*/memory/external/openviking_memory_provider.py\")\n ov_patch_apply \"$jw_prov\" \"ov-fix-limit\" \\\n \"Provider top_k→limit patch\" _jw_patch_prov_apply || true\n ov_patch_apply \"$jw_prov\" \"ov-fix-session\" \\\n \"Provider jw- session ID patch\" _jw_patch_session_apply || true\n ov_patch_apply \"$jw_prov\" \"ov-fix-commit\" \\\n \"Provider commit-after-sync_turn patch\" _jw_patch_commit_apply || true\n}\n\nagent_workswarm_unbind() {\n local sandbox; sandbox=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n local tpl=\"${AGENT_META[template_path]}\"\n local cf=\"\"\n local ov_conf=\"\"\n if [[ -n \"$sandbox\" ]]; then\n cf=\"${sandbox}/.jiuwenswarm/config/config.yaml\"\n ov_conf=\"${sandbox}/.config/opencode/openviking-config.json\"\n fi\n local has_sandbox=0 has_template=0\n if [[ -n \"$cf\" && -f \"$cf\" ]]; then\n grep -q \"name: openviking\\|openviking:\\|viking_search\\|MEMORY_ENGINE:-both\\|MEMORY_EXTERNAL_PROVIDER:-openviking\" \"$cf\" 2>/dev/null && has_sandbox=1\n fi\n [[ -n \"$ov_conf\" && -f \"$ov_conf\" ]] && has_sandbox=1\n [[ -f \"$tpl\" ]] && grep -q \"OpenViking\\|openviking-config\\|Enhanced 4-section AGENTS\\|MEMORY_EXTERNAL_PROVIDER=openviking\\|ov-jiuwenswarm-init.sh\" \"$tpl\" 2>/dev/null && has_template=1\n if [[ \"$has_sandbox\" -eq 0 && \"$has_template\" -eq 0 ]]; then\n return 0\n fi\n require_confirmation \"UNBIND OpenViking\" \"workswarm\" \"Remove OpenViking native memory provider (+ legacy MCP if present) from sandbox and template\" \"$RED\" || return 1\n if dry_run_msg \"Would remove OpenViking from ${cf:-\u003cno-sandbox>} and $tpl\"; then return 0; fi\n # Template start.sh (persistent) — clean first, does not depend on sandbox\n if [[ \"$has_template\" -eq 1 ]]; then\n backup_file \"$tpl\"\n _ov_workswarm unbind-template \"$tpl\" \"$OV_SHARED_DIR\"\n rm -f \"$OV_SHARED_DIR/ov-jiuwenswarm-init.sh\"\n fi\n # Sandbox config — only if sandbox exists\n if [[ \"$has_sandbox\" -eq 1 ]]; then\n if [[ -n \"$cf\" && -f \"$cf\" ]]; then\n backup_file \"$cf\"\n _jw_mutate_config revert \"$cf\"\n fi\n if [[ -n \"$ov_conf\" && -f \"$ov_conf\" ]]; then\n rm -f \"$ov_conf\"\n fi\n else\n log_warn \"sandbox not found — template cleaned, skipping sandbox-layer cleanup\"\n fi\n # ISSUE-010: unbind must clean what integrate wrote — workspace/AGENTS.md is\n # written by integrate.sh and was left behind after unbind\n if [[ -n \"$sandbox\" && -f \"${sandbox}/workspace/AGENTS.md\" ]]; then\n local _agents_tmp2; _agents_tmp2=$(mktemp)\n _jw_agents_md > \"$_agents_tmp2\"\n if cmp -s \"${sandbox}/workspace/AGENTS.md\" \"$_agents_tmp2\"; then\n rm -f \"${sandbox}/workspace/AGENTS.md\"\n log_ok \"AGENTS.md removed from sandbox workspace\"\n else\n log_warn \"AGENTS.md in sandbox differs from OpenViking template (user-modified) — left untouched\"\n fi\n rm -f \"$_agents_tmp2\"\n fi\n local jw_iface; jw_iface=$(ov_find_runtime_file \"*/jiuwenswarm*/agent_adapter/interface_code.py\")\n ov_patch_revert \"$jw_iface\" \"code-mode ExternalMemoryRail\" \\\n \"Code-mode ExternalMemoryRail patch\" _jw_patch_iface_revert || true\n local jw_prov; jw_prov=$(ov_find_runtime_file \"*/jiuwenswarm*/memory/external/openviking_memory_provider.py\")\n ov_patch_revert \"$jw_prov\" \"ov-fix-limit\" \\\n \"Provider top_k→limit patch\" _jw_patch_prov_revert || true\n ov_patch_revert \"$jw_prov\" \"ov-fix-session\" \\\n \"Provider jw- session ID patch\" _jw_patch_session_revert || true\n ov_patch_revert \"$jw_prov\" \"ov-fix-commit\" \\\n \"Provider commit-after-sync_turn patch\" _jw_patch_commit_revert || true\n if [[ -n \"$sandbox\" ]]; then\n for proc_dir in \"${sandbox}/process_dir\" \"${sandbox}/.process_dir\"; do\n if [[ -f \"${proc_dir}/start.sh\" ]]; then\n cp \"$tpl\" \"${proc_dir}/start.sh\"\n break\n fi\n done\n fi\n}\n\nagent_workswarm_status() {\n local sandbox; sandbox=$(find_sandbox \"${AGENT_META[sandbox_pattern]%-*}\")\n [[ -z \"$sandbox\" ]] && { ov_status \"workswarm\" \"unknown\" \"sandbox not found\"; return; }\n local cf=\"${sandbox}/.jiuwenswarm/config/config.yaml\"\n [[ ! -f \"$cf\" ]] && { ov_status \"workswarm\" \"unknown\" \"config not found\"; return; }\n local tpl=\"${AGENT_META[template_path]}\"\n local tpl_has_ov=false\n has_ov_injection \"$tpl\" 2>/dev/null && tpl_has_ov=true\n grep -q \"ov-jiuwenswarm-init.sh\" \"$tpl\" 2>/dev/null && tpl_has_ov=true\n local live_native=false live_mcp=false detail=\"\"\n local cfg_engine=false cfg_provider=false\n grep -q \"engine:.*both\\|engine:.*external\" \"$cf\" 2>/dev/null && cfg_engine=true\n { grep -q \"provider:.*openviking\\|MEMORY_EXTERNAL_PROVIDER:-openviking\" \"$cf\" 2>/dev/null || \\\n { [[ -f \"$tpl\" ]] && grep -q \"MEMORY_EXTERNAL_PROVIDER=openviking\" \"$tpl\" 2>/dev/null; }; } && cfg_provider=true\n [[ \"$cfg_engine\" == \"true\" && \"$cfg_provider\" == \"true\" ]] && live_native=true\n if grep -q \"name: openviking\" \"$cf\" 2>/dev/null; then\n live_mcp=true\n fi\n if [[ \"$live_native\" == \"true\" && \"$live_mcp\" == \"true\" ]]; then\n detail=\"native memory provider + MCP\"\n elif [[ \"$live_native\" == \"true\" ]]; then\n detail=\"native memory provider only (MCP missing — re-integrate to add MCP)\"\n elif [[ \"$live_mcp\" == \"true\" ]]; then\n detail=\"MCP only (native provider not configured)\"\n fi\n local jw_iface jw_prov\n jw_iface=$(ov_find_runtime_file \"*/jiuwenswarm*/agent_adapter/interface_code.py\")\n jw_prov=$(ov_find_runtime_file \"*/jiuwenswarm*/memory/external/openviking_memory_provider.py\")\n if [[ -n \"$detail\" && ( -n \"$jw_iface\" || -n \"$jw_prov\" ) ]]; then\n local iface_mark prov_mark session_mark commit_mark\n iface_mark=$(ov_patch_mark \"$jw_iface\" \"code-mode ExternalMemoryRail\")\n prov_mark=$(ov_patch_mark \"$jw_prov\" \"ov-fix-limit\")\n session_mark=$(ov_patch_mark \"$jw_prov\" \"ov-fix-session\")\n commit_mark=$(ov_patch_mark \"$jw_prov\" \"ov-fix-commit\")\n detail+=\" | patches: iface${iface_mark} prov${prov_mark} sess${session_mark} commit${commit_mark}\"\n fi\n local live_has=false\n [[ \"$live_native\" == \"true\" || \"$live_mcp\" == \"true\" ]] && live_has=true\n # ISSUE-012: status detection must match unbind — unbind counts the sandbox\n # openviking-config.json artifact, status did not\n if [[ \"$live_has\" == \"false\" && -f \"${sandbox}/.config/opencode/openviking-config.json\" ]]; then\n live_has=true\n [[ -n \"$detail\" ]] && detail+=\" | config artifact\"\n fi\n agent::report_status \"${AGENT_META[name]}\" \"$tpl_has_ov\" \"$live_has\" \\\n \"$detail (template + live)\" \\\n \"template only, restart to activate\" \\\n \"$detail (live only, lost on restart)\" \\\n \"No OpenViking integration found\"\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":11320,"content_sha256":"1b575f5987a61be75345e2700fe2d5ffd1d6df670820eac3faac73c0b98cfad9"},{"filename":"scripts/integrate.sh","content":"#!/bin/bash\n# integrate.sh — OpenViking integration entry point\n# Usage: ./integrate.sh --agent \u003cname>|--all [--endpoint URL] [--api-key KEY] [--dry-run] [--yes]\nset -euo pipefail\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/entrypoint.sh\"\nexport OV_API_KEY=\"${OV_API_KEY:-}\"\n# Register agents early so usage()/error paths can list supported agents (ISSUE-006)\nregistry_init\nregistry_discover \"$SCRIPT_DIR/agents\"\nOV_MCP_URL=\"${OV_ENDPOINT}/mcp\"\nDRY_RUN=false\nAUTO_YES=false\nAGENT=\"\"\nALL_AGENTS=false\nusage() {\n echo \"Usage: $0 --agent \u003cname>|--all [--endpoint URL] [--api-key KEY] [--dry-run] [--yes]\"\n echo \"Supported agents: $(ov_supported_agents 2>/dev/null || echo \"(none)\")\"\n}\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --agent)\n if [[ $# -lt 2 ]]; then log_error \"--agent requires a value\"; usage; exit 1; fi\n AGENT=\"$2\"; shift 2 ;;\n --api-key)\n if [[ $# -lt 2 ]]; then log_error \"--api-key requires a value\"; usage; exit 1; fi\n OV_API_KEY=\"$2\"; shift 2 ;;\n --all) ALL_AGENTS=true; shift ;;\n --endpoint)\n if [[ $# -lt 2 ]]; then log_error \"--endpoint requires a value\"; usage; exit 1; fi\n OV_ENDPOINT=\"$2\"; OV_MCP_URL=\"${OV_ENDPOINT}/mcp\"; shift 2 ;;\n --dry-run) DRY_RUN=true; shift ;;\n --yes|-y) AUTO_YES=true; shift ;;\n --help|-h) usage; exit 0 ;;\n *) log_error \"Unknown option: $1\"; usage; exit 1 ;;\n esac\ndone\nif [[ \"$ALL_AGENTS\" == \"true\" && -n \"$AGENT\" ]]; then\n log_error \"--agent and --all are mutually exclusive — specify exactly one\"\n exit 1\nfi\nagents=()\nif [[ \"$ALL_AGENTS\" == \"true\" ]]; then\n while IFS= read -r a; do agents+=(\"$a\"); done \u003c \u003c(registry_list)\nelif [[ -n \"$AGENT\" ]]; then\n # ISSUE-006: reject unknown agent names with the supported list up front\n ov_require_supported_agent \"$AGENT\" || exit 1\n agents=(\"$AGENT\")\nelse\n log_error \"Specify --agent \u003cname> or --all\"\n usage; exit 1\nfi\ncheck_ov_health || exit 1\nrc=0\nfor a in \"${agents[@]}\"; do\n display=\"${a//_/-}\"\n log_info \"Processing agent: $display\"\n # ISSUE-007: fail loudly instead of silently \"completing\" when the sandbox is missing\n agent::clear_meta\n reg_fn=\"agent_${a}_register\"\n declare -f \"$reg_fn\" &>/dev/null && \"$reg_fn\"\n if [[ \"$DRY_RUN\" != \"true\" ]] && [[ -n \"${AGENT_META[sandbox_pattern]:-}\" ]] && ! ov_require_agent_sandbox; then\n log_error \"Agent $display: integration aborted (sandbox missing)\"\n rc=1\n continue\n fi\n # ISSUE-004: capture the dispatch exit code explicitly — errexit is disabled\n # inside if-conditions, so `if dispatch; then` masked real failures\n ( registry_dispatch \"$a\" integrate ); dsp_rc=$?\n if [[ $dsp_rc -eq 0 ]]; then\n log_ok \"Agent $display: integration complete\"\n else\n log_error \"Agent $display: integration failed\"\n rc=1\n fi\ndone\n# ISSUE-003: fail loudly when --api-key was requested but never consumed\nif [[ -n \"$OV_API_KEY\" ]] && ! ov_api_key_was_consumed; then\n log_warn \"--api-key provided but never consumed by any agent configuration — authentication may fail at runtime\"\nfi\nif [[ $rc -ne 0 ]]; then\n log_warn \"Some integrations failed or were skipped. Review output above.\"\nfi\nexit $rc","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3149,"content_sha256":"c342d0dedd0890fa1802b37ccad1a1dfd8a61d657990d594b17ac480b6588690"},{"filename":"scripts/lib/base.sh","content":"#!/bin/bash\n# lib/base.sh — Agent base class (shared operations for all agent subclasses)\n: \"${OV_MARKER:=added by huawei-cloud-openviking-agent-integration skill}\"\n: \"${OV_MARKER_LEGACY:=added by openviking-agent-integration skill}\"\n: \"${OV_SKILL_VERSION:=1.2.2}\"\n: \"${OV_HOME:=${HOME:-/root}}\"\n: \"${OV_RUNTIME_DIR:=${OV_HOME}/runtime}\"\n: \"${OV_TEMPLATE_DIR:=${OV_HOME}/template}\"\n: \"${OV_SHARED_DIR:=${OV_RUNTIME_DIR}/shared}\"\n: \"${OV_SANDBOX_DIR:=${OV_HOME}/job-envs/sandboxes}\"\n: \"${OV_PY_DIR:=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../py\" && pwd)}\"\n_ov_health() { \"$OV_PY\" \"$OV_PY_DIR/ov_health.py\" \"$@\"; }\n\nov_resolve_cmd() { command -v \"$1\" 2>/dev/null; }\n\nov_detect_python() {\n local py\n for py in python3.12 python3.11 python3.10 python3; do\n local p; p=$(command -v \"$py\" 2>/dev/null) && \"$p\" -c 'import json' 2>/dev/null && { printf '%s' \"$p\"; return 0; }\n done\n return 1\n}\n: \"${OV_PY:=$(ov_detect_python)}\"\n: \"${OV_NODE:=$(ov_resolve_cmd node)}\"\n: \"${OV_NPM:=$(ov_resolve_cmd npm)}\"\n: \"${OV_NPX:=$(ov_resolve_cmd npx)}\"\n: \"${OV_CURL_BIN:=$(ov_resolve_cmd curl)}\"\n: \"${OV_GIT:=$(ov_resolve_cmd git)}\"\n: \"${OV_CURL_CONNECT_TIMEOUT:=5}\"\n: \"${OV_CURL_MAX_TIME:=10}\"\nov_curl() { \"$OV_CURL_BIN\" --connect-timeout \"$OV_CURL_CONNECT_TIMEOUT\" --max-time \"$OV_CURL_MAX_TIME\" \"$@\"; }\n\n# Probe an already-installed package version from a package.json path.\n# Prints the version (or nothing) — returns 1 if unreadable/absent.\nov_probe_package_version() {\n local pkg_json=\"$1\"\n [[ -n \"$pkg_json\" && -f \"$pkg_json\" ]] || return 1\n \"$OV_PY\" -c \"import json,sys; print(json.load(open(sys.argv[1])).get('version',''))\" \"$pkg_json\" 2>/dev/null || return 1\n}\n\n# Resolve effective plugin version: env override -> host-installed probe -> latest.\nov_effective_plugin_ver() {\n local pkg_json=\"$1\" env_ver=\"${2:-}\"\n [[ -n \"$env_ver\" && \"$env_ver\" != \"latest\" ]] && { echo \"$env_ver\"; return 0; }\n local probe; probe=$(ov_probe_package_version \"$pkg_json\") || true\n [[ -n \"$probe\" ]] && { echo \"$probe\"; return 0; }\n echo \"latest\"\n}\n\n: \"${OV_ENDPOINT:=http://127.0.0.1:1933}\"\n: \"${OV_ACCOUNT_DEFAULT:=$(whoami 2>/dev/null || echo default)}\"\n: \"${OV_USER_DEFAULT:=$(whoami 2>/dev/null || echo default)}\"\n: \"${OV_MCP_URL:=${OV_ENDPOINT}/mcp}\"\n# Version policy: never pin plugin/SDK versions. Default to npm \"latest\" so the\n# installed plugin matches the current agent engine; probe the host install first\n# when present (see ov_probe_package_version). Explicit env overrides still win.\n: \"${OV_OPENCODE_PLUGIN_VER:=latest}\"\n: \"${OV_PLUGIN_SDK_VER:=latest}\"\n: \"${OV_DSH_PLUGIN_VER:=latest}\" # unused for installs; dsh deps come from host sync\n: \"${OV_NPM_REGISTRY_DEFAULT:=https://mirrors.huaweicloud.com/repository/npm/}\"\n: \"${OV_PLUGIN_CACHE_DIR:=${OV_SHARED_DIR}/openviking-plugins}\"\n: \"${OV_PLUGIN_CACHE_TTL:=86400}\"\n\nov_plugin_cache_path() {\n [[ -z \"$1\" ]] && return 1\n [[ \"$1\" == *..* || \"$1\" == /* ]] && { log_warn \"ov_plugin_cache_path: rejecting suspicious name: $1\"; return 1; }\n printf '%s/%s' \"$OV_PLUGIN_CACHE_DIR\" \"$1\"\n}\n\n# ISSUE-019: cache is valid only when it exists, is within TTL AND is content-sane\n# (package.json non-empty + more than a lone manifest — a truncated/corrupted cache\n# is rejected so callers re-download instead of silently reusing it).\nov_plugin_cache_valid() {\n local dir; dir=$(ov_plugin_cache_path \"$1\")\n [[ -d \"$dir\" && -f \"$dir/package.json\" && -s \"$dir/package.json\" ]] || return 1\n local file_count; file_count=$(find \"$dir\" -mindepth 1 -type f 2>/dev/null | wc -l)\n (( file_count >= 2 )) || return 1\n local now mtime\n now=$(date +%s)\n mtime=$(stat -c %Y \"$dir\" 2>/dev/null || echo 0)\n [[ $((now - mtime)) -le $OV_PLUGIN_CACHE_TTL ]]\n}\n\nov_plugin_cache_put() {\n local pkg=\"$1\" src=\"$2\" dir tmp_dir\n dir=$(ov_plugin_cache_path \"$pkg\") || return 1\n mkdir -p \"$(dirname \"$dir\")\"\n tmp_dir=\"${dir}.tmp.$\"\n ov_safe_rm \"$tmp_dir\" 2>/dev/null || true\n cp -a \"$src\" \"$tmp_dir\" && mv \"$tmp_dir\" \"$dir\" || { ov_safe_rm \"$tmp_dir\" 2>/dev/null || true; return 1; }\n touch \"$dir\"\n}\n\nov_plugin_cache_get() {\n local pkg=\"$1\" dst=\"$2\" dir\n dir=$(ov_plugin_cache_path \"$pkg\")\n [[ -d \"$dir\" ]] || return 1\n mkdir -p \"$(dirname \"$dst\")\"\n ov_safe_rm \"$dst\" 2>/dev/null || true\n cp -a \"$dir\" \"$dst\"\n}\n: \"${OV_VERIFY_QUERY:=华为云 ECS 创建 项目配置 区域}\"\n: \"${OV_BACKUP_KEEP:=5}\"\ndeclare -gA AGENT_META\n\nagent::set_meta() { AGENT_META[\"$1\"]=\"$2\"; }\nagent::get_meta() { echo \"${AGENT_META[$1]:-}\"; }\nagent::clear_meta() { AGENT_META=(); }\n\nfind_sandbox() {\n local matches\n mapfile -t matches \u003c \u003c(find \"$OV_SANDBOX_DIR/\" -maxdepth 1 -type d -name \"${1}-*\" -print 2>/dev/null)\n [[ ${#matches[@]} -gt 0 ]] || return 1\n if (( ${#matches[@]} > 1 )); then\n log_warn \"find_sandbox: ${#matches[@]} sandboxes match '${1}-*'; using first (${matches[0]}) — specify a unique sandbox if ambiguous\" >&2\n fi\n printf '%s' \"${matches[0]}\"\n}\n\nbackup_file() {\n [[ -f \"$1\" ]] || { log_warn \"backup_file: $1 not found\"; return 1; }\n cp \"$1\" \"${1}.bak.$(date +%s)\"\n local _bak_prefix=\"${1}.bak.\"\n ls -1t \"${_bak_prefix}\"* 2>/dev/null | tail -n +$((OV_BACKUP_KEEP + 1)) | while IFS= read -r _old; do\n rm -f \"$_old\"\n done\n}\n\nhas_ov_injection() {\n grep -v \"MCP SDK install\" \"$1\" 2>/dev/null | grep -q \"$OV_MARKER\\|$OV_MARKER_LEGACY\"\n}\n\nov_template_path() {\n if [[ -n \"${AGENT_META[template_path]:-}\" ]]; then\n echo \"${AGENT_META[template_path]}\"\n else\n echo \"${OV_TEMPLATE_DIR}/$1/start.sh\"\n fi\n}\n\nov_status() { echo \"${1}|${2}|${3}\"; }\n\ncheck_ov_health() {\n local endpoint=\"${OV_ENDPOINT:-http://127.0.0.1:1933}\"\n local resp\n resp=$(ov_curl -sf \"${endpoint}/health\" 2>/dev/null) || {\n log_error \"OpenViking server not reachable at $endpoint\"\n return 1\n }\n local status\n status=$(echo \"$resp\" | _ov_health parse-field - status 2>/dev/null || echo \"unknown\")\n [[ \"$status\" == \"ok\" ]] && return 0\n log_error \"OpenViking server unhealthy: $resp\"\n return 1\n}\n\nagent::default_integrate() { log_error \"Agent '${AGENT_META[name]}' does not implement integrate()\"; return 1; }\nagent::default_unbind() { log_error \"Agent '${AGENT_META[name]}' does not implement unbind()\"; return 1; }\nagent::default_status() { ov_status \"${AGENT_META[name]:-unknown}\" \"unknown\" \"status not implemented\"; }\n\n# --- Shared 4-way status reporting (eliminates duplication across 8 agents) ---\nagent::report_status() {\n local name=\"$1\" tpl_has=\"$2\" live_has=\"$3\"\n local d_integrated=\"$4\" d_template=\"$5\" d_live=\"$6\" d_none=\"$7\"\n if [[ \"$tpl_has\" == \"true\" && \"$live_has\" == \"true\" ]]; then\n ov_status \"$name\" \"integrated\" \"$d_integrated\"\n elif [[ \"$tpl_has\" == \"true\" ]]; then\n ov_status \"$name\" \"integrated\" \"$d_template\"\n elif [[ \"$live_has\" == \"true\" ]]; then\n ov_status \"$name\" \"partial\" \"$d_live\"\n else\n ov_status \"$name\" \"not_integrated\" \"$d_none\"\n fi\n}\n\ncreate_ov_config() {\n local conf_subdir=\"${2:-.config/opencode}\"\n local conf_dir=\"$1/$conf_subdir\"\n mkdir -p \"$conf_dir\"\n if [[ ! -f \"$conf_dir/openviking-config.json\" ]]; then\n cat > \"$conf_dir/openviking-config.json\" \u003c\u003c'OVCONF'\n{\"enabled\":true,\"timeoutMs\":30000,\"repoContext\":{\"enabled\":true,\"cacheTtlMs\":60000},\"autoRecall\":{\"enabled\":true,\"limit\":10,\"scoreThreshold\":0.35,\"maxContentChars\":500,\"preferAbstract\":true,\"tokenBudget\":2000,\"minQueryLength\":3},\"recallLimit\":15,\"recallMaxContentChars\":20000,\"commitTokenThreshold\":20000,\"commitKeepRecentCount\":10,\"profileTokenBudget\":10000,\"resumeContextBudget\":32000}\nOVCONF\n chmod 600 \"$conf_dir/openviking-config.json\" 2>/dev/null || true\n fi\n}\n\nov_find_runtime_file() {\n find \"$OV_RUNTIME_DIR\" -path \"$1\" -not -path \"*__pycache__*\" 2>/dev/null | head -1\n}\n\nov_clear_pycache() {\n local file=\"$1\" base dir\n [[ -z \"$file\" || ! -f \"$file\" ]] && return 0\n base=$(basename \"$file\" .py)\n dir=$(dirname \"$file\")\n find \"$dir\" -path \"*__pycache__*${base}*\" -delete 2>/dev/null\n}\n\nov_patch_present() {\n local file=\"$1\" marker=\"$2\"\n [[ -n \"$file\" && -f \"$file\" ]] && grep -q \"$marker\" \"$file\" 2>/dev/null\n}\n\nov_patch_apply() {\n local file=\"$1\" marker=\"$2\" desc=\"$3\" apply_fn=\"$4\"\n if [[ -z \"$file\" ]]; then\n log_warn \"$desc: target file not found — patch skipped\"\n return 1\n fi\n ov_patch_present \"$file\" \"$marker\" && return 0\n backup_file \"$file\"\n if \"$apply_fn\" \"$file\"; then\n ov_clear_pycache \"$file\"\n ov_patch_present \"$file\" \"$marker\" || log_warn \"$desc: marker not found after transform (version mismatch?)\"\n return 0\n else\n log_error \"$desc: transformation failed\"\n return 1\n fi\n}\n\nov_patch_revert() {\n local file=\"$1\" marker=\"$2\" desc=\"$3\" revert_fn=\"$4\"\n if [[ -z \"$file\" ]] || ! ov_patch_present \"$file\" \"$marker\"; then return 0; fi\n backup_file \"$file\"\n if \"$revert_fn\" \"$file\"; then\n ov_clear_pycache \"$file\"\n ov_patch_present \"$file\" \"$marker\" && { log_warn \"$desc: revert ran but marker still present\"; return 1; }\n return 0\n else\n log_error \"$desc: revert failed\"\n return 1\n fi\n}\n\nov_patch_mark() {\n if ov_patch_present \"$1\" \"$2\"; then printf '✓'; else printf '✗'; fi\n}\n\n# --- Sync shared/ .mjs files from upstream (post-provision fix for commit aa3ee2f) ---\n# Downloads shared/ files that an upstream refactor moved out of the extension/plugin\n# directory. The pack-time step that copies them back is not run by ov_plugin_provision,\n# so we fetch them here to prevent ERR_MODULE_NOT_FOUND at runtime.\n# Args: $1 = persist_src (persistent source directory to sync shared/ into)\n# Globals used: GH_RAW_MIRRORS, OV_CURL_BIN, OV_PLUGIN_REPO_PATH\nov_sync_shared_files() {\n local persist_src=\"$1\"\n [[ -z \"$persist_src\" ]] && return 1\n # Already synced — nothing to do\n if [[ -d \"$persist_src/shared\" && -f \"$persist_src/shared/ov-http.mjs\" ]]; then\n return 0\n fi\n local _sha; _sha=$(ov_plugin_upstream_sha 2>/dev/null || echo \"\")\n [[ -z \"$_sha\" ]] && return 0\n local _shared_dir=\"$persist_src/shared\"\n mkdir -p \"$_shared_dir\"\n local _f _url _src_paths=(\n \"agent-plugins/servers/shared\"\n \"examples/claude-code-memory-plugin/scripts/shared\"\n )\n local _needed=\"ov-http.mjs capture-utils.mjs plugin-config.mjs config-schema.mjs credentials.mjs debug-log.mjs input-filters.mjs mcp-proxy-config.mjs mcp-proxy-core.mjs pending-queue.mjs profile-inject.mjs recall-compress-core.mjs recall-core.mjs retryable.mjs session-model.mjs uri-guard.mjs workspace-identity.mjs workspace-peer.mjs workspace-config.mjs workspace-registry.mjs agent-hook-runtime.mjs async-writer.mjs batch-send.mjs doctor-core.mjs setup-wizard.mjs\"\n for _f in $_needed; do\n [[ -f \"$_shared_dir/$_f\" ]] && continue\n for _src in \"${_src_paths[@]}\"; do\n for _base in \"${GH_RAW_MIRRORS[@]}\"; do\n _url=\"${_base}/${OV_PLUGIN_REPO_PATH}/${_sha}/${_src}/${_f}\"\n if \"$OV_CURL_BIN\" -fsS --connect-timeout 10 --max-time 25 \"$_url\" -o \"$_shared_dir/$_f\" 2>/dev/null; then\n break\n fi\n done\n [[ -f \"$_shared_dir/$_f\" ]] && break\n done\n done\n log_ok \"shared/ files synced (${_sha:0:7})\"\n}\n\n# --- Safe removal (refuses dangerous paths, allows only known dirs) ---\n# ISSUE-017: resolves symlinks first, then evaluates the allowlist on the real\n# path — a symlink inside an allowlisted dir can no longer escape it. Sandbox\n# paths are allowed only at least two segments below the sandbox root, so a\n# whole sandbox can never be removed through this helper.\nov_safe_rm() {\n local target=\"$1\"\n [[ -z \"$target\" ]] && return 1\n case \"$target\" in\n /|/root|\"$HOME\"|/tmp|/usr|/bin|/sbin|/lib|/opt|/etc|/var|/boot|/proc|/sys|/dev|/home) return 1 ;;\n esac\n [[ \"$target\" != /* ]] && return 1\n local real; real=$(realpath -m \"$target\" 2>/dev/null || echo \"$target\")\n case \"$real\" in\n /|/root|\"$HOME\"|/tmp|/usr|/bin|/sbin|/lib|/opt|/etc|/var|/boot|/proc|/sys|/dev|/home) return 1 ;;\n esac\n local ok=false\n # Runtime / shared / plugin-cache / skill-home trees are fully managed by the skill\n case \"$real\" in\n \"${OV_RUNTIME_DIR:-/nonexistent}\"/*|\\\n \"${OV_SHARED_DIR:-/nonexistent}\"/*|\\\n \"${OV_PLUGIN_CACHE_DIR:-/nonexistent}\"/*|\\\n \"${OV_HOME:-/nonexistent}\"/*|\\\n /tmp/openviking/*|\\\n /tmp/ov-*) ok=true ;;\n esac\n # Sandbox sub-paths: allow, but never the sandbox itself nor an arbitrary\n # one-segment child. OV-owned shadow dirs (.openviking/.codeartsdoer) are\n # created only by this skill, so they are safe even at depth one.\n if [[ \"$real\" == \"${OV_SANDBOX_DIR:-/nonexistent}/\"* ]]; then\n local _rest=\"${real#\"${OV_SANDBOX_DIR}/\"}\"\n case \"$_rest\" in\n .openviking|.codeartsdoer) ok=true ;;\n */*) ok=true ;;\n esac\n fi\n if [[ \"$ok\" != \"true\" ]]; then\n log_warn \"ov_safe_rm: path outside allowlist: $real (was: $target)\"\n return 1\n fi\n rm -rf \"$real\"\n}\n\n# --- Require sandbox exists ---\nov_require_sandbox() {\n local pattern=\"$1\"\n local sbx; sbx=$(find_sandbox \"$pattern\")\n [[ -n \"$sbx\" ]] || { log_error \"No sandbox found for: $pattern\"; return 1; }\n printf '%s' \"$sbx\"\n}\n\n# --- ISSUE-007: require the current agent's sandbox before integrating ---\n# Reads AGENT_META (populated by agent::register) so entry points can gate on it.\nov_require_agent_sandbox() {\n local pattern=\"${AGENT_META[sandbox_pattern]:-}\"\n local name=\"${AGENT_META[name]:-unknown}\"\n [[ -n \"$pattern\" ]] || { log_error \"Agent '$name' has no sandbox_pattern meta — cannot validate sandbox\"; return 1; }\n ov_require_sandbox \"${pattern%-*}\"\n}\n\n# --- ISSUE-006: validate an agent name against the discovered registry ---\nov_supported_agents() { registry_list | tr '\\n' ',' | sed 's/,$//'; }\nov_require_supported_agent() {\n local name=\"$1\"\n if ! registry_validate \"$name\"; then\n log_error \"Unknown agent: $name — supported: $(ov_supported_agents)\"\n return 1\n fi\n}\n\n# --- ISSUE-003: track whether an explicitly provided --api-key was consumed ---\n# bash-side flag plus a marker file that the python injectors touch when they\n# actually embed the key into an agent config (the marker survives the bash ->\n# python -> bash round-trip).\nOV_API_KEY_CONSUMED=false\n: \"${OV_API_KEY_CONSUMED_FILE:=${OV_RUNTIME_DIR}/.ov_api_key_consumed}\"\nexport OV_API_KEY_CONSUMED_FILE\n# ISSUE-003: sh-side marker writer. Kept as reserved API: the Python injectors\n# (_ov_mark_key_consumed) currently write the marker file, but sh-only injection\n# paths can call this function directly.\nov_mark_api_key_consumed() {\n OV_API_KEY_CONSUMED=true\n mkdir -p \"$(dirname \"$OV_API_KEY_CONSUMED_FILE\")\" 2>/dev/null || true\n : > \"$OV_API_KEY_CONSUMED_FILE\" 2>/dev/null || true\n}\nov_api_key_was_consumed() {\n [[ \"$OV_API_KEY_CONSUMED\" == \"true\" || -f \"${OV_API_KEY_CONSUMED_FILE:-}\" ]]\n}\n\n# --- P2-8: Sync template to sandbox process_dir ---\n# ISSUE-013: join with a separator so a sandbox path without a trailing \"/\"\n# never produces a bogus \"${sandbox}process_dir\" that silently never exists.\nov_sync_template_to_sandbox() {\n local tpl=\"$1\" sandbox=\"$2\" process_subdir=\"${3:-process_dir}\"\n [[ -z \"$tpl\" || -z \"$sandbox\" || ! -f \"$tpl\" ]] && return 1\n local target_dir=\"${sandbox%/}/${process_subdir#/}\"\n if [[ -d \"$target_dir\" ]]; then\n cp \"$tpl\" \"${target_dir}/start.sh\" && chmod +x \"${target_dir}/start.sh\" \\\n && log_ok \"Synced start.sh to sandbox (${process_subdir})\" \\\n || { log_error \"Failed to sync start.sh to sandbox\"; return 1; }\n return 0\n fi\n log_warn \"No ${process_subdir} in sandbox '${sandbox}' — start.sh NOT synced (do NOT report it as synced)\"\n return 0\n}\n\n# --- Deploy plugin from cache (returns 1 on cache miss) ---\n# --- Tiered plugin deploy: existing → cache → npm → GitHub provision ---\n# Unified deployment extracted from opencode.sh and deepseek_harness.sh.\n# Sets global _ov_tiered_result: \"existing\" | \"cache\" | \"npm\" | \"provision\" | \"failed\"\n# Args: $1=pkg_name $2=dest_dir $3=npm_registry $4=provision_name $5=plugin_subpath $6=pkg_ver (opt, default \"latest\")\n# $7=force_provision (opt, default false) — when true, an existing install still\n# runs ov_plugin_provision to compare upstream commit (zero download if same,\n# keeps existing on network failure) instead of short-circuiting as \"existing\".\n# If plugin_subpath is empty, deploys directly to dest_dir (no node_modules/ prefix).\nov_deploy_plugin_tiered() {\n local pkg_name=\"$1\" dest_dir=\"$2\" npm_registry=\"$3\" provision_name=\"$4\" plugin_subpath=\"$5\"\n local pkg_ver=\"${6:-latest}\"\n local force_provision=\"${7:-false}\"\n local plugin_dst\n if [[ -z \"$plugin_subpath\" ]]; then\n plugin_dst=\"$dest_dir\"\n else\n plugin_dst=\"${dest_dir}/node_modules/${plugin_subpath}\"\n fi\n _ov_tiered_result=\"failed\"\n\n # 1. Already installed\n if [[ -d \"$plugin_dst\" && -f \"$plugin_dst/package.json\" ]]; then\n if [[ \"$force_provision\" == \"true\" && -n \"$provision_name\" ]]; then\n if ov_plugin_provision \"$provision_name\" \"$plugin_dst\"; then\n _ov_tiered_result=\"provision\"\n return 0\n fi\n log_warn \"Upstream check failed for $provision_name — keeping existing install at $plugin_dst\"\n fi\n _ov_tiered_result=\"existing\"\n return 0\n fi\n\n # 2. Shared cache\n local cache_dir; cache_dir=$(ov_plugin_cache_path \"$pkg_name\" 2>/dev/null) || true\n if [[ -n \"$cache_dir\" && -d \"$cache_dir\" && -f \"$cache_dir/package.json\" ]]; then\n mkdir -p \"$(dirname \"$plugin_dst\")\"\n ov_safe_rm \"$plugin_dst\" 2>/dev/null || true\n cp -a \"$cache_dir\" \"$plugin_dst\"\n _ov_tiered_result=\"cache\"\n return 0\n fi\n\n # 3. npm install\n if [[ -n \"$npm_registry\" && -n \"$pkg_name\" ]] && { command -v npm &>/dev/null || command -v \"${OV_NPM:-npm}\" &>/dev/null; }; then\n local npm_bin=\"${OV_NPM:-npm}\"\n mkdir -p \"$dest_dir\"\n printf '{\"dependencies\":{\"%s\":\"%s\"}}\\n' \"$pkg_name\" \"$pkg_ver\" > \"$dest_dir/package.json\"\n if (cd \"$dest_dir\" && \"$npm_bin\" install --registry=\"$npm_registry\" --no-audit --no-fund 2>&1 | tail -5) && \\\n [[ -d \"$plugin_dst\" ]]; then\n ov_plugin_cache_put \"$pkg_name\" \"$plugin_dst\" || log_warn \"ISSUE-019: cache write failed for $pkg_name\"\n _ov_tiered_result=\"npm\"\n return 0\n fi\n fi\n\n # 4. GitHub provision fallback\n log_warn \"npm/cache failed — trying GitHub provision for $provision_name\"\n local _prov_dir; _prov_dir=$(mktemp -d /tmp/ov-prov.XXXXXX)\n if ov_plugin_provision \"$provision_name\" \"$_prov_dir\"; then\n mkdir -p \"$(dirname \"$plugin_dst\")\"\n ov_safe_rm \"$plugin_dst\" 2>/dev/null || true\n cp -a \"$_prov_dir\" \"$plugin_dst\"\n ov_plugin_cache_put \"$pkg_name\" \"$plugin_dst\" || log_warn \"ISSUE-019: cache write failed for $pkg_name\"\n ov_safe_rm \"$_prov_dir\" 2>/dev/null || true\n _ov_tiered_result=\"provision\"\n return 0\n fi\n ov_safe_rm \"$_prov_dir\" 2>/dev/null || true\n _ov_tiered_result=\"failed\"\n return 1\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":18661,"content_sha256":"9b0404e01a045204e18d9b094994b4950cde34d2d8163c5faa4f3dfc19e56f40"},{"filename":"scripts/lib/entrypoint.sh","content":"#!/bin/bash\n# lib/entrypoint.sh — Common entry point initialization (R18: ov_entrypoint_init)\n# Source this file at the top of entry point scripts after set -euo pipefail:\n# #!/bin/bash\n# set -euo pipefail\n# source \"$(cd \"$(dirname \"$0\")\" && pwd)/lib/entrypoint.sh\"\nset -euo pipefail\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/ui.sh\"\nsource \"$SCRIPT_DIR/lib/json.sh\"\nsource \"$SCRIPT_DIR/lib/plugins.sh\"\nsource \"$SCRIPT_DIR/lib/base.sh\"\nsource \"$SCRIPT_DIR/lib/registry.sh\"\n# (tmp cleanup trap removed: ov_tmp_register had no callers — dead chain)\nOV_ENDPOINT=\"${OV_ENDPOINT:-http://127.0.0.1:1933}\"\n# P1-8: Validate endpoint — warn on plaintext http:// for non-loopback hosts\nov_validate_endpoint() {\n local ep=\"$1\"\n [[ \"$ep\" == http://127.* || \"$ep\" == http://localhost* ]] && return 0\n if [[ \"$ep\" == http://* ]]; then\n log_warn \"Endpoint uses plaintext HTTP for non-loopback host: $ep — traffic is unencrypted\"\n fi\n}\nov_validate_endpoint \"$OV_ENDPOINT\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1020,"content_sha256":"c5da2ebae65cbb1a7cde43cfde0a4febd4ae83c42f4c28aa7b107e88911391aa"},{"filename":"scripts/lib/json.sh","content":"#!/bin/bash\n# lib/json.sh — JSON utilities for agent config manipulation (delegates to py/ov_json.py)\n: \"${OV_PY_DIR:=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../py\" && pwd)}\"\n_ov_json() { \"$OV_PY\" \"$OV_PY_DIR/ov_json.py\" \"$@\"; }\n\n# Check if a JSON config file has openviking MCP enabled\ncheck_json_mcp() {\n _ov_json check-mcp \"$1\" 2>/dev/null\n}\n# Extract the openviking MCP URL from a JSON config file\nget_json_mcp_url() {\n _ov_json get-mcp-url \"$1\" 2>/dev/null\n}\n# json_read \u003cfile> \u003cdotted.key> → print value (empty string if missing)\n# json_read \u003cfile> \u003cdotted.key> \u003cdefault> → print default if missing\njson_read() {\n local file=\"$1\" key=\"$2\" default=\"${3:-}\"\n _ov_json read \"$file\" \"$key\" \"$default\" 2>/dev/null\n}\n# json_write \u003cfile> \u003cdotted.key> \u003cvalue> → set value (creates intermediate dicts)\n# value must be a JSON literal (e.g. '\"string\"', 'true', '42', 'null')\njson_write() {\n local file=\"$1\" key=\"$2\" value=\"$3\"\n _ov_json write \"$file\" \"$key\" \"$value\" 2>/dev/null\n}\n# json_has_key \u003cfile> \u003cdotted.key> → exit 0 if key exists, 1 if not\njson_has_key() {\n _ov_json has-key \"$1\" \"$2\" 2>/dev/null\n}\n# json_remove_key \u003cfile> \u003cdotted.key> → remove key (no-op if missing)\njson_remove_key() {\n local file=\"$1\" key=\"$2\"\n _ov_json remove-key \"$file\" \"$key\" 2>/dev/null\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1297,"content_sha256":"09824a669f6844f340648ef9dbe3330aec8aa25d62dfce1e5c20ed88b384ff36"},{"filename":"scripts/lib/ov_jw_mutate.py","content":"#!/usr/bin/env python3\n\"\"\"WorkSwarm config.yaml mutation — single source of truth.\nCalled by _jw_mutate_config (during integrate/unbind) and by the sandbox\ninit script (ov-jiuwenswarm-init.sh) on every start.\n\nUsage: python3 ov_jw_mutate.py \u003capply|revert> \u003cconfig.yaml> [mcp_url]\n\"\"\"\nimport sys, re, os\n\naction, path = sys.argv[1], sys.argv[2]\nmcp_url = sys.argv[3] if len(sys.argv) > 3 else ''\n\ndef _ov_mark_key_consumed():\n \"\"\"ISSUE-003: notify the parent bash entry point that --api-key was used.\"\"\"\n f = os.environ.get(\"OV_API_KEY_CONSUMED_FILE\")\n if not f:\n return\n try:\n os.makedirs(os.path.dirname(f), exist_ok=True)\n open(f, \"a\").close()\n except OSError:\n pass\n\ntry:\n with open(path, encoding='utf-8') as f:\n content = f.read()\nexcept Exception as e:\n print(f\"ERROR: cannot read {path}: {e}\", file=sys.stderr)\n sys.exit(1)\nchanged = False\n\ndef _dash_indent_of(content):\n \"\"\"Detect existing mcp.servers entry dash indent; default 2-space compact style.\"\"\"\n m = re.search(r'servers:[^\\n]*\\n([ \\t]*)-[ \\t]', content)\n return m.group(1) if m else ' '\n\ndef _insert_mcp_entry(content, mcp_url):\n \"\"\"Insert openviking MCP entry with indentation matching existing entries.\n Pure-regex, no yaml library dependency (works inside bwrap sandbox).\"\"\"\n if \"name: openviking\" in content:\n return content, False\n dash = _dash_indent_of(content)\n inner = dash + ' '\n entry = (dash + '- name: openviking\\n'\n + inner + 'transport: streamable-http\\n'\n + inner + 'url: %s\\n' % mcp_url\n + inner + 'enabled: true\\n')\n # Case A: inline empty list servers: []\n new = re.sub(r'(mcp:\\s*\\n\\s*servers:\\s*)\\[\\]', lambda m: m.group(1) + '\\n' + entry, content)\n if new != content:\n return new, True\n # Case B: empty multi-line list servers:\\n (next line not a dash)\n new = re.sub(r'(mcp:\\s*\\n\\s*servers:\\s*\\n)(?!\\s*-)', lambda m: m.group(1) + entry, content)\n if new != content:\n return new, True\n # Case C: existing entries — append after the whole block, matching detected indent\n block_re = re.compile(r'(mcp:\\s*\\n\\s*servers:\\n(?:[ \\t]*-[^\\n]*\\n(?:[ \\t]+[^\\n]*\\n)*)*)')\n mm = block_re.search(content)\n if mm:\n return content[:mm.end()] + entry + content[mm.end():], True\n return content, False\n\ndef _check_mcp_indent(content):\n \"\"\"Verify all entries under mcp.servers share one dash indent (no yaml lib needed).\"\"\"\n m = re.search(r'mcp:\\s*\\n\\s*servers:\\s*\\n((?:[ \\t]*-[^\\n]*\\n(?:[ \\t]+[^\\n]*\\n)*)+)', content)\n if not m:\n return True\n block = m.group(1)\n dashes = set(re.findall(r'^([ \\t]*)- ', block, re.M))\n return len(dashes) \u003c= 1\n\ndef _validate_yaml(path, content, label):\n \"\"\"Write-time guard: abort WITHOUT writing if the result is not parseable YAML.\"\"\"\n ok = False\n try:\n import yaml as _y\n _y.safe_load(content)\n ok = True\n except Exception:\n pass\n if not ok:\n try:\n from ruamel.yaml import YAML as _RY\n _ry = _RY()\n _ry.load(content)\n ok = True\n except Exception:\n pass\n if not ok:\n if _check_mcp_indent(content):\n ok = True # structural check passed, yaml libs unavailable\n if not ok:\n print(\"ERROR: %s produced invalid YAML — aborting, file NOT written (%s)\" % (label, path), file=sys.stderr)\n sys.exit(2)\n\n\nif action == 'apply':\n new = re.sub(r'(engine:\\s*\\$\\{MEMORY_ENGINE:-)builtin(\\})', r'\\1external\\2', content)\n if new != content: content = new; changed = True\n new = re.sub(r'(provider:\\s*\\$\\{MEMORY_EXTERNAL_PROVIDER:-)(\\})', r'\\1openviking\\2', content)\n if new != content: content = new; changed = True\n new = re.sub(r'(auto_memory_enabled:\\s*)false', r'\\1true', content)\n if new != content: content = new; changed = True\n new = re.sub(r'(proactive_recommendation:\\s*\\n\\s*enabled:\\s*)false', r'\\1true', content)\n if new != content: content = new; changed = True\n # MCP insertion: use ruamel.yaml for correct indentation (ov-fix-yaml-indent)\n try:\n from ruamel.yaml import YAML\n _yaml = YAML()\n _yaml.preserve_quotes = True\n _yaml.indent(mapping=2, sequence=4, offset=2)\n _cfg = _yaml.load(content)\n _mcp = _cfg.get('mcp', None)\n if _mcp is not None:\n _servers = _mcp.get('servers', None)\n if _servers is None:\n from ruamel.yaml.comments import CommentedSeq\n _servers = CommentedSeq()\n _mcp['servers'] = _servers\n _has_ov = any(s.get('name') == 'openviking' for s in _servers)\n if not _has_ov:\n from ruamel.yaml.comments import CommentedMap\n _ov_entry = CommentedMap()\n _ov_entry['name'] = 'openviking'\n _ov_entry['transport'] = 'streamable-http'\n _ov_entry['url'] = mcp_url\n _ov_entry['enabled'] = True\n _servers.insert(0, _ov_entry)\n import io\n _buf = io.StringIO()\n _yaml.dump(_cfg, _buf)\n content = _buf.getvalue()\n changed = True\n except Exception:\n content, _mc = _insert_mcp_entry(content, mcp_url)\n if _mc:\n changed = True\n if \" openviking:\" not in content:\n ov_block = \" openviking:\\n endpoint: ${OPENVIKING_ENDPOINT:-http://127.0.0.1:1933}\\n api_key: ${OPENVIKING_API_KEY:-}\\n account: ${OPENVIKING_ACCOUNT:-default}\\n user: ${OPENVIKING_USER:-default}\\n\"\n # ISSUE-003: --api-key was a dead parameter — the runtime env never carried\n # it, so only ${OPENVIKING_API_KEY:-} (empty) was ever expanded. Embed the\n # literal value when the entry point passed one.\n _api_key = os.environ.get(\"OV_API_KEY\", \"\")\n if _api_key:\n ov_block = ov_block.replace(\"${OPENVIKING_API_KEY:-}\", _api_key)\n _ov_mark_key_consumed()\n new = re.sub(r'( external:\\s*\\n(?: [^\\n]*\\n)*?)( lakebase:)', lambda m: m.group(1) + ov_block + m.group(2), content)\n if new != content: content = new; changed = True\n new = re.sub(r\"(account:\\s*\\$\\{OPENVIKING_ACCOUNT:-)root(\\})\", r\"\\1default\\2\", content)\n if new != content: content = new; changed = True\n if \"viking_search: allow\" not in content:\n viking_perms = \" viking_search: allow\\n viking_read: allow\\n viking_browse: allow\\n viking_remember: allow\\n viking_add_resource: allow\\n\"\n new = re.sub(r'( mem0_conclude: allow\\n)', lambda m: m.group(1) + viking_perms, content)\n if new != content: content = new; changed = True\n new = re.sub(r'(fast:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n\\s*is_proactive:\\s*)false', r'\\1true', content)\n if new != content: content = new; changed = True\n if re.search(r'(code:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n)(?!\\s*is_proactive)', content):\n new = re.sub(r'(code:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n)(?!\\s*is_proactive)', r'\\1 is_proactive: true\\n', content)\n if new != content: content = new; changed = True\n # ov-fix: code mode is_proactive false→true\n new = re.sub(r'(code:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n\\s*is_proactive:\\s*)false', r'\\1true', content)\n if new != content: content = new; changed = True\n # ov-fix: agent mode is_proactive\n if re.search(r'(agent:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n)(?!\\s*is_proactive)', content):\n new = re.sub(r'(agent:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n)(?!\\s*is_proactive)', r'\\1 is_proactive: true\\n', content)\n if new != content: content = new; changed = True\n new = re.sub(r'(agent:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n\\s*is_proactive:\\s*)false', r'\\1true', content)\n if new != content: content = new; changed = True\n # ov-fix: compression_recall_config enabled\n new = re.sub(r'(compression_recall_config:\\s*\\n\\s*enabled:\\s*)false', r'\\1true', content)\n if new != content: content = new; changed = True\n new = re.sub(r'(memory:\\s*\\n\\s*enabled:\\s*)false(\\s*\\n\\s*scenario:)', r'\\1true\\2', content)\n if new != content: content = new; changed = True\n # ov-optimization: keep_recent 0->10 + commit thresholds (ruamel.yaml for correct indentation)\n try:\n from ruamel.yaml import YAML\n _yaml2 = YAML()\n _yaml2.preserve_quotes = True\n _yaml2.indent(mapping=2, sequence=4, offset=2)\n _cfg2 = _yaml2.load(content)\n _crc = _cfg2.get('compression_recall_config') if _cfg2 else None\n if _crc is not None:\n _need_rewrite = False\n # keep_recent: 0 -> 10\n if 'keep_recent' in _crc:\n try:\n if int(_crc['keep_recent']) == 0:\n _crc['keep_recent'] = 10\n _need_rewrite = True\n except (ValueError, TypeError):\n pass\n else:\n _crc['keep_recent'] = 10\n _need_rewrite = True\n # commit thresholds for auto-commit\n if 'commit_token_threshold' not in _crc:\n _crc['commit_token_threshold'] = 20000\n _need_rewrite = True\n if 'commit_turn_threshold' not in _crc:\n _crc['commit_turn_threshold'] = 5\n _need_rewrite = True\n if _need_rewrite:\n import io as _io2\n _buf2 = _io2.StringIO()\n _yaml2.dump(_cfg2, _buf2)\n content = _buf2.getvalue()\n changed = True\n except Exception:\n # Fallback: regex with 6-space indent (compression_recall_config nested at 4sp, children at 6sp)\n new = re.sub(r'(keep_recent:\\s*)0\\b', r'\\g\u003c1>10', content)\n if new != content: content = new; changed = True\n if 'commit_token_threshold' not in content:\n new = re.sub(r'(compression_recall_config:\\s*\\n\\s*enabled:\\s*true\\s*\\n(?: \\w+: [^\\n]+\\n)*)',\n r'\\1 commit_token_threshold: 20000\\n commit_turn_threshold: 5\\n', content)\n if new != content: content = new; changed = True\n if changed:\n _validate_yaml(path, content, 'apply')\n try:\n with open(path, 'w', encoding='utf-8') as f: f.write(content)\n except Exception as e:\n print(f\"ERROR: cannot write {path}: {e}\", file=sys.stderr)\n sys.exit(1)\n\nelif action == 'revert':\n original = content\n # Match both 4-space (apply attempt 1/2) and 2-space (apply attempt 3) indent\n content = re.sub(r' - name: openviking\\n(?: .+\\n)+', '', content, count=1)\n content = re.sub(r' - name: openviking\\n(?: .+\\n)+', '', content, count=1)\n # Only collapse to [] if no server entries remain (skip blank lines)\n if not re.search(r' servers:\\n(?:\\s*\\n)* *- ', content):\n content = re.sub(r' servers:\\n(?:\\s*\\n)*', ' servers: []\\n', content, count=1)\n content = content.replace(\" servers: []\\n\\n # 示例\", \" servers: []\\n # 示例\")\n content = re.sub(r'(engine:\\s*\\$\\{MEMORY_ENGINE:-)(?:both|external)(\\})', r'\\1builtin\\2', content)\n content = re.sub(r'(provider:\\s*\\$\\{MEMORY_EXTERNAL_PROVIDER:-)openviking(\\})', r'\\1\\2', content)\n content = re.sub(r'(\\n openviking:\\n(?: [^\\n]*\\n)+)', '\\n', content)\n for perm in ('viking_search', 'viking_read', 'viking_browse', 'viking_remember', 'viking_add_resource'):\n content = re.sub(r'\\n ' + perm + r': allow\\b', '', content)\n content = re.sub(r'(auto_memory_enabled:\\s*)true', r'\\1false', content)\n content = re.sub(r'(proactive_recommendation:\\s*\\n\\s*enabled:\\s*)true', r'\\1false', content)\n content = re.sub(r'(fast:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n\\s*is_proactive:\\s*)true', r'\\1false', content)\n content = re.sub(r'(code:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n\\s*is_proactive:\\s*)true', r'\\1false', content)\n content = re.sub(r'(agent:\\s*\\n\\s*memory:\\s*\\n\\s*enabled:\\s*true\\s*\\n\\s*is_proactive:\\s*)true', r'\\1false', content)\n content = re.sub(r'(compression_recall_config:\\s*\\n\\s*enabled:\\s*)true', r'\\1false', content)\n content = re.sub(r'(memory:\\s*\\n\\s*enabled:\\s*)true(\\s*\\n\\s*scenario:)', r'\\1false\\2', content)\n # ov-optimization: revert keep_recent 10->0 + remove thresholds (ruamel.yaml for correct indentation)\n try:\n from ruamel.yaml import YAML\n _yaml3 = YAML()\n _yaml3.preserve_quotes = True\n _yaml3.indent(mapping=2, sequence=4, offset=2)\n _cfg3 = _yaml3.load(content)\n _crc3 = _cfg3.get('compression_recall_config') if _cfg3 else None\n if _crc3 is not None:\n _need_revert = False\n if 'keep_recent' in _crc3:\n try:\n if int(_crc3['keep_recent']) == 10:\n _crc3['keep_recent'] = 0\n _need_revert = True\n except (ValueError, TypeError):\n pass\n for _key in ('commit_token_threshold', 'commit_turn_threshold'):\n if _key in _crc3:\n del _crc3[_key]\n _need_revert = True\n if _need_revert:\n import io as _io3\n _buf3 = _io3.StringIO()\n _yaml3.dump(_cfg3, _buf3)\n content = _buf3.getvalue()\n except Exception:\n # Fallback: regex (best-effort)\n content = re.sub(r'(keep_recent:\\s*)10\\b', r'\\g\u003c1>0', content)\n content = re.sub(r'\\n commit_token_threshold: \\d+\\n commit_turn_threshold: \\d+', '', content)\n if content != original:\n _validate_yaml(path, content, 'revert')\n try:\n with open(path, 'w', encoding='utf-8') as f: f.write(content)\n except Exception as e:\n print(f\"ERROR: cannot write {path}: {e}\", file=sys.stderr)\n sys.exit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13984,"content_sha256":"c66e8ae436768ab028636a833c59ee3ab60840d9974ecbc5be278c2f853fdcbb"},{"filename":"scripts/lib/ov_kimi_hook.sh","content":"#!/usr/bin/env bash\nset -uo pipefail # ROB-1: catch unset vars + pipe failures (omit -e: hook is error-tolerant by design)\n# ov-kimi-hook.sh — KimiCode OpenViking hook dispatcher (v3)\n# Usage: ov-kimi-hook.sh \u003cevent> (reads JSON from stdin)\n# Events: session-start | user-prompt | stop | session-end | pre-compact | pre-tool-use | subagent-start | subagent-stop\n#\n# v3 (aligned with the official @openviking/opencode-plugin / dsh-memory-plugin):\n# - P0-1: incremental idempotent wire.jsonl capture via byte-offset cursor\n# (fixes \"last user + last-5 assistant\" loss; no re-upload, no dropped turns)\n# - P0-2: commit gate driven by server pending_tokens (GET /sessions/{sid}),\n# with keep_recent_count; turn/chars as local fallback\n# - P1-3: durable pending queue (~/.openviking/pending, 0700/0600, sha256 dedup,\n# .processing rename, 3 retries / 7d TTL, replay on session-start + stop)\n# - P1-4: Bash no longer blocked by the PreToolUse viking:// URI guard\n# - P2-5: signal filter drops ack / slash / punctuation / too-short text\n# - P2-6: session-start injects \u003csession-archive> from GET context archive overview\n# - P2-7: recall adds purpose:\"coding\" + dedup_turns (+ optional peer_scope),\n# 400/422/404/405/5xx → /api/v1/search/recall fallback\n# - P2-8: config-file values now actually read (openviking-config.json),\n# env > config > default precedence\n# Preserved: redaction, sanitize, git peer-id, bypass env, per-session flock,\n# subagent isolation, crash-recovery sweep.\n\nOV_ENDPOINT=\"${OV_ENDPOINT:-http://127.0.0.1:1933}\"\nOV_HOME=\"${OV_HOME:-${HOME:-/root}}\"\nOV_RUNTIME_DIR=\"${OV_RUNTIME_DIR:-${OV_HOME}/runtime}\"\nOV_STATE_DIR=\"${OV_STATE_DIR:-/tmp/ov-kimi-hooks}\"\n# KIMI_CODE_HOME is set by the job-env start.sh (e.g. /root/runtime/kimicode/data),\n# which is the correct parent of the sessions/ dir — prefer it over $OV_HOME/runtime.\nKC_DATA_DIR=\"${KC_DATA_DIR:-${KIMI_CODE_HOME:-${OV_RUNTIME_DIR}/kimicode/data}}\"\nOV_PY3=\"${OV_PY3:-$(command -v python3.12 2>/dev/null || command -v python3.11 2>/dev/null || command -v python3.10 2>/dev/null || command -v python3 2>/dev/null || echo python3)}\"\nOV_PY=\"${OV_PY:-$OV_PY3}\"\nOV_PY_DIR=\"${OV_PY_DIR:-$(dirname \"$(readlink -f \"$0\")\")/../py}\"\nOV_CURL_BIN=\"${OV_CURL_BIN:-$(command -v curl 2>/dev/null || echo curl)}\"\nOV_PENDING_DIR=\"${OV_PENDING_DIR:-${OV_HOME}/.openviking/pending}\"\nOV_PENDING_MAX_RETRIES=\"${OV_PENDING_MAX_RETRIES:-3}\"\nOV_PENDING_TTL_DAYS=\"${OV_PENDING_TTL_DAYS:-7}\"\nOV_PENDING_REPLAY_LIMIT=\"${OV_PENDING_REPLAY_LIMIT:-50}\"\nOV_MAX_ATTEMPTS=\"${OV_MAX_ATTEMPTS:-3}\"\nOV_COMMIT_TURN_THRESHOLD=\"${OV_COMMIT_TURN_THRESHOLD:-5}\"\nOV_COMMIT_CHAR_THRESHOLD=\"${OV_COMMIT_CHAR_THRESHOLD:-20000}\"\nOV_RECALL_DEDUP_TURNS=\"${OV_RECALL_DEDUP_TURNS:-5}\"\nOV_RECALL_PEER_SCOPE=\"${OV_RECALL_PEER_SCOPE:-}\"\nOV_CAPTURE_USER_MAX_CHARS=\"${OV_CAPTURE_USER_MAX_CHARS:-8000}\"\nOV_CAPTURE_ASSISTANT_MAX_CHARS=\"${OV_CAPTURE_ASSISTANT_MAX_CHARS:-32000}\"\n\nmkdir -p \"$OV_STATE_DIR\" && chmod 700 \"$OV_STATE_DIR\" 2>/dev/null || true\nEVENT=\"$1\"; INPUT=$(cat)\n\n# ── Python CLI dispatch ──\n_ov_kimi_hook(){ \"$OV_PY\" \"$OV_PY_DIR/ov_kimi_hook.py\" \"$@\"; }\n\n# ── Helpers ──\n_jf(){ echo \"$INPUT\"|_ov_kimi_hook json-field \"$1\" 2>/dev/null;}\n_sid(){ echo -n \"$1\"|md5sum|cut -c1-16;}\n_ov_out(){ _ov_kimi_hook hook-output \"$1\" 2>/dev/null;}\n_ov_deny(){ _ov_kimi_hook hook-deny \"$1\" 2>/dev/null;}\n_ov_log(){ echo \"[ov-hook:$EVENT] $*\" >&2;}\n_json_str(){ _ov_kimi_hook json-encode \"$1\" 2>/dev/null;}\n\n# Shared response buffer + cleanup (one hook process per event; $ is unique)\n_OV_RESP=\"${OV_STATE_DIR}/ov-resp-$\"\ntrap 'rm -f \"$_OV_RESP\" 2>/dev/null' EXIT\n\n# ── Status-aware POST with bounded retry ──\n# Echoes nothing on stdout; stores the HTTP code in _OV_CODE and the response\n# body in $_OV_RESP. Retries transient failures (408/429/5xx/409-retryable/000)\n# up to OV_MAX_ATTEMPTS, then gives up (callers enqueue to the pending queue).\n_ov_send(){\n local path=\"$1\" body=\"$2\" i code\n i=0; code=\"\"\n while [ \"$i\" -lt \"$OV_MAX_ATTEMPTS\" ]; do\n i=$((i+1))\n code=$(\"$OV_CURL_BIN\" -s -o \"$_OV_RESP\" -w \"%{http_code}\" --connect-timeout 3 --max-time 12 -X POST \"${OV_ENDPOINT}${path}\" -H 'Content-Type: application/json' -H 'X-OpenViking-Agent: kimicode' -d \"$body\" 2>/dev/null)\n case \"$code\" in\n 2*) break ;;\n 409) grep -q '\"retryable\"[[:space:]]*:[[:space:]]*true' \"$_OV_RESP\" 2>/dev/null || break ;;\n 408|429|5[0-9][0-9]) : ;;\n *) break ;;\n esac\n [ \"$i\" -eq \"$OV_MAX_ATTEMPTS\" ] && break\n sleep 1\n done\n _OV_CODE=\"$code\"\n}\n\n# ── Retryability test (P1-3) ──\n_ov_retryable(){\n case \"$1\" in\n 408|429|5[0-9][0-9]) return 0 ;;\n esac\n if [ \"$1\" = \"409\" ] && [ -n \"$2\" ]; then\n echo \"$2\" | grep -q '\"retryable\"[[:space:]]*:[[:space:]]*true' && return 0\n fi\n return 1\n}\n\n# ── Pending queue ──\n_ov_queue_add(){ # $1=kind $2=path $3=body\n mkdir -p \"$OV_PENDING_DIR\" && chmod 700 \"$OV_PENDING_DIR\" 2>/dev/null || true\n _ov_kimi_hook pending-enqueue \"$OV_PENDING_DIR\" \"$1\" \"$2\" \"$3\" 2>/dev/null\n}\n\n_ov_post(){ # $1=path $2=body $3=kind(default:message); enqueues on retryable failure\n local path=\"$1\" body=\"$2\" kind=\"${3:-message}\"\n _ov_send \"$path\" \"$body\"\n if [ \"${_OV_CODE:-000}\" = \"000\" ] || _ov_retryable \"${_OV_CODE:-000}\" \"$(cat \"$_OV_RESP\" 2>/dev/null)\"; then\n _ov_queue_add \"$kind\" \"$path\" \"$body\"\n _ov_log \"queue+ http=${_OV_CODE} ${path}\"\n return 1\n fi\n return 0\n}\n\n_ov_replay(){ # claim + deliver pending items (idempotent; bounded)\n [ -d \"$OV_PENDING_DIR\" ] || return 0\n local i=0 file claimed path body code resp\n for file in \"$OV_PENDING_DIR\"/*.json; do\n [ -e \"$file\" ] || continue\n [ \"$i\" -ge \"$OV_PENDING_REPLAY_LIMIT\" ] && break\n i=$((i+1))\n claimed=\"${file}.processing\"\n mv \"$file\" \"$claimed\" 2>/dev/null || continue\n if [ \"$(_ov_kimi_hook pending-should-send \"$claimed\" \"$OV_PENDING_MAX_RETRIES\" \"$OV_PENDING_TTL_DAYS\" 2>/dev/null)\" != \"SEND\" ]; then\n rm -f \"$claimed\" 2>/dev/null; continue\n fi\n path=$(_ov_kimi_hook pending-get \"$claimed\" path 2>/dev/null)\n body=$(_ov_kimi_hook pending-body \"$claimed\" 2>/dev/null)\n [ -z \"$path\" ] && { rm -f \"$claimed\" 2>/dev/null; continue; }\n _ov_send \"$path\" \"$body\"\n code=\"${_OV_CODE:-000}\"; resp=$(cat \"$_OV_RESP\" 2>/dev/null)\n if [ \"$code\" = \"000\" ] || _ov_retryable \"$code\" \"$resp\"; then\n _ov_kimi_hook pending-bump \"$claimed\" 2>/dev/null\n mv \"$claimed\" \"$file\" 2>/dev/null\n _ov_log \"replay retryable http=${code}: ${path}\"\n else\n rm -f \"$claimed\" 2>/dev/null\n _ov_log \"replay settled http=${code}: ${path}\"\n fi\n done\n}\n\n_ov_pending_tokens(){ # GET /sessions/{sid} → pending_tokens int (0 on failure)\n local R\n R=$(\"$OV_CURL_BIN\" -s --connect-timeout 3 --max-time 8 -H 'Accept: application/json' \"${OV_ENDPOINT}/api/v1/sessions/$1\" 2>/dev/null)\n echo \"$R\" | _ov_kimi_hook pending-tokens 2>/dev/null\n}\n\n_ov_signal_filter(){ _ov_kimi_hook filter-signal \"$1\" 2>/dev/null; }\n\n# ── Config-file resolution (P2-8): env > openviking-config.json > default ──\nOV_CONFIG_FILE=\"${OV_CONFIG_FILE:-${OV_HOME}/.config/opencode/openviking-config.json}\"\n_ov_conf(){\n local key=\"$1\" def=\"${2:-}\" v\n if [ -n \"$OV_CONFIG_FILE\" ] && [ -f \"$OV_CONFIG_FILE\" ]; then\n v=$(_ov_kimi_hook config-get \"$OV_CONFIG_FILE\" \"$key\" 2>/dev/null)\n [ -n \"$v\" ] && { printf '%s' \"$v\"; return; }\n fi\n printf '%s' \"$def\"\n}\nOV_COMMIT_TOKEN_THRESHOLD=\"${OV_COMMIT_TOKEN_THRESHOLD:-$(_ov_conf commitTokenThreshold 20000)}\"\nOV_COMMIT_KEEP_RECENT_COUNT=\"${OV_COMMIT_KEEP_RECENT_COUNT:-$(_ov_conf commitKeepRecentCount 10)}\"\nOV_RESUME_CONTEXT_BUDGET=\"${OV_RESUME_CONTEXT_BUDGET:-$(_ov_conf resumeContextBudget 32000)}\"\nOV_RECALL_LIMIT=\"${OV_RECALL_LIMIT:-$(_ov_conf recallLimit 6)}\"\nOV_RECALL_SCORE_THRESHOLD=\"${OV_RECALL_SCORE_THRESHOLD:-$(_ov_conf autoRecall.scoreThreshold 0.35)}\"\nOV_RECALL_MAX_CHARS=\"${OV_RECALL_MAX_CHARS:-$(_ov_conf recallMaxContentChars 4000)}\"\n\nKC_SID=$(_jf session_id);CWD=$(_jf cwd)\nif [ -n \"$KC_SID\" ];then SID=\"kimi-$(_sid \"$KC_SID\")\";else SID=\"kimi-$(_sid \"$CWD\")\";fi\nSTFILE=\"${OV_STATE_DIR}/ws-$(_sid \"${CWD:-default}\").state\"\nLOCKFILE=\"${OV_STATE_DIR}/ws-$(_sid \"${CWD:-default}\").lock\"\n\n# ── Bypass check ──\n_ov_bypass(){\n [ \"${OPENVIKING_BYPASS_SESSION:-}\" = \"1\" ] && return 0\n if [ -n \"${OPENVIKING_BYPASS_SESSION_PATTERNS:-}\" ] && [ -n \"$CWD\" ]; then\n echo \"$OPENVIKING_BYPASS_SESSION_PATTERNS\" | tr ',' '\\n' | while read -r pat; do\n [ -n \"$pat\" ] && echo \"$CWD\" | grep -q \"$pat\" && return 0\n done && return 0\n fi\n return 1\n}\n\n# ── Git-based peer derivation ──\n_ov_peer_id(){\n local dir=\"${1:-$CWD}\"\n _ov_kimi_hook peer-id \"$dir\" 2>/dev/null\n}\n\n# ── Locking (flock-based, kernel-level advisory lock — no TOCTOU) ──\n_ov_lock(){\n exec 9>\"${LOCKFILE}\"\n flock -w 2 9\n}\n_ov_unlock(){ flock -u 9 2>/dev/null || true; }\n\n# ── Memory pollution prevention: strip injected tags ──\n_ov_sanitize(){\n _ov_kimi_hook sanitize \"$1\" 2>/dev/null\n}\n\n# ── PII/Secret redaction before transmission to OpenViking ──\n_ov_redact(){\n if [ \"${OV_REDACT_SECRETS:-true}\" != \"true\" ]; then printf '%s' \"$1\"; return; fi\n local _tmp; _tmp=$(mktemp 2>/dev/null) || { printf '%s' \"$1\"; return; }\n printf '%s' \"$1\" > \"$_tmp\"\n \"$OV_PY\" \"$OV_PY_DIR/ov_redact.py\" redact \"$_tmp\" 2>/dev/null || printf '%s' \"$1\"\n rm -f \"$_tmp\" 2>/dev/null\n}\n\n# ── Extract user/assistant text from wire.jsonl (incremental, idempotent) ──\n_ov_extract_wire(){\n local kc_sid=\"$1\" cursor=\"$2\" sd wf\n [ -z \"$kc_sid\" ] && return\n sd=$(find \"$KC_DATA_DIR/sessions\" -maxdepth 2 -type d \\( -name \"$kc_sid\" -o -name \"session_${kc_sid}\" \\) 2>/dev/null | head -1)\n [ -z \"$sd\" ] && return\n wf=\"$sd/agents/main/wire.jsonl\"\n [ ! -f \"$wf\" ] && return\n _ov_kimi_hook extract-wire \"$wf\" \"$cursor\" \"$OV_CAPTURE_USER_MAX_CHARS\" \"$OV_CAPTURE_ASSISTANT_MAX_CHARS\" 2>/dev/null\n}\n\n# ── Recall search and format (P2-7) ──\n_ov_recall(){\n local query=\"$1\" session_id=\"$2\" code fb body\n body=\"{\\\"query\\\":$(_json_str \"$query\"),\\\"mode\\\":\\\"context\\\",\\\"purpose\\\":\\\"coding\\\",\\\"session_id\\\":\\\"${session_id}\\\",\\\"limit\\\":${OV_RECALL_LIMIT},\\\"score_threshold\\\":${OV_RECALL_SCORE_THRESHOLD},\\\"dedup_turns\\\":${OV_RECALL_DEDUP_TURNS}\"\n [ -n \"$OV_RECALL_PEER_SCOPE\" ] && body=\"${body},\\\"peer_scope\\\":$(_json_str \"$OV_RECALL_PEER_SCOPE\")\"\n body=\"${body}}\"\n _ov_send \"/api/v1/search/search\" \"$body\"\n code=\"${_OV_CODE:-000}\"\n case \"$code\" in\n 400|422|404|405|5[0-9][0-9])\n _ov_log \"context search http=${code} → recall fallback\"\n fb=\"{\\\"query\\\":$(_json_str \"$query\"),\\\"session_id\\\":\\\"${session_id}\\\",\\\"score_threshold\\\":${OV_RECALL_SCORE_THRESHOLD},\\\"dedup_turns\\\":${OV_RECALL_DEDUP_TURNS},\\\"max_chars\\\":${OV_RECALL_MAX_CHARS}}\"\n _ov_send \"/api/v1/search/recall\" \"$fb\"\n ;;\n esac\n cat \"$_OV_RESP\" 2>/dev/null | _ov_kimi_hook recall-format \"${OV_RECALL_MAX_CHARS}\" \"${OV_RECALL_SCORE_THRESHOLD}\" 2>/dev/null\n}\n\n# ── Commit session (P0-2: keep_recent_count) ──\n_ov_commit(){\n local session_id=\"$1\" reason=\"$2\"\n _ov_post \"/api/v1/sessions/${session_id}/commit\" \"{\\\"reason\\\":\\\"${reason:-auto}\\\",\\\"keep_recent_count\\\":${OV_COMMIT_KEEP_RECENT_COUNT}}\" \"commit\"\n}\n\n# ── Update state file ──\n_ov_state_update(){\n _ov_kimi_hook state-update \"$1\" \"$2\" \"$STFILE\" 2>/dev/null\n}\n\n# ── Early bypass exit ──\nif _ov_bypass; then _ov_out \"\"; exit 0; fi\n\n# ── Main event dispatch ──\ncase \"$EVENT\" in\n session-start)\n SOURCE=$(_jf source)\n PEER_ID=$(_ov_peer_id)\n echo \"{\\\"sid\\\":\\\"${SID}\\\",\\\"kc_sid\\\":\\\"${KC_SID}\\\",\\\"cwd\\\":\\\"${CWD}\\\",\\\"peer_id\\\":\\\"${PEER_ID}\\\",\\\"started\\\":$(date +%s),\\\"turn_count\\\":0,\\\"accumulated_chars\\\":0,\\\"needs_commit\\\":false,\\\"last_recall_hash\\\":\\\"\\\"}\">\"$STFILE\"\n _ov_log \"sid=${SID} source=${SOURCE} peer=${PEER_ID}\"\n\n # Crash recovery: scan for orphaned sessions with needs_commit=true\n for sf in \"$OV_STATE_DIR\"/ws-*.state; do\n [ -f \"$sf\" ] || continue\n [ \"$sf\" = \"$STFILE\" ] && continue\n ORPHAN_SID=$(_ov_kimi_hook state-orphan-check \"$sf\" 2>/dev/null)\n if [ -n \"$ORPHAN_SID\" ]; then\n _ov_log \"crash recovery: committing orphan session ${ORPHAN_SID}\"\n _ov_commit \"$ORPHAN_SID\" \"crash-recovery\"\n _ov_kimi_hook state-mark-crash-recovered \"$sf\" 2>/dev/null\n fi\n done\n\n # Replay pending items left by a previous run (P1-3)\n _ov_replay\n\n # P2-6: inject \u003csession-archive> overview + workspace recall context\n ARCHIVE=$(\"$OV_CURL_BIN\" -s --connect-timeout 3 --max-time 8 -H 'Accept: application/json' \"${OV_ENDPOINT}/api/v1/sessions/${SID}/context?token_budget=${OV_RESUME_CONTEXT_BUDGET}\" 2>/dev/null | _ov_kimi_hook archive-overview 2>/dev/null)\n RECALL=$(_ov_recall \"workspace context: ${CWD##*/}\" \"$SID\")\n INJECT=$(printf '%s' \"$RECALL\" | _ov_kimi_hook session-inject \"${SID}\" \"$ARCHIVE\" 2>/dev/null)\n if [ -n \"$INJECT\" ]; then\n _ov_out \"$INJECT\"\n else\n _ov_out \"\"\n fi;;\n\n user-prompt)\n PROMPT=$(_jf prompt)\n [ -f \"$STFILE\" ]&&_ov_kimi_hook state-set-prompt \"$STFILE\" \"$PROMPT\" \"$(date +%s)\" 2>/dev/null\n\n # Recall with dedup\n QUERY_HASH=$(echo -n \"$PROMPT\" | md5sum | cut -c1-8)\n LAST_HASH=\"\"; [ -f \"$STFILE\" ]&&LAST_HASH=$(_ov_kimi_hook state-get \"$STFILE\" last_recall_hash 2>/dev/null)\n\n if [ \"$QUERY_HASH\" != \"$LAST_HASH\" ]; then\n RECALL=$(_ov_recall \"$(_ov_redact \"$PROMPT\")\" \"$SID\")\n if [ -n \"$RECALL\" ]; then\n _ov_state_update \"last_recall_hash\" \"$QUERY_HASH\"\n echo \"$RECALL\" > \"${OV_STATE_DIR}/recall-$(_sid \"${CWD:-default}\").md\"\n chmod 600 \"${OV_STATE_DIR}/recall-$(_sid \"${CWD:-default}\").md\" 2>/dev/null || true\n _ov_out \"$RECALL\"\n else\n _ov_out \"\"\n fi\n else\n _ov_out \"\"\n fi;;\n\n pre-tool-use)\n TOOL_NAME=$(_jf tool_name)\n TOOL_INPUT=$(_jf tool_input)\n case \"$TOOL_NAME\" in\n Read|Glob|Grep|Write|Edit)\n echo \"$TOOL_INPUT\" | _ov_kimi_hook check-viking-uri 2>/dev/null | grep -q \"BLOCK\" && {\n _ov_deny \"viking:// URIs are virtual paths managed by OpenViking. Use MCP tools instead: openviking_read, openviking_search, openviking_glob, openviking_grep, openviking_list.\"\n exit 0\n }\n ;;\n esac\n _ov_out \"\";;\n\n stop)\n _ov_log \"sid=${SID} kc_sid=${KC_SID}\"\n\n if ! _ov_lock; then _ov_log \"could not acquire lock, skipping\"; _ov_out \"\"; exit 0; fi\n date +%s > \"${LOCKFILE}.ts\" 2>/dev/null\n\n # Flush queued items first (P1-3)\n _ov_replay\n\n # P0-1: incremental capture (cursor advanced atomically by extract-wire)\n CURSOR_FILE=\"${OV_STATE_DIR}/wire-${SID}.cursor\"\n WD=$(_ov_extract_wire \"$KC_SID\" \"$CURSOR_FILE\"); UT=\"\"; AT=\"\"\n if [ -n \"$WD\" ];then\n UT=$(echo \"$WD\"|_ov_kimi_hook json-stdin-get user 2>/dev/null)\n AT=$(echo \"$WD\"|_ov_kimi_hook json-stdin-get assistant 2>/dev/null)\n fi\n if [ -z \"$UT\" ]&&[ -f \"$STFILE\" ];then UT=$(_ov_kimi_hook state-get \"$STFILE\" last_prompt 2>/dev/null);fi\n\n # Sanitize + redact + signal filter (P2-5)\n UT=$(_ov_sanitize \"$UT\")\n AT=$(_ov_sanitize \"$AT\")\n UT=$(_ov_redact \"$UT\")\n AT=$(_ov_redact \"$AT\")\n UT=$(_ov_signal_filter \"$UT\")\n AT=$(_ov_signal_filter \"$AT\")\n\n _ov_log \"user_len=${#UT} assistant_len=${#AT}\"\n\n if [ -n \"$UT\" ];then\n ESC=$(_json_str \"$UT\")\n _ov_post \"/api/v1/sessions/${SID}/messages\" \"{\\\"role\\\":\\\"user\\\",\\\"parts\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":${ESC}}]}\" \"message\"\n fi\n if [ -n \"$AT\" ];then\n EA=$(_json_str \"$AT\")\n _ov_post \"/api/v1/sessions/${SID}/messages\" \"{\\\"role\\\":\\\"assistant\\\",\\\"parts\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":${EA}}]}\" \"message\"\n else\n _ov_post \"/api/v1/sessions/${SID}/messages\" \"{\\\"role\\\":\\\"assistant\\\",\\\"parts\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Turn completed.\\\"}]}\" \"message\"\n fi\n\n # Update state with accumulated stats\n TURN_COUNT=0; ACCUM_CHARS=0\n [ -f \"$STFILE\" ]&&TURN_COUNT=$(_ov_kimi_hook state-get \"$STFILE\" turn_count 0 2>/dev/null)\n [ -f \"$STFILE\" ]&&ACCUM_CHARS=$(_ov_kimi_hook state-get \"$STFILE\" accumulated_chars 0 2>/dev/null)\n TURN_COUNT=$((TURN_COUNT + 1))\n ACCUM_CHARS=$((ACCUM_CHARS + ${#UT} + ${#AT}))\n\n # P0-2: commit gate — server pending_tokens primary, turn/chars fallback\n PT=$(_ov_pending_tokens \"$SID\")\n COMMITTED=false\n if [ -n \"$PT\" ] && [ \"$PT\" -ge \"$OV_COMMIT_TOKEN_THRESHOLD\" ] 2>/dev/null; then\n _ov_commit \"$SID\" \"threshold-pending-${PT}\"\n TURN_COUNT=0; ACCUM_CHARS=0; COMMITTED=true\n _ov_log \"commit via pending_tokens=${PT}\"\n elif [ \"$TURN_COUNT\" -ge \"$OV_COMMIT_TURN_THRESHOLD\" ] || [ \"$ACCUM_CHARS\" -ge \"$OV_COMMIT_CHAR_THRESHOLD\" ]; then\n _ov_commit \"$SID\" \"threshold-turn-${TURN_COUNT}\"\n TURN_COUNT=0; ACCUM_CHARS=0; COMMITTED=true\n _ov_log \"commit via fallback threshold (turn/chars)\"\n fi\n if [ \"$COMMITTED\" = true ]; then NEEDS_COMMIT=false; else NEEDS_COMMIT=true; fi\n\n _ov_kimi_hook state-update-stop \"$STFILE\" \"$NEEDS_COMMIT\" \"$TURN_COUNT\" \"$ACCUM_CHARS\" 2>/dev/null\n\n _ov_unlock\n _ov_out \"\";;\n\n session-end|pre-compact)\n REASON=$(_jf reason);TRIGGER=$(_jf trigger)\n _ov_log \"sid=${SID} reason=${REASON:-${TRIGGER:-compact}}\"\n\n # Flush queued items first (P1-3)\n _ov_replay\n\n NC=false;[ -f \"$STFILE\" ]&&NC=$(_ov_kimi_hook state-get-bool \"$STFILE\" needs_commit 2>/dev/null)\n TC=0;[ -f \"$STFILE\" ]&&TC=$(_ov_kimi_hook state-get \"$STFILE\" turn_count 0 2>/dev/null)\n\n if [ \"$NC\" = \"true\" ] || [ \"$TC\" -gt 0 ]; then\n if [ \"$EVENT\" = \"session-end\" ]; then\n # Detached worker to avoid timeout\n (\n _ov_commit \"$SID\" \"session-end\"\n [ -f \"$STFILE\" ]&&_ov_kimi_hook state-mark-committed \"$STFILE\" 2>/dev/null\n ) & disown 2>/dev/null || true\n _ov_out \"OpenViking session committed. Memory extraction in progress.\"\n else\n # PreCompact: synchronous\n _ov_commit \"$SID\" \"pre-compact\"\n [ -f \"$STFILE\" ]&&_ov_kimi_hook state-mark-committed \"$STFILE\" 2>/dev/null\n _ov_out \"\"\n fi\n else\n _ov_out \"\"\n fi;;\n\n\n\n subagent-start)\n # SubagentStart: create isolated OV session for this subagent\n AGENT_NAME=$(_jf agent_name)\n SUBAGENT_PROMPT=$(_jf prompt)\n [ -z \"$AGENT_NAME\" ] && AGENT_NAME=\"unknown\"\n\n # Derive isolated subagent session ID\n SUBAGENT_SID=\"${SID}__subagent-${AGENT_NAME}-$(date +%s)\"\n SUBAGENT_STATE=\"${OV_STATE_DIR}/subagent-${SID}-${AGENT_NAME}.state\"\n\n PEER_ID=$(_ov_peer_id)\n # Save state (use Python for safe JSON writing)\n _ov_kimi_hook subagent-state-save \"$SUBAGENT_STATE\" \"$SID\" \"$SUBAGENT_SID\" \"$AGENT_NAME\" \"$PEER_ID\" \"$(date +%s)\" 2>/dev/null\n\n _ov_log \"subagent-start: agent=${AGENT_NAME} subagent_sid=${SUBAGENT_SID}\"\n _ov_out \"\";;\n\n subagent-stop)\n # SubagentStop: capture subagent work to isolated session, then commit\n AGENT_NAME=$(_jf agent_name)\n SUBAGENT_RESPONSE=$(_jf response)\n [ -z \"$AGENT_NAME\" ] && AGENT_NAME=\"unknown\"\n\n SUBAGENT_STATE=\"${OV_STATE_DIR}/subagent-${SID}-${AGENT_NAME}.state\"\n\n # Load subagent state\n SUBAGENT_SID=\"\"\n if [ -f \"$SUBAGENT_STATE\" ]; then\n SUBAGENT_SID=$(_ov_kimi_hook state-get \"$SUBAGENT_STATE\" subagent_sid 2>/dev/null)\n fi\n # Fallback: derive session ID if state file missing\n if [ -z \"$SUBAGENT_SID\" ]; then\n SUBAGENT_SID=\"${SID}__subagent-${AGENT_NAME}-orphan\"\n fi\n\n _ov_log \"subagent-stop: agent=${AGENT_NAME} subagent_sid=${SUBAGENT_SID}\"\n\n # Try to extract full transcript from subagent wire.jsonl (best-effort, opt-in)\n SUBAGENT_FULL=\"\"\n if [ -n \"$KC_SID\" ] && [ \"${OV_CAPTURE_SUBAGENT:-false}\" = \"true\" ]; then\n SUBAGENT_FULL=$(_ov_kimi_hook extract-subagent-transcript \"$KC_SID\" \"$KC_DATA_DIR\" 2>/dev/null)\n fi\n\n # Use full transcript if available, otherwise fall back to hook payload\n if [ -n \"$SUBAGENT_FULL\" ]; then\n CAPTURE_TEXT=\"$SUBAGENT_FULL\"\n elif [ -n \"$SUBAGENT_RESPONSE\" ]; then\n CAPTURE_TEXT=\"$SUBAGENT_RESPONSE\"\n else\n CAPTURE_TEXT=\"\"\n fi\n\n # Sanitize + redact\n CAPTURE_TEXT=$(_ov_sanitize \"$CAPTURE_TEXT\")\n CAPTURE_TEXT=$(_ov_redact \"$CAPTURE_TEXT\")\n\n # Push subagent work to isolated session\n if [ -n \"$CAPTURE_TEXT\" ]; then\n ESC=$(_json_str \"$CAPTURE_TEXT\")\n _ov_post \"/api/v1/sessions/${SUBAGENT_SID}/messages\" \"{\\\"role\\\":\\\"assistant\\\",\\\"parts\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":${ESC}}]}\" \"message\"\n fi\n\n # Commit the subagent session immediately (detached to avoid timeout)\n (\n _ov_commit \"$SUBAGENT_SID\" \"subagent-stop\"\n ) & disown 2>/dev/null || true\n\n # Clean up state\n rm -f \"$SUBAGENT_STATE\" 2>/dev/null\n _ov_out \"\";;\n\n\n *) _ov_out \"\";;\nesac","content_type":"application/x-sh; charset=utf-8","language":"bash","size":20706,"content_sha256":"3fe66f8cb670e87385e53802c6048d07eb418c702cb81a15df00ddf4c4bdbcaa"},{"filename":"scripts/lib/plugins.sh","content":"#!/bin/bash\n# lib/plugins.sh — Plugin provisioning: on-demand install, domestic-first, SHA-1 verified.\n# npm: mirrors.huaweicloud.com → npmmirror → npmjs.org\n# GitHub raw files: proxy mirrors → direct raw.githubusercontent.com (byte-verified later\n# against the official GitHub tree SHA). API metadata (commit SHA, recursive tree JSON)\n# goes through its own proxy-first chain with direct api.github.com as fallback. Chains\n# are overridable via OPENVIKING_GH_RAW_MIRRORS / OPENVIKING_GITHUB_API_MIRROR /\n# OPENVIKING_NPM_REGISTRIES (see notes.md).\n: \"${OV_PY_DIR:=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../py\" && pwd)}\"\n_ov_plugins() { \"$OV_PY\" \"$OV_PY_DIR/ov_plugins.py\" \"$@\"; }\nOV_PLUGIN_REPO=\"https://github.com/volcengine/OpenViking.git\"\nOV_PLUGIN_API=\"https://api.github.com/repos/volcengine/OpenViking\"\nOV_PLUGIN_REPO_HOST=\"github.com\"\nOV_PLUGIN_REPO_PATH=\"volcengine/OpenViking\"\n# npm registries, domestic mirrors first. OPENVIKING_NPM_REGISTRIES (space-separated)\n# is prepended to this list when set (health-checked by ov_first_npm_registry).\nNPM_REGISTRIES=(\n \"https://mirrors.huaweicloud.com/repository/npm/\"\n \"https://registry.npmmirror.com/\"\n \"https://registry.npmjs.org/\"\n)\n# GitHub raw file mirrors, domestic first. Each entry is a base URL; the repo-relative\n# path (volcengine/OpenViking/\u003csha>/...) is appended directly. OPENVIKING_GH_RAW_MIRRORS\n# (space-separated) replaces the defaults; direct raw.githubusercontent.com is always\n# kept as the final fallback. ov_pick_gh_mirror() health-checks the list once per run.\nGH_RAW_MIRROR_DEFAULTS=(\n \"https://gh-proxy.com/https://raw.githubusercontent.com\"\n \"https://ghfast.top/https://raw.githubusercontent.com\"\n \"https://raw.githubusercontent.com\"\n)\nGH_RAW_MIRRORS=(\"${GH_RAW_MIRROR_DEFAULTS[@]}\")\nif [[ -n \"${OPENVIKING_GH_RAW_MIRRORS:-}\" ]]; then\n read -r -a GH_RAW_MIRRORS \u003c\u003c\u003c \"$OPENVIKING_GH_RAW_MIRRORS\"\nfi\n# GitHub API mirror chain (commit SHA + recursive tree JSON), domestic first. Each\n# entry is the full API base URL; callers append /commits/HEAD or /git/trees/...\n# OPENVIKING_PLUGIN_API_URL (or OPENVIKING_GITHUB_API_MIRROR) wins over this chain.\nGH_API_MIRROR_DEFAULTS=(\n \"https://gh-proxy.com/https://api.github.com/repos/volcengine/OpenViking\"\n \"https://api.github.com/repos/volcengine/OpenViking\"\n)\n# name|npm package (empty = GitHub-only)|upstream example dir|npm peer deps (optional)\n# opencode-plugin: published to npm; pure .mjs, zero runtime deps\n# openclaw-plugin: published to npm; needs @sinclair/typebox + fflate after tsc build\n# dsh/pi-coding-agent-extension: NOT published to npm — pulled from GitHub only\n# Version policy: NEVER pin host-coupled peer deps. dsh-llm/dsh-tools are synced\n# from the host dsh runtime node_modules (see dsh_sync_peer_deps), so @latest here\n# is only a fallback when the host install is missing. openclaw typebox/fflate stay\n# pinned because they are build deps of the plugin itself, not host-coupled.\nOV_PLUGINS=(\n \"opencode-plugin|@openviking/opencode-plugin|examples/opencode-plugin|\"\n \"openclaw-plugin|@openviking/openclaw-plugin|examples/openclaw-plugin|@sinclair/[email protected] fflate@^0.8.2\"\n \"dsh-memory-plugin||examples/dsh-memory-plugin|@deepseek-ai/dsh-llm@latest @deepseek-ai/dsh-tools@latest\"\n \"pi-coding-agent-extension||examples/pi-coding-agent-extension|\"\n)\n# Validate that a plugin source URL is the official volcengine/OpenViking repo.\nov_plugin_validate_source() {\n local url=\"$1\"\n _ov_plugins validate-source \"$url\" \"$OV_PLUGIN_REPO_HOST\" \"$OV_PLUGIN_REPO_PATH\"\n}\n# Latest upstream commit SHA of the plugin repo (GitHub API first — fast; \"$OV_GIT\" ls-remote fallback).\nov_plugin_upstream_sha() {\n local repo_url=\"${OPENVIKING_PLUGIN_REPO_URL:-$OV_PLUGIN_REPO}\"\n ov_plugin_validate_source \"$repo_url\" || { log_error \"Plugin source not allowed: $repo_url\"; return 1; }\n local sha\n local api_base; api_base=$(ov_plugin_api_base) || return 1\n sha=$(\"$OV_CURL_BIN\" -fsS --retry 2 --connect-timeout 10 --max-time 30 \"$api_base/commits/HEAD\" 2>/dev/null \\\n | _ov_plugins parse-sha 2>/dev/null || true)\n if [[ ${#sha} -ne 40 ]]; then\n sha=$(timeout 20 \"$OV_GIT\" ls-remote \"$repo_url\" HEAD 2>/dev/null | awk '{print $1}') || true\n fi\n [[ ${#sha} -eq 40 ]] || return 1\n echo \"$sha\"\n}\n# Commit SHA recorded in the runtime cache .openviking-sync (empty if absent).\nov_plugin_cache_sha() {\n local sync=\"$1/.openviking-sync\"\n [[ -f \"$sync\" ]] || { echo \"\"; return 0; }\n awk -F': ' '/^# Commit:/{print $2; exit}' \"$sync\" | awk '{print $1}'\n}\n# First reachable npm registry from the domestic-first NPM_REGISTRIES list.\nov_first_npm_registry() {\n local reg\n local regs=()\n if [[ -n \"${OPENVIKING_NPM_REGISTRIES:-}\" ]]; then\n read -r -a regs \u003c\u003c\u003c \"$OPENVIKING_NPM_REGISTRIES\"\n fi\n regs+=(\"${NPM_REGISTRIES[@]}\")\n for reg in \"${regs[@]}\"; do\n [[ -n \"$reg\" ]] || continue\n if \"$OV_CURL_BIN\" -fsS --connect-timeout 8 --max-time 15 \"$reg\" -o /dev/null 2>/dev/null; then\n echo \"$reg\"; return 0\n fi\n done\n echo \"${NPM_REGISTRIES[${#NPM_REGISTRIES[@]}-1]}\"\n}\n# Robust single-file fetch with bounded retries + backoff (network to the mirrors is\n# intermittently slow; each attempt stays under ~33s).\n_ov_fetch_file() {\n local url=\"$1\" out=\"$2\"\n local t_start tries\n t_start=$(date +%s); tries=0\n while true; do\n tries=$((tries + 1))\n if \"$OV_CURL_BIN\" -fsS --connect-timeout 8 --max-time 25 \"$url\" -o \"$out\"; then return 0; fi\n if (( tries >= 8 )); then log_error \"Gave up after 8 attempts: $url\"; return 1; fi\n if (( $(date +%s) - t_start > 200 )); then log_error \"Timed out fetching: $url\"; return 1; fi\n local _delay=$((3 * (1 \u003c\u003c (tries - 1))))\n ((_delay > 30)) && _delay=30\n sleep \"$_delay\"\n done\n}\n# Pick the first reachable raw mirror (health-checked once per run, cached in the parent\n# shell so parallel download subshells inherit one result). Honors\n# OPENVIKING_GH_RAW_MIRRORS when set, else GH_RAW_MIRROR_DEFAULTS; direct\n# raw.githubusercontent.com is the built-in final fallback.\n_OV_GH_MIRROR_CHOSEN=\"\"\nov_pick_gh_mirror() {\n [[ -n \"$_OV_GH_MIRROR_CHOSEN\" ]] && { echo \"$_OV_GH_MIRROR_CHOSEN\"; return 0; }\n local m\n for m in \"${GH_RAW_MIRRORS[@]}\" \"https://raw.githubusercontent.com\"; do\n [[ -n \"$m\" ]] || continue\n if \"$OV_CURL_BIN\" -fsS --connect-timeout 5 --max-time 12 \"$m/$OV_PLUGIN_REPO_PATH/main/README.md\" -o /dev/null 2>/dev/null; then\n _OV_GH_MIRROR_CHOSEN=\"$m\"\n break\n fi\n done\n [[ -n \"$_OV_GH_MIRROR_CHOSEN\" ]] || _OV_GH_MIRROR_CHOSEN=\"https://raw.githubusercontent.com\"\n echo \"$_OV_GH_MIRROR_CHOSEN\"\n return 0\n}\n# Pick the API base URL: OPENVIKING_PLUGIN_API_URL / OPENVIKING_GITHUB_API_MIRROR win,\n# else first reachable of GH_API_MIRROR_DEFAULTS (cached per run), else direct.\n_OV_API_MIRROR_CHOSEN=\"\"\nov_plugin_api_base() {\n if [[ -n \"${OPENVIKING_PLUGIN_API_URL:-}\" ]]; then\n echo \"$OPENVIKING_PLUGIN_API_URL\"; return 0\n fi\n if [[ -n \"${OPENVIKING_GITHUB_API_MIRROR:-}\" ]]; then\n echo \"$OPENVIKING_GITHUB_API_MIRROR\"; return 0\n fi\n [[ -n \"$_OV_API_MIRROR_CHOSEN\" ]] && { echo \"$_OV_API_MIRROR_CHOSEN\"; return 0; }\n local m\n for m in \"${GH_API_MIRROR_DEFAULTS[@]}\"; do\n if \"$OV_CURL_BIN\" -fsS --connect-timeout 5 --max-time 12 \"$m\" -o /dev/null 2>/dev/null; then\n _OV_API_MIRROR_CHOSEN=\"$m\"; echo \"$m\"; return 0\n fi\n done\n # Direct api.github.com as final fallback even if the probe failed.\n _OV_API_MIRROR_CHOSEN=\"${GH_API_MIRROR_DEFAULTS[${#GH_API_MIRROR_DEFAULTS[@]}-1]}\"\n echo \"$_OV_API_MIRROR_CHOSEN\"\n return 0\n}\n# Fetch a repo-relative \u003cpath> at \u003csha>. Uses the health-checked mirror first; if it\n# dies mid-download, falls through the remaining mirrors once each. Returns 0 on\n# success (writes \u003cout>).\n_ov_fetch_mirrored() {\n local sha=\"$1\" path=\"$2\" out=\"$3\"\n local base chosen\n chosen=$(ov_pick_gh_mirror)\n if _ov_fetch_file \"$chosen/$OV_PLUGIN_REPO_PATH/$sha/$path\" \"$out\"; then return 0; fi\n # Chosen mirror failed after _ov_fetch_file's retry loop — uncache it and retry\n # the rest of the chain once each (direct raw last).\n _OV_GH_MIRROR_CHOSEN=\"\"\n for base in \"${GH_RAW_MIRRORS[@]}\" \"https://raw.githubusercontent.com\"; do\n [[ -n \"$base\" ]] || continue\n [[ \"$base\" != \"$chosen\" ]] || continue\n if _ov_fetch_file \"$base/$OV_PLUGIN_REPO_PATH/$sha/$path\" \"$out\"; then return 0; fi\n done\n return 1\n}\n# Fetch the recursive repo tree JSON for \u003csha> (GitHub API, reachable directly).\n# Caches under \u003cstage>/trees/.\n_ov_tree_fetch() {\n local sha=\"$1\" stage=\"$2\"\n local out=\"$stage/trees/${sha}.json\"\n [[ -f \"$out\" ]] && { echo \"$out\"; return 0; }\n mkdir -p \"$stage/trees\"\n local api_base; api_base=$(ov_plugin_api_base) || return 1\n if ! \"$OV_CURL_BIN\" -fsS --retry 3 --connect-timeout 10 --max-time 60 \"$api_base/git/trees/$sha?recursive=1\" -o \"$out\"; then\n return 1\n fi\n echo \"$out\"\n}\n# Diff plugin files between old and new tree JSONs for \u003cexdir>.\n# Emits: \"A \u003cpath>\" added, \"M \u003cpath>\" modified, \"D \u003cpath>\" deleted.\n_ov_diff_blobs() {\n _ov_plugins diff-blobs \"$1\" \"$2\" \"$3\"\n}\n# path\u003ctab>blob-sha map for \u003cexdir> from a GitHub tree JSON.\nov_plugin_blob_map() {\n _ov_plugins blob-map \"$1\" \"$2\"\n}\n# Download repo-relative \u003cpaths> at \u003csha> into \u003cdest> (relative to \u003cexdir>), and\n# VERIFY each file's SHA-1 against the authoritative GitHub tree blob SHA — this is\n# the source-check: content is accepted only if it matches the official tree, no\n# matter which domestic mirror actually served the bytes.\n_ov_download_files() {\n local sha=\"$1\" newtree=\"$2\" exdir=\"$3\" dest=\"$4\"\n shift 4\n local paths=(\"$@\")\n [[ ${#paths[@]} -gt 0 ]] || return 0\n local blobmap; blobmap=$(ov_plugin_blob_map \"$newtree\" \"$exdir\")\n local path rel blobsha\n # Parallel download: launch up to OV_DOWNLOAD_PARALLEL (default 8) background jobs,\n # each downloading + SHA-1 verifying one file. Subshells inherit all functions and\n # variables (GH_RAW_MIRRORS, _ov_fetch_mirrored, etc.) from the parent shell.\n local max_parallel=${OV_DOWNLOAD_PARALLEL:-8}\n local pids=() pid rc=0\n for path in \"${paths[@]}\"; do\n rel=\"${path#*$exdir/}\"\n blobsha=$(printf '%s\\n' \"$blobmap\" | awk -v p=\"$path\" -F'\\t' '$1==p{print $2; exit}')\n [[ -n \"$blobsha\" ]] || { log_error \"No blob sha for $path in upstream tree\"; return 1; }\n mkdir -p \"$dest/$(dirname \"$rel\")\"\n # Download + verify in background subshell\n (\n _ov_fetch_mirrored \"$sha\" \"$path\" \"$dest/$rel\" || exit 1\n _ov_plugins sha1-verify \"$dest/$rel\" \"$blobsha\"\n ) &\n pids+=($!)\n # Throttle: when at capacity, wait for the oldest job to finish\n if (( ${#pids[@]} >= max_parallel )); then\n wait \"${pids[0]}\" || rc=1\n pids=(\"${pids[@]:1}\")\n fi\n done\n # Wait for all remaining background jobs\n for pid in \"${pids[@]}\"; do\n wait \"$pid\" || rc=1\n done\n (( rc == 0 )) || { log_error \"One or more parallel file downloads failed\"; return 1; }\n return 0\n}\n# Write .openviking-sync traceability metadata for \u003cdest> at \u003csha>/\u003cexdir>.\n_ov_write_sync_meta() {\n local dest=\"$1\" sha=\"$2\" exdir=\"$3\"\n cat > \"$dest/.openviking-sync\" \u003c\u003cSYNC\n# OpenViking agent plugin (installed on demand by huawei-cloud-openviking-agent-integration skill)\n# Source: ${OV_PLUGIN_REPO_HOST}/${OV_PLUGIN_REPO_PATH}\n# Commit: ${sha}\n# Path: ${exdir}\n# Synced: $(date -u +%Y-%m-%dT%H:%M:%SZ)\nSYNC\n}\n# Ensure npm peer deps exist in \u003cdest>/node_modules (domestic-first registry list).\n_ov_ensure_peers() {\n local name=\"$1\" peers=\"$2\" dest=\"$3\"\n [[ -n \"$peers\" ]] || return 0\n local need=false pk pn\n for pk in $peers; do\n # Resolve module path under node_modules: scoped (@scope/name) keeps the @ prefix.\n if [[ \"$pk\" == @* ]]; then\n pn=\"${pk#@}\"; pn=\"${pn%%@*}\"; pn=\"@$pn\"\n else\n pn=\"${pk%%@*}\"\n fi\n [[ -d \"$dest/node_modules/$pn\" ]] || need=true\n done\n [[ \"$need\" == \"false\" ]] && return 0\n local reg; reg=$(ov_first_npm_registry)\n log_info \"Fetching $name peer deps from $reg: $peers\"\n mkdir -p /tmp/openviking\n local pkgdir; pkgdir=$(mktemp -d /tmp/openviking/ov-npm.XXXXXX) || { log_error \"mktemp failed for $name peer deps staging\"; return 1; }\n printf '{\"name\":\"ov-peer-stage\",\"private\":true}\\n' > \"$pkgdir/package.json\"\n if ! ( cd \"$pkgdir\" && timeout 180 \"$OV_NPM\" install --legacy-peer-deps --registry=\"$reg\" --no-audit --no-fund --no-save $peers >/dev/null 2>&1 ); then\n ov_safe_rm \"$pkgdir\" 2>/dev/null || true\n log_error \"\"$OV_NPM\" install failed for $name peer deps: $peers\"\n return 1\n fi\n mkdir -p \"$dest/node_modules\"\n cp -a \"$pkgdir/node_modules/.\" \"$dest/node_modules/\"\n ov_safe_rm \"$pkgdir\" 2>/dev/null || true\n return 0\n}\n# Provision a plugin on demand into \u003cdest> for the agent that needs it. Diff-based:\n# checks upstream, downloads ONLY the actual file changes (verified against the\n# official GitHub tree), all served via the domestic-first raw mirror list. If the\n# cache already records the upstream commit, nothing is re-downloaded. If upstream\n# is unreachable, the existing \u003cdest> copy is reused (warn) — no network, no skill\n# vendoring involved. Globals used: DRY_RUN.\nov_plugin_provision() {\n local name=\"$1\" dest=\"$2\"\n local exdir=\"\" peers=\"\"\n local p rest\n for p in \"${OV_PLUGINS[@]}\"; do\n if [[ \"${p%%|*}\" == \"$name\" ]]; then\n rest=\"${p#*|}\"\n rest=\"${rest#*|}\" # drop npm package field\n exdir=\"${rest%%|*}\"; peers=\"${rest#*|}\"\n [[ \"$peers\" == \"$rest\" ]] && peers=\"\"\n break\n fi\n done\n [[ -n \"$exdir\" ]] || { log_error \"Unknown plugin: $name\"; return 1; }\n # P9: a stale non-directory at $dest (e.g. an empty placeholder file left by an\n # earlier run) makes mkdir -p \"$dest/...\" fail with \"Not a directory\" and trigger an\n # error flood. Remove it cleanly before provisioning writes into the destination.\n if [[ -e \"$dest\" && ! -d \"$dest\" ]]; then\n if [[ \"${DRY_RUN:-false}\" == \"true\" ]]; then\n log_warn \"[DRY-RUN] would remove stale non-directory at plugin destination: $dest\"\n else\n log_warn \"Removing stale non-directory at plugin destination: $dest\"\n rm -f -- \"$dest\" || { log_error \"Cannot remove stale path: $dest\"; return 1; }\n fi\n fi\n local upstream=\"\"\n if ! upstream=$(ov_plugin_upstream_sha); then\n log_warn \"Plugin source unreachable — reusing existing install at $dest (if present)\"\n [[ -d \"$dest\" ]] || { log_error \"No existing plugin install at $dest\"; return 1; }\n return 0\n fi\n # Warm mirror pickers in the parent shell so parallel download subshells inherit a\n # single health-check result instead of each probing the chain independently.\n ov_pick_gh_mirror >/dev/null 2>&1 || true\n ov_plugin_api_base >/dev/null 2>&1 || true\n local cached; cached=$(ov_plugin_cache_sha \"$dest\")\n if [[ -n \"$cached\" && \"$cached\" == \"$upstream\" ]]; then\n log_ok \"Plugin $name already installed at upstream commit (${upstream:0:7})\"\n return 0\n fi\n mkdir -p /tmp/openviking\n local stage; stage=$(mktemp -d /tmp/openviking/ov-plugin.XXXXXX) || { log_error \"mktemp failed for $name plugin staging\"; return 1; }\n local newtree\n if ! newtree=$(_ov_tree_fetch \"$upstream\" \"$stage\"); then\n log_warn \"Failed to fetch upstream tree — reusing existing install at $dest (if present)\"\n ov_safe_rm \"$stage\" 2>/dev/null || true\n [[ -d \"$dest\" ]] || { log_error \"No existing plugin install at $dest\"; return 1; }\n return 0\n fi\n local entries=\"\"\n if [[ -n \"$cached\" ]]; then\n local oldtree=\"\"\n oldtree=$(_ov_tree_fetch \"$cached\" \"$stage\") || true\n if [[ -n \"$oldtree\" && -f \"$oldtree\" ]]; then\n entries=$(_ov_diff_blobs \"$oldtree\" \"$newtree\" \"$exdir\")\n fi\n fi\n # No diff baseline: emit add entries from the current tree. Strip the blob sha so\n# path matches the \"A \u003cpath>\" format used by _ov_diff_blobs.\n [[ -n \"$entries\" ]] || entries=\"$(ov_plugin_blob_map \"$newtree\" \"$exdir\" | awk -F'\\t' '{print \"A \" $1}')\"\n\n local dl=() del=() op path\n while IFS= read -r line; do\n [[ -z \"$line\" ]] && continue\n read -r op path \u003c\u003c\u003c \"$line\"\n if [[ \"$op\" == \"D\" ]]; then del+=(\"$path\"); else dl+=(\"$path\"); fi\n done \u003c\u003c\u003c \"$entries\"\n if [[ ${#dl[@]} -eq 0 && ${#del[@]} -eq 0 && -n \"$cached\" ]]; then\n log_ok \"Plugin $name: no file changes upstream (${upstream:0:7}) — recording commit\"\n if [[ \"${DRY_RUN:-false}\" != \"true\" ]]; then\n _ov_write_sync_meta \"$dest\" \"$upstream\" \"$exdir\"\n fi\n else\n log_info \"Plugin $name: ${#dl[@]} file(s) to install, ${#del[@]} to delete (upstream ${upstream:0:7})\"\n if [[ \"${DRY_RUN:-false}\" == \"true\" ]]; then\n log_warn \"[DRY-RUN] would update plugin source at $dest\"\n ov_safe_rm \"$stage\" 2>/dev/null || true\n return 0\n fi\n # Dest dir is created lazily by _ov_download_files / _ov_write_sync_meta.\n _ov_download_files \"$upstream\" \"$newtree\" \"$exdir\" \"$dest\" \"${dl[@]}\" || { ov_safe_rm \"$stage\" 2>/dev/null || true; return 1; }\n local dd\n for dd in \"${del[@]}\"; do rm -f \"$dest/${dd#*$exdir/}\"; done\n _ov_ensure_peers \"$name\" \"$peers\" \"$dest\" || { ov_safe_rm \"$stage\" 2>/dev/null || true; return 1; }\n _ov_write_sync_meta \"$dest\" \"$upstream\" \"$exdir\"\n log_ok \"Plugin $name installed on demand (${#dl[@]} downloaded, ${#del[@]} deleted) -> $dest\"\n fi\n ov_safe_rm \"$stage\" 2>/dev/null || true\n return 0\n}\n\n# P2-12: Post-install npm audit check — warns on high/critical vulnerabilities\nov_npm_audit_check() {\n local dir=\"$1\"\n [[ -z \"$dir\" || ! -d \"$dir\" ]] && return 0\n if [[ \"${OV_NPM_AUDIT:-false}\" != \"true\" ]]; then return 0; fi\n local audit_result\n audit_result=$(cd \"$dir\" && \"$OV_NPM\" audit --json 2>/dev/null) || return 0\n local high_crit\n high_crit=$(echo \"$audit_result\" | \"$OV_PY\" -c \"\nimport json,sys\ntry:\n d=json.load(sys.stdin)\n m=d.get('metadata',{}).get('vulnerabilities',{})\n print(m.get('high',0)+m.get('critical',0))\nexcept: print(0)\n\" 2>/dev/null || echo 0)\n if [[ \"$high_crit\" -gt 0 ]]; then\n log_warn \"npm audit: $high_crit high/critical vulnerabilities found in $dir\"\n fi\n}\n\n# P2-21: Runtime integrity verification — compare installed plugin SHA against upstream\nov_verify_runtime_integrity() {\n local plugin_dir=\"$1\" plugin_name=\"${2:-plugin}\"\n [[ -z \"$plugin_dir\" || ! -d \"$plugin_dir\" ]] && return 0\n local meta_file=\"${plugin_dir}/.ov-sync-meta\"\n [[ ! -f \"$meta_file\" ]] && return 0\n local recorded_sha; recorded_sha=$(head -1 \"$meta_file\" 2>/dev/null)\n [[ -z \"$recorded_sha\" ]] && return 0\n local upstream_sha; upstream_sha=$(ov_plugin_upstream_sha 2>/dev/null)\n [[ -z \"$upstream_sha\" ]] && return 0\n if [[ \"$recorded_sha\" != \"$upstream_sha\" ]]; then\n log_warn \"$plugin_name: runtime SHA mismatch (recorded=${recorded_sha:0:7}, upstream=${upstream_sha:0:7}) — may need re-sync\"\n return 1\n fi\n log_ok \"$plugin_name: runtime integrity verified (${recorded_sha:0:7})\"\n return 0\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":18737,"content_sha256":"8a08b9a69a8ade85acbfc0bb00aa663614b3ace9e83c09d94052247dfd437fea"},{"filename":"scripts/lib/registry.sh","content":"#!/bin/bash\n# lib/registry.sh — Agent registry (auto-discover + dispatch)\ndeclare -ga REGISTRY_AGENTS=()\ndeclare -gA REGISTRY_LOADED=()\nregistry_init() {\n REGISTRY_AGENTS=()\n REGISTRY_LOADED=()\n}\n# Maps hyphenated names to internal underscored names (deepseek-harness → deepseek_harness)\nregistry_normalize() {\n local name=\"$1\"\n local existing\n for existing in \"${REGISTRY_AGENTS[@]}\"; do\n [[ \"$existing\" == \"$name\" ]] && { echo \"$name\"; return 0; }\n done\n local normalized=\"${name//-/_}\"\n for existing in \"${REGISTRY_AGENTS[@]}\"; do\n [[ \"$existing\" == \"$normalized\" ]] && { echo \"$normalized\"; return 0; }\n done\n echo \"$name\"\n return 1\n}\n# Scans agents/ directory, sources each .sh, calls agent_\u003cname>_register()\nregistry_discover() {\n local agents_dir=\"${1:-$(dirname \"${BASH_SOURCE[0]}\")/../agents}\"\n local f name\n for f in \"$agents_dir\"/*.sh; do\n [[ -f \"$f\" ]] || continue\n name=$(basename \"$f\" .sh)\n [[ -n \"${REGISTRY_LOADED[$name]:-}\" ]] && continue\n # shellcheck disable=SC1090\n source \"$f\"\n REGISTRY_LOADED[\"$name\"]=1\n local reg_fn=\"agent_${name}_register\"\n if declare -f \"$reg_fn\" &>/dev/null; then\n agent::clear_meta\n \"$reg_fn\"\n # P2-4: Contract validation — warn if required methods are missing\n local _method\n for _method in integrate unbind status; do\n if ! declare -f \"agent_${name}_${_method}\" &>/dev/null; then\n log_warn \"Agent '$name' missing required method: ${_method}() — will use default\"\n fi\n done\n fi\n done\n}\nregistry_add() {\n local name=\"$1\"\n local existing\n for existing in \"${REGISTRY_AGENTS[@]}\"; do\n [[ \"$existing\" == \"$name\" ]] && return 0\n done\n REGISTRY_AGENTS+=(\"$name\")\n}\nregistry_list() { printf '%s\\n' \"${REGISTRY_AGENTS[@]}\"; }\nregistry_validate() {\n local target=\"$1\"\n target=$(registry_normalize \"$target\")\n local existing\n for existing in \"${REGISTRY_AGENTS[@]}\"; do\n [[ \"$existing\" == \"$target\" ]] && return 0\n done\n return 1\n}\n# registry_dispatch \u003cagent> \u003cmethod>: calls agent_\u003cname>_\u003cmethod>() or agent::default_\u003cmethod>()\nregistry_dispatch() {\n local name=\"$1\" method=\"$2\"\n name=$(registry_normalize \"$name\") || true\n if ! registry_validate \"$name\"; then\n log_error \"Unknown agent: $1\"\n return 1\n fi\n agent::clear_meta\n local reg_fn=\"agent_${name}_register\"\n if declare -f \"$reg_fn\" &>/dev/null; then \"$reg_fn\"; fi\n local method_fn=\"agent_${name}_${method}\"\n if declare -f \"$method_fn\" &>/dev/null; then \"$method_fn\"; else \"agent::default_${method}\"; fi\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2540,"content_sha256":"c8a8ab234186dc0d9c0917f34a7b3589b267deec1b6bd05d56a123c43b5a3bd2"},{"filename":"scripts/lib/ui.sh","content":"#!/bin/bash\n# lib/ui.sh — Logging, authorization, dry-run, i18n\nRED='\\033[0;31m'; GREEN='\\033[0;32m'; YELLOW='\\033[1;33m'; BLUE='\\033[0;34m'; NC='\\033[0m'\nlog_info() { echo -e \"${BLUE}[INFO]${NC} $*\"; }\nlog_ok() { echo -e \"${GREEN}[OK]${NC} $*\"; }\nlog_warn() { echo -e \"${YELLOW}[WARN]${NC} $*\"; }\nlog_error() { echo -e \"${RED}[ERROR]${NC} $*\" >&2; }\n# Returns \"zh\" for Chinese, \"en\" for English.\n# Detection order: OV_LANG env var > LANG/LC_ALL/LANGUAGE env vars > default \"zh\"\nov_detect_lang() {\n if [[ -n \"${OV_LANG:-}\" ]]; then\n echo \"${OV_LANG}\"\n return\n fi\n for var in LANG LC_ALL LANGUAGE; do\n local val=\"${!var:-}\"\n if [[ \"$val\" == zh* ]]; then\n echo \"zh\"\n return\n fi\n done\n echo \"zh\"\n}\n# i18n-aware logging: ov_log_info \"中文\" \"English\" (picks by detected language)\n# (ov_log_ok / ov_log_warn had no callers and were removed)\nov_log_info() { local zh=\"$1\" en=\"${2:-$1}\"; if [[ \"$(ov_detect_lang)\" == \"zh\" ]]; then log_info \"$zh\"; else log_info \"$en\"; fi; }\n# Globals used: AUTO_YES, DRY_RUN\nrequire_confirmation() {\n local action=\"$1\" agent=\"$2\" details=\"$3\"\n if [[ \"${AUTO_YES:-false}\" == \"true\" ]]; then return 0; fi\n if [[ \"${DRY_RUN:-false}\" == \"true\" ]]; then return 0; fi\n local color=\"${4:-$YELLOW}\" # optional: use RED for unbind\n echo \"\"\n echo -e \"${color}━━━ Authorization Required ━━━${NC}\"\n echo \" Action: $action\"\n echo \" Agent: $agent\"\n echo \" Details: $details\"\n echo -e \"${color}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n echo \"\"\n read -p \"Type 'confirm' to proceed: \" response\n if [[ \"$response\" != \"confirm\" ]]; then\n log_error \"Authorization denied. Aborting.\"\n return 1\n fi\n return 0\n}\n# Globals used: DRY_RUN\ndry_run_msg() {\n if [[ \"${DRY_RUN:-false}\" == \"true\" ]]; then\n log_warn \"[DRY-RUN] $*\"\n return 0\n fi\n return 1\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1901,"content_sha256":"4cf4cd4907c58713710b7580b86b57c86606818d508d90a9071402e82c26ea90"},{"filename":"scripts/py/agents/ov_codearts.py","content":"#!/usr/bin/env python3\n\"\"\"ov_codearts.py — CodeArts CLI agent Python operations CLI.\n\nReplaces all heredoc Python and inline ``python3 -c`` calls in\nagents/codearts.sh.\n\nSubcommands:\n register-plugin \u003ccodearts_cli.json> add @openviking/opencode-plugin to plugin list, clean old MCP+prompt\n inject-template \u003ctpl> \u003csdk_ver> \u003cplugin_ver> \u003cnpm_registry> \u003cplugin_cache_dir> \u003cshared_dir>\n write init script + inject source block into template\n has-plugin \u003ccodearts_cli.json> exit 0 if plugin in list, 1 otherwise\n has-prompt \u003ccodearts_cli.json> exit 0 if 'OpenViking' in agent.build.prompt, 1 otherwise\n unbind-template \u003ctpl> remove OpenViking integration block from template\n remove-plugin \u003ccodearts_cli.json> remove plugin + old MCP+prompt from codearts_cli.json\n\"\"\"\nimport json\nimport os\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\nfrom ov_common import (\n read_text,\n write_text,\n write_init,\n source_block,\n load_json,\n)\n\nOV_PLUGIN = \"@openviking/opencode-plugin\"\n\n\n# ── register-plugin (PYREG heredoc) ──────────────────────────\ndef cmd_register_plugin(args):\n \"\"\"Add @openviking/opencode-plugin to plugin list; clean old MCP+prompt.\"\"\"\n path = args[0]\n cfg = load_json(path)\n changed = False\n plugins = cfg.setdefault(\"plugin\", [])\n if OV_PLUGIN not in plugins:\n plugins.append(OV_PLUGIN)\n changed = True\n if \"mcp\" in cfg and \"openviking\" in cfg[\"mcp\"]:\n del cfg[\"mcp\"][\"openviking\"]\n if not cfg[\"mcp\"]:\n del cfg[\"mcp\"]\n changed = True\n if \"agent\" in cfg and \"build\" in cfg[\"agent\"] and \"prompt\" in cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"][\"prompt\"]\n if not cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"]\n if not cfg[\"agent\"]:\n del cfg[\"agent\"]\n changed = True\n if changed:\n with open(path, \"w\", encoding=\"utf-8\") as f:\n json.dump(cfg, f, indent=2, ensure_ascii=False)\n print(\"Plugin registered (old MCP+prompt cleaned)\")\n\n\n# ── inject-template (PYTPL heredoc) ──────────────────────────\ndef cmd_inject_template(args):\n \"\"\"Generate cache-first plugin deployment init script and inject into template.\"\"\"\n tpl_path = args[0]\n sdk_ver = args[1]\n plugin_ver = args[2]\n npm_registry = args[3]\n plugin_cache_dir = args[4]\n shared_dir = args[5]\n\n tpl = read_text(tpl_path)\n\n block = \"\"\"\nexport NPM_CONFIG_REGISTRY=__OV_NPM_REGISTRY__\nexport npm_config_prefer_offline=true\nexport npm_config_package_lock=true\nOV_NPM_DIR=\"$HOME/.codeartsdoer\"\nmkdir -p \"$OV_NPM_DIR/node_modules/@openviking\"\nOV_PLUGIN_DST=\"$OV_NPM_DIR/node_modules/@openviking/opencode-plugin\"\nOV_PLUGIN_CACHE=\"$HOME/.cache/codeartsdoer/packages/@openviking/opencode-plugin@latest/node_modules/@openviking/opencode-plugin\"\nOV_PLUGIN_SHARED=\"__OV_PLUGIN_CACHE_DIR__/@openviking/opencode-plugin\"\nOV_PLUGIN_SOURCE=\"none\"\nif [[ -d \"$OV_PLUGIN_DST\" && -f \"$OV_PLUGIN_DST/package.json\" ]]; then\n OV_PLUGIN_SOURCE=\"existing\"\nfi\nif [[ \"$OV_PLUGIN_SOURCE\" == \"none\" && -d \"$OV_PLUGIN_SHARED\" && -f \"$OV_PLUGIN_SHARED/package.json\" ]]; then\n _cache_age=$(( $(date +%s) - $(stat -c %Y \"$OV_PLUGIN_SHARED\" 2>/dev/null || echo 0) ))\n if [[ $_cache_age -le 86400 ]]; then\n rm -rf \"$OV_PLUGIN_DST\"\n cp -a \"$OV_PLUGIN_SHARED\" \"$OV_PLUGIN_DST\"\n OV_PLUGIN_SOURCE=\"shared-cache\"\n if [[ ! -d \"$OV_NPM_DIR/node_modules/@opencode-ai\" && -d \"$OV_PLUGIN_SHARED/../../@opencode-ai\" ]]; then\n cp -a \"$OV_PLUGIN_SHARED/../../@opencode-ai\" \"$OV_NPM_DIR/node_modules/@opencode-ai\"\n fi\n fi\nfi\nif [[ \"$OV_PLUGIN_SOURCE\" == \"none\" && -d \"$OV_PLUGIN_CACHE\" ]]; then\n rm -rf \"$OV_PLUGIN_DST\"\n cp -a \"$OV_PLUGIN_CACHE\" \"$OV_PLUGIN_DST\"\n OV_PLUGIN_SOURCE=\"cache\"\nfi\nif [[ \"$OV_PLUGIN_SOURCE\" == \"none\" ]]; then\n if command -v npm &>/dev/null; then\n cat > \"$OV_NPM_DIR/package.json\" \u003c\u003c'PKGEOF'\n{\"dependencies\":{\"@opencode-ai/plugin\":\"__OV_PLUGIN_SDK_VER__\",\"@openviking/opencode-plugin\":\"__OV_OPENCODE_PLUGIN_VER__\"}}\nPKGEOF\n if (cd \"$OV_NPM_DIR\" && npm install --registry=__OV_NPM_REGISTRY__ --no-audit --no-fund 2>&1 | tail -5) && \\\\\n [[ -d \"$OV_PLUGIN_DST\" ]]; then\n OV_PLUGIN_SOURCE=\"npm\"\n mkdir -p \"$(dirname \"$OV_PLUGIN_SHARED\")\"\n rm -rf \"$OV_PLUGIN_SHARED\"\n cp -a \"$OV_PLUGIN_DST\" \"$OV_PLUGIN_SHARED\"\n touch \"$OV_PLUGIN_SHARED\"\n if [[ -d \"$OV_NPM_DIR/node_modules/@opencode-ai\" ]]; then\n rm -rf \"$OV_PLUGIN_SHARED/../../@opencode-ai\"\n cp -a \"$OV_NPM_DIR/node_modules/@opencode-ai\" \"$OV_PLUGIN_SHARED/../../@opencode-ai\"\n fi\n else\n echo \"WARN: npm install failed — plugin will not load\"\n fi\n else\n echo \"WARN: npm not found and no cache available — plugin will not load\"\n fi\nfi\nif [[ ! -d \"$OV_NPM_DIR/node_modules/@opencode-ai\" ]]; then\n if [[ -d \"$OV_PLUGIN_SHARED/../../@opencode-ai\" ]]; then\n cp -a \"$OV_PLUGIN_SHARED/../../@opencode-ai\" \"$OV_NPM_DIR/node_modules/@opencode-ai\"\n elif command -v npm &>/dev/null; then\n cat > \"$OV_NPM_DIR/package.json\" \u003c\u003c'PKGEOF2'\n{\"dependencies\":{\"@opencode-ai/plugin\":\"__OV_PLUGIN_SDK_VER__\"}}\nPKGEOF2\n (cd \"$OV_NPM_DIR\" && npm install --registry=__OV_NPM_REGISTRY__ --no-audit --no-fund 2>&1 | tail -5) || \\\\\n echo \"WARN: Plugin SDK install failed\"\n fi\nfi\n_ov_py=\"\"\nfor _py in python3.12 python3.11 python3.10 python3; do command -v \"$_py\" >/dev/null 2>&1 && _ov_py=\"$_py\" && break; done\n: \"${_ov_py:=python3}\"\nOV_PLUGIN_VER=\"__OV_OPENCODE_PLUGIN_VER__\"\nOV_SDK_VER=\"__OV_PLUGIN_SDK_VER__\"\nif [[ -f \"$OV_NPM_DIR/node_modules/@opencode-ai/plugin/package.json\" ]]; then\n OV_SDK_VER=$($_ov_py -c \"import json; print(json.load(open('$OV_NPM_DIR/node_modules/@opencode-ai/plugin/package.json')).get('version','__OV_PLUGIN_SDK_VER__'))\" 2>/dev/null || echo \"__OV_PLUGIN_SDK_VER__\")\nfi\nif [[ -f \"$OV_NPM_DIR/node_modules/@openviking/opencode-plugin/package.json\" ]]; then\n OV_PLUGIN_VER=$($_ov_py -c \"import json; print(json.load(open('$OV_NPM_DIR/node_modules/@openviking/opencode-plugin/package.json')).get('version','__OV_OPENCODE_PLUGIN_VER__'))\" 2>/dev/null || echo \"__OV_OPENCODE_PLUGIN_VER__\")\nfi\ncat > \"$OV_NPM_DIR/package.json\" \u003c\u003cPKGLCK\n{\"dependencies\":{\"@opencode-ai/plugin\":\"$OV_SDK_VER\",\"@openviking/opencode-plugin\":\"$OV_PLUGIN_VER\"}}\nPKGLCK\ncat > \"$OV_NPM_DIR/package-lock.json\" \u003c\u003cLOCKEOF\n{\"name\":\"\",\"version\":\"1.0.0\",\"lockfileVersion\":3,\"requires\":true,\"packages\":{\"\":{\"dependencies\":{\"@opencode-ai/plugin\":\"$OV_SDK_VER\",\"@openviking/opencode-plugin\":\"$OV_PLUGIN_VER\"}},\"node_modules/@opencode-ai/plugin\":{\"version\":\"$OV_SDK_VER\",\"resolved\":\"\",\"integrity\":\"\"},\"node_modules/@openviking/opencode-plugin\":{\"version\":\"$OV_PLUGIN_VER\",\"resolved\":\"\",\"integrity\":\"\"}}}\nLOCKEOF\nCODEARTS_CFG=\"$HOME/.codeartsdoer/codearts_cli.json\"\nif [[ -f \"$CODEARTS_CFG\" ]]; then\n $_ov_py - \"$CODEARTS_CFG\" \u003c\u003c'OVREG' || true\nimport json, sys\npath = sys.argv[1]\nwith open(path) as f: cfg = json.load(f)\nchanged = False\nplugins = cfg.setdefault(\"plugin\", [])\nif \"@openviking/opencode-plugin\" not in plugins: plugins.append(\"@openviking/opencode-plugin\"); changed = True\nif \"mcp\" in cfg and \"openviking\" in cfg[\"mcp\"]:\n del cfg[\"mcp\"][\"openviking\"]\n if not cfg[\"mcp\"]: del cfg[\"mcp\"]\n changed = True\nif \"agent\" in cfg and \"build\" in cfg[\"agent\"] and \"prompt\" in cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"][\"prompt\"]\n if not cfg[\"agent\"][\"build\"]: del cfg[\"agent\"][\"build\"]\n if not cfg[\"agent\"]: del cfg[\"agent\"]\n changed = True\nif changed:\n with open(path, \"w\") as f: json.dump(cfg, f, indent=2, ensure_ascii=False)\n print(\"Plugin registered in codearts_cli.json\")\nOVREG\nfi\nOV_CONF_DIR=\"$HOME/.codeartsdoer\"\nmkdir -p \"$OV_CONF_DIR\"\nif [[ ! -f \"$OV_CONF_DIR/openviking-config.json\" ]]; then\n cat > \"$OV_CONF_DIR/openviking-config.json\" \u003c\u003c'OVCONF'\n{\"enabled\":true,\"timeoutMs\":30000,\"repoContext\":{\"enabled\":true,\"cacheTtlMs\":60000},\"autoRecall\":{\"enabled\":true,\"limit\":10,\"scoreThreshold\":0.35,\"maxContentChars\":500,\"preferAbstract\":true,\"tokenBudget\":2000,\"minQueryLength\":3},\"recallLimit\":15,\"recallMaxContentChars\":20000,\"commitTokenThreshold\":20000,\"commitKeepRecentCount\":10,\"profileTokenBudget\":10000,\"resumeContextBudget\":32000}\nOVCONF\nfi\n\"\"\"\n block = block.replace(\"__OV_NPM_REGISTRY__\", npm_registry)\n block = block.replace(\"__OV_PLUGIN_SDK_VER__\", sdk_ver)\n block = block.replace(\"__OV_OPENCODE_PLUGIN_VER__\", plugin_ver)\n block = block.replace(\"__OV_PLUGIN_CACHE_DIR__\", plugin_cache_dir)\n\n init_path = f\"{shared_dir}/ov-codearts-init.sh\"\n write_init(init_path, \"CodeArts\", \"Sourced by the CodeArts template start.sh.\", block.lstrip(\"\\n\"))\n\n sb = source_block(shared_dir, \"ov-codearts-init.sh\")\n\n marker = 'echo \"Starting CodeArts...\"'\n idx = tpl.find(marker)\n if idx == -1:\n marker = \"sleep infinity\"\n idx = tpl.find(marker)\n if idx == -1:\n print(\"ERROR: insertion marker not found\", file=sys.stderr)\n sys.exit(1)\n tpl = tpl[:idx] + sb + tpl[idx:]\n write_text(tpl_path, tpl)\n\n\n# ── has-plugin (inline python3 -c) ───────────────────────────\ndef cmd_has_plugin(args):\n \"\"\"Exit 0 if @openviking/opencode-plugin is in the plugin list, 1 otherwise.\"\"\"\n path = args[0]\n cfg = load_json(path)\n sys.exit(0 if OV_PLUGIN in cfg.get(\"plugin\", []) else 1)\n\n\n# ── has-prompt (inline python3 -c) ───────────────────────────\ndef cmd_has_prompt(args):\n \"\"\"Exit 0 if 'OpenViking' appears in agent.build.prompt, 1 otherwise.\"\"\"\n path = args[0]\n cfg = load_json(path)\n prompt = cfg.get(\"agent\", {}).get(\"build\", {}).get(\"prompt\", \"\")\n sys.exit(0 if \"OpenViking\" in prompt else 1)\n\n\n# ── unbind-template (PYUNBIND heredoc) ───────────────────────\ndef cmd_unbind_template(args):\n \"\"\"Remove OpenViking integration block from template.\"\"\"\n path = args[0]\n content = read_text(path)\n marker = \"# ── OpenViking integration\"\n idx = content.find(marker)\n if idx != -1:\n end_marker = \"# ── End OpenViking integration ──\"\n end_idx = content.find(end_marker, idx)\n if end_idx != -1:\n end_pos = end_idx + len(end_marker)\n while end_pos \u003c len(content) and content[end_pos] == '\\n':\n end_pos += 1\n write_text(path, content[:idx] + content[end_pos:])\n print(\"Removed OpenViking integration block from template\")\n else:\n print(\"WARNING: end marker not found, skipping template removal\")\n else:\n print(\"No OpenViking integration block found in template\")\n\n\n# ── remove-plugin (PYUNBIND2 heredoc) ────────────────────────\ndef cmd_remove_plugin(args):\n \"\"\"Remove @openviking/opencode-plugin + old MCP+prompt from codearts_cli.json.\"\"\"\n path = args[0]\n cfg = load_json(path)\n changed = False\n if \"plugin\" in cfg and OV_PLUGIN in cfg[\"plugin\"]:\n cfg[\"plugin\"].remove(OV_PLUGIN)\n if not cfg[\"plugin\"]:\n del cfg[\"plugin\"]\n changed = True\n if \"mcp\" in cfg and \"openviking\" in cfg[\"mcp\"]:\n del cfg[\"mcp\"][\"openviking\"]\n if not cfg[\"mcp\"]:\n del cfg[\"mcp\"]\n changed = True\n if \"agent\" in cfg and \"build\" in cfg[\"agent\"] and \"prompt\" in cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"][\"prompt\"]\n if not cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"]\n if not cfg[\"agent\"]:\n del cfg[\"agent\"]\n changed = True\n if changed:\n with open(path, \"w\", encoding=\"utf-8\") as f:\n json.dump(cfg, f, indent=2, ensure_ascii=False)\n print(\"Removed OpenViking from codearts_cli.json\")\n\n\ndef main():\n if len(sys.argv) \u003c 2:\n print(\"Usage: ov_codearts.py \u003csubcommand> [args...]\", file=sys.stderr)\n sys.exit(2)\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"register-plugin\": cmd_register_plugin,\n \"inject-template\": cmd_inject_template,\n \"has-plugin\": cmd_has_plugin,\n \"has-prompt\": cmd_has_prompt,\n \"unbind-template\": cmd_unbind_template,\n \"remove-plugin\": cmd_remove_plugin,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12752,"content_sha256":"9ccf358429da18c13439dbd1485b878fd5f0b9910f39a65eec3f31b7fc209fad"},{"filename":"scripts/py/agents/ov_deepseek_harness.py","content":"#!/usr/bin/env python3\n\"\"\"ov_deepseek_harness.py — DeepSeek Harness agent Python operations CLI.\n\nReplaces all heredoc Python and inline ``python3 -c`` calls in\nagents/deepseek_harness.sh.\n\nSubcommands:\n ov_body \u003chome> \u003csrc> \u003curl>\n Install @openviking/dsh-memory-plugin bundle into live dsh profiles\n (web, dsh-tui). Equivalent to the dsh_ov_body() heredoc piped to python3.\n\n tpl_block \u003curl>\n Emit the shell template block (for ov-deepseek-harness-init.sh) with\n embedded Python runtime code. Equivalent to dsh_tpl_block() in the shell.\n\n seed_profiles \u003chome> \u003csrc>\n Pre-seed runtime .dsh/profiles (deploy-resilient layer).\n Equivalent to the DSHSEED heredoc.\n\n inject_template \u003ctpl_path> \u003cinit_path> \u003ctemplate_dir> \u003cshared_dir>\n Read a template block from stdin, write it to the standalone init script,\n and inject a source line into the template start.sh.\n Equivalent to the DSHINJ heredoc.\n\n unbind_template \u003cpath>\n Remove the OpenViking integration block from a template start.sh.\n Equivalent to the DSHUNBINJ heredoc.\n\n remove_bundle \u003cpackage_json_path>\n Remove @openviking/dsh-memory-plugin from dependencies and dsh.profile.bundles\n in a package.json. Equivalent to the DSHPJSON / DSHRTJSON heredocs.\n\"\"\"\nimport os\nimport shutil\nimport sys\n\n_HERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, _HERE)\nsys.path.insert(0, os.path.dirname(_HERE)) # parent dir holds ov_common.py\nfrom ov_common import (\n read_text,\n write_text,\n write_init,\n source_block,\n load_json,\n save_json,\n)\n\nBUNDLE = \"@openviking/dsh-memory-plugin\"\n\n# ── Runtime Python code embedded in the init script ──────────────────\n# This is the exact code from the dsh_ov_body() DSHOVBODY heredoc.\n# It runs at sandbox startup via:\n# \"$_ov_py\" - \"$DSH_HOME\" \"$DSH_RUNTIME/.../dsh-memory-plugin\" \"$OPENVIKING_URL\" \u003c\u003c'OVDSPY'\n# The code uses sys.argv[1:] directly (not subcommand dispatch) because it is\n# fed to the interpreter via stdin heredoc, not invoked as a CLI script.\nOV_BODY_CODE = \"\"\"\\\nimport json\nimport os\nimport shutil\nimport sys\nhome = sys.argv[1]\nsrc = sys.argv[2]\nurl = sys.argv[3]\nBUNDLE = '@openviking/dsh-memory-plugin'\ndef install(prof):\n pdir = os.path.join(home, 'profiles', prof)\n manifest = os.path.join(pdir, 'package.json')\n if not os.path.isfile(manifest):\n sys.stderr.write('skip %s profile: package.json not found\\\\n' % prof)\n return\n target = os.path.join(pdir, 'node_modules', '@openviking', 'dsh-memory-plugin')\n if os.path.islink(target) and not os.path.isdir(target):\n os.remove(target)\n if os.path.islink(target):\n os.remove(target)\n parent = os.path.dirname(target)\n os.makedirs(parent, exist_ok=True)\n if os.path.isdir(src):\n # Re-copy if target missing OR src commit differs (src/.openviking-sync newer):\n # keeps live profiles from going stale when the runtime cache is updated.\n need_copy = not os.path.isdir(target)\n if not need_copy:\n src_meta = os.path.join(src, '.openviking-sync')\n dst_meta = os.path.join(target, '.openviking-sync')\n if os.path.isfile(src_meta):\n src_t = os.path.getmtime(src_meta)\n dst_t = os.path.getmtime(dst_meta) if os.path.isfile(dst_meta) else 0\n need_copy = src_t > dst_t\n if need_copy:\n if os.path.isdir(target):\n shutil.rmtree(target)\n shutil.copytree(src, target)\n with open(manifest, encoding='utf-8') as f:\n pkg = json.load(f)\n changed = False\n # Don't add link: dep — pnpm creates broken symlinks; real dir + bundles registration suffices.\n deps = pkg.get('dependencies', {})\n if BUNDLE in deps:\n del deps[BUNDLE]\n changed = True\n bundles = pkg.setdefault('dsh', {}).setdefault('profile', {}).setdefault('bundles', [])\n if BUNDLE not in bundles:\n bundles.append(BUNDLE)\n changed = True\n if changed:\n with open(manifest, 'w', encoding='utf-8') as f:\n json.dump(pkg, f, indent=2)\n print('openviking-memory bundle ready in %s profile -> %s' % (prof, target))\ninstall('web')\ninstall('dsh-tui')\n\"\"\"\n\n# ── Template block (dsh_tpl_block in the shell) ──────────────────────\n# Shell variables ($DSH_RUNTIME, $DSH_HOME, $OPENVIKING_URL, $_ov_py, …) are\n# preserved as literal dollar signs — they are expanded at sandbox startup time.\n# __URL__ is replaced with the OpenViking endpoint; __OV_BODY_CODE__ is replaced\n# with the embedded runtime Python code above.\n_TPL_BLOCK_TEMPLATE = \"\"\"# ── OpenViking memory integration (added by huawei-cloud-openviking-agent-integration skill) ──\n# @openviking/dsh-memory-plugin installed as real package into web/dsh-tui profile node_modules,\n# registered in dsh.profile.bundles. Idempotent; cache-first (peer dep sync guarded by marker).\nif [ -d \"$DSH_RUNTIME/plugins/@openviking/dsh-memory-plugin\" ]; then\n export OPENVIKING_URL=\"${OPENVIKING_URL:-__URL__}\"\n _ov_py=\"\"\n for _py in python3.12 python3.11 python3.10 python3; do command -v \"$_py\" >/dev/null 2>&1 && _ov_py=\"$_py\" && break; done\n : \"${_ov_py:=python3}\"\n \"$_ov_py\" - \"$DSH_HOME\" \"$DSH_RUNTIME/plugins/@openviking/dsh-memory-plugin\" \"$OPENVIKING_URL\" \u003c\u003c'OVDSPY'\n__OV_BODY_CODE__\nOVDSPY\n _dsh_nm=\"$DSH_RUNTIME/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai\"\n _plugin_da=\"$DSH_RUNTIME/plugins/@openviking/dsh-memory-plugin/node_modules/@deepseek-ai\"\n _peers_marker=\"$_plugin_da/.openviking-peers-synced\"\n # Host fingerprint = dsh version + every @deepseek-ai/* package version. Marker\n # stores this fingerprint; sync runs only when the host runtime actually changed.\n _dsh_fp=\"\"\n if [[ -f \"$DSH_RUNTIME/lib/node_modules/@deepseek-ai/dsh/package.json\" ]]; then\n _dsh_fp=$(\"$_ov_py\" -c \"import json,glob,os,hashlib\nbase='$DSH_RUNTIME/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai'\nh=hashlib.sha1()\nfor pkg in sorted(glob.glob(os.path.join(base,'*','package.json'))):\n try: h.update(json.load(open(pkg)).get('version','').encode())\n except Exception: pass\nprint(h.hexdigest())\" 2>/dev/null)\n fi\n if [[ -f \"$_peers_marker\" && \"$(cat \"$_peers_marker\" 2>/dev/null)\" == \"$_dsh_fp\" && -n \"$_dsh_fp\" ]]; then\n echo \"OV peer deps already synced (host fingerprint match).\"\n elif [[ -d \"$_dsh_nm\" && -d \"$_plugin_da\" && -n \"$_dsh_fp\" ]]; then\n for _pd in \"$_dsh_nm\"/*; do\n [[ -d \"$_pd\" ]] || continue\n _pn=$(basename \"$_pd\")\n # Host-matched: copy EVERY @deepseek-ai/* package (incl. dsh-llm/dsh-tools) so\n # plugin deps always equal the installed dsh runtime version — never pinned.\n rm -rf \"$_plugin_da/$_pn\"\n cp -r \"$_pd\" \"$_plugin_da/$_pn\"\n done\n printf '%s' \"$_dsh_fp\" > \"$_peers_marker\"\n echo \"OV peer deps synced (host fingerprint $_dsh_fp).\"\n else\n echo \"OV peer deps: host fingerprint unavailable — skipping (no sync)\"\n fi\nfi\n\"\"\"\n\n\n# ── ov_body (DSHOVBODY heredoc) ──────────────────────────────────────\ndef cmd_ov_body(args):\n \"\"\"Install @openviking/dsh-memory-plugin bundle into live dsh profiles (web, dsh-tui).\n\n Args: home, src, url (url is accepted for parity with the heredoc but unused).\n \"\"\"\n home = args[0]\n src = args[1]\n # url = args[2] # accepted for parity with the heredoc; unused in this logic\n\n def install(prof):\n pdir = os.path.join(home, \"profiles\", prof)\n manifest = os.path.join(pdir, \"package.json\")\n if not os.path.isfile(manifest):\n sys.stderr.write(\"skip %s profile: package.json not found\\n\" % prof)\n return\n target = os.path.join(pdir, \"node_modules\", \"@openviking\", \"dsh-memory-plugin\")\n if os.path.islink(target) and not os.path.isdir(target):\n os.remove(target)\n if os.path.islink(target):\n os.remove(target)\n parent = os.path.dirname(target)\n os.makedirs(parent, exist_ok=True)\n if os.path.isdir(src):\n # Re-copy if target missing OR src commit differs (src/.openviking-sync newer):\n # keeps live profiles from going stale when the runtime cache is updated.\n need_copy = not os.path.isdir(target)\n if not need_copy:\n src_meta = os.path.join(src, \".openviking-sync\")\n dst_meta = os.path.join(target, \".openviking-sync\")\n if os.path.isfile(src_meta):\n src_t = os.path.getmtime(src_meta)\n dst_t = os.path.getmtime(dst_meta) if os.path.isfile(dst_meta) else 0\n need_copy = src_t > dst_t\n if need_copy:\n if os.path.isdir(target):\n shutil.rmtree(target)\n shutil.copytree(src, target)\n pkg = load_json(manifest)\n changed = False\n # Don't add link: dep — pnpm creates broken symlinks; real dir + bundles registration suffices.\n deps = pkg.get(\"dependencies\", {})\n if BUNDLE in deps:\n del deps[BUNDLE]\n changed = True\n bundles = pkg.setdefault(\"dsh\", {}).setdefault(\"profile\", {}).setdefault(\"bundles\", [])\n if BUNDLE not in bundles:\n bundles.append(BUNDLE)\n changed = True\n if changed:\n save_json(manifest, pkg)\n print(\"openviking-memory bundle ready in %s profile -> %s\" % (prof, target))\n\n install(\"web\")\n install(\"dsh-tui\")\n\n\n# ── tpl_block (dsh_tpl_block shell function) ─────────────────────────\ndef cmd_tpl_block(args):\n \"\"\"Emit the shell template block for ov-deepseek-harness-init.sh.\n\n Args: url (the OpenViking endpoint).\n \"\"\"\n url = args[0]\n block = _TPL_BLOCK_TEMPLATE.replace(\"__URL__\", url).replace(\n \"__OV_BODY_CODE__\", OV_BODY_CODE\n )\n sys.stdout.write(block)\n\n\n# ── seed_profiles (DSHSEED heredoc) ──────────────────────────────────\ndef cmd_seed_profiles(args):\n \"\"\"Pre-seed runtime .dsh/profiles (deploy-resilient layer).\n\n Args: home, src.\n \"\"\"\n home = args[0]\n src = args[1]\n\n for prof in (\"web\", \"dsh-tui\"):\n pdir = os.path.join(home, \"profiles\", prof)\n manifest = os.path.join(pdir, \"package.json\")\n if not os.path.isfile(manifest):\n continue\n pkg = load_json(manifest)\n changed = False\n deps = pkg.get(\"dependencies\", {})\n if BUNDLE in deps:\n del deps[BUNDLE]\n changed = True\n bundles = pkg.setdefault(\"dsh\", {}).setdefault(\"profile\", {}).setdefault(\"bundles\", [])\n if BUNDLE not in bundles:\n bundles.append(BUNDLE)\n changed = True\n if changed:\n save_json(manifest, pkg)\n target = os.path.join(pdir, \"node_modules\", \"@openviking\", \"dsh-memory-plugin\")\n if os.path.islink(target):\n os.remove(target)\n if not os.path.isdir(target):\n os.makedirs(os.path.dirname(target), exist_ok=True)\n shutil.copytree(src, target)\n\n\n# ── inject_template (DSHINJ heredoc) ─────────────────────────────────\ndef cmd_inject_template(args):\n \"\"\"Write init script from stdin block and inject source line into template start.sh.\n\n Args: tpl_path, init_path, template_dir, shared_dir.\n Reads the template block from stdin.\n \"\"\"\n tpl_path = args[0]\n init_path = args[1]\n template_dir = args[2]\n shared_dir = args[3]\n block = sys.stdin.read()\n\n write_init(\n init_path,\n \"DeepSeek Harness\",\n f\"Sourced by {template_dir}/deepseek-harness/start.sh (single source line).\",\n block.lstrip(\"\\n\"),\n )\n sb = source_block(shared_dir, \"ov-deepseek-harness-init.sh\")\n\n content = read_text(tpl_path)\n if \"ov-deepseek-harness-init.sh\" in content:\n sys.exit(0)\n anchor = 'echo \"==> starting DeepSeek Harness web UI (http://127.0.0.1:${DSH_WEB_PORT}) ...\"\\n'\n if anchor not in content:\n sys.stderr.write(\"ERROR: anchor not found in template start.sh\\n\")\n sys.exit(1)\n content = content.replace(anchor, sb + anchor, 1)\n write_text(tpl_path, content)\n\n\n# ── unbind_template (DSHUNBINJ heredoc) ──────────────────────────────\ndef cmd_unbind_template(args):\n \"\"\"Remove OpenViking integration block from template start.sh.\n\n Args: path.\n Handles both new-style (3-line source block) and old-style (═══ rule lines)\n injection formats.\n \"\"\"\n path = args[0]\n content = read_text(path)\n changed = False\n # New-style: remove the 3-line source block\n marker = \"# ── OpenViking integration (added by huawei-cloud-openviking-agent-integration skill) ──\"\n idx = content.find(marker)\n if idx != -1:\n end_marker = \"# ── End OpenViking integration ──\"\n end_idx = content.find(end_marker, idx)\n if end_idx != -1:\n cut_end = end_idx + len(end_marker)\n while cut_end \u003c len(content) and content[cut_end] == \"\\n\":\n cut_end += 1\n content = content[:idx] + content[cut_end:]\n changed = True\n # Old-style fallback: remove the full injected block with ═══ rule lines\n if not changed and \"OpenViking long-term memory integration\" in content:\n lines = content.split(\"\\n\")\n anchor = 'echo \"==> starting DeepSeek Harness web UI'\n anchor_idx = None\n for i, line in enumerate(lines):\n if anchor in line:\n anchor_idx = i\n break\n if anchor_idx is not None:\n marker_idx = None\n for i in range(anchor_idx - 1, -1, -1):\n if \"OpenViking long-term memory integration\" in lines[i]:\n marker_idx = i\n break\n start = None\n if marker_idx is not None:\n for i in range(marker_idx - 1, -1, -1):\n if lines[i].startswith(\"# ═══════════\"):\n start = i\n break\n if start is None and marker_idx is not None:\n start = marker_idx\n if start is not None:\n end = anchor_idx\n while end > start and lines[end - 1] == \"\":\n end -= 1\n new_lines = lines[:start] + lines[anchor_idx:]\n while new_lines and new_lines[-1] == \"\":\n new_lines.pop()\n content = \"\\n\".join(new_lines) + \"\\n\"\n changed = True\n if changed:\n write_text(path, content)\n\n\n# ── remove_bundle (DSHPJSON / DSHRTJSON heredocs) ────────────────────\ndef cmd_remove_bundle(args):\n \"\"\"Remove @openviking/dsh-memory-plugin from dependencies and bundles in package.json.\n\n Args: path (path to package.json).\n Used for both live profiles (DSHPJSON) and runtime profiles (DSHRTJSON).\n \"\"\"\n path = args[0]\n pkg = load_json(path)\n changed = False\n deps = pkg.get(\"dependencies\")\n if deps:\n if deps.pop(BUNDLE, None) is not None:\n changed = True\n if not deps:\n pkg.pop(\"dependencies\")\n bundles = pkg.get(\"dsh\", {}).get(\"profile\", {}).get(\"bundles\")\n if bundles:\n b = [x for x in bundles if x != BUNDLE]\n if len(b) != len(bundles):\n bundles[:] = b\n changed = True\n if not bundles:\n profile = pkg.get(\"dsh\", {}).get(\"profile\", {})\n profile.pop(\"bundles\", None)\n if not profile:\n pkg.get(\"dsh\", {}).pop(\"profile\", None)\n if not pkg.get(\"dsh\"):\n pkg.pop(\"dsh\", None)\n if changed:\n save_json(path, pkg)\n\n\n# ── dispatch ─────────────────────────────────────────────────────────\ndef main():\n if len(sys.argv) \u003c 2:\n sys.stderr.write(\n \"Usage: ov_deepseek_harness.py \u003csubcommand> [args...]\\n\"\n \"Subcommands: ov_body, tpl_block, seed_profiles, inject_template, \"\n \"unbind_template, remove_bundle\\n\"\n )\n sys.exit(2)\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"ov_body\": cmd_ov_body,\n \"tpl_block\": cmd_tpl_block,\n \"seed_profiles\": cmd_seed_profiles,\n \"inject_template\": cmd_inject_template,\n \"unbind_template\": cmd_unbind_template,\n \"remove_bundle\": cmd_remove_bundle,\n }\n fn = dispatch.get(cmd)\n if not fn:\n sys.stderr.write(f\"Unknown subcommand: {cmd}\\n\")\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":17124,"content_sha256":"5bd9454fabfa7551a59750578dfd967fe6acb1d5e58fa05575c6ece6bf00e45a"},{"filename":"scripts/py/agents/ov_hermes.py","content":"#!/usr/bin/env python3\n\"\"\"ov_hermes.py — Hermes agent Python operations CLI.\n\nReplaces all heredoc Python in agents/hermes.sh.\n\nSubcommands:\n integrate \u003ctpl> \u003cendpoint> \u003cshared_dir> write init script + inject source block\n clean-legacy-mcp \u003cconfig.yaml> remove legacy mcp_servers from live config\n unbind-template \u003ctpl> \u003cshared_dir> remove source block from template\n unbind-live \u003cconfig.yaml> remove MCP + memory provider from live config\n clean-snapshot \u003csnapshot.json> remove openviking entries from skills snapshot\n clean-usage \u003cusage.json> remove openviking entries from skills usage\n clean-memory \u003cMEMORY.md> remove openviking lines from MEMORY.md\n\"\"\"\nimport json\nimport os\nimport re\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\nfrom ov_common import read_text, write_text, write_init\n\n\ndef cmd_integrate(args):\n tpl_path, endpoint, shared_dir = args[0], args[1], args[2]\n tpl = read_text(tpl_path)\n\n block = f\"\"\"\n# ── OpenViking memory provider (added by huawei-cloud-openviking-agent-integration skill) ──\n# Re-injects memory.provider after model config is written on each start.\nif ! grep -q \"provider: openviking\" \"$HOME/.hermes/config.yaml\" 2>/dev/null; then\n cat >> \"$HOME/.hermes/config.yaml\" \u003c\u003c 'OVYAML'\nmemory:\n provider: openviking\n openviking:\n endpoint: {endpoint}\nOVYAML\nfi\nif ! grep -q \"OPENVIKING_ENDPOINT\" \"$HOME/.hermes/.env\" 2>/dev/null; then\n echo \"OPENVIKING_ENDPOINT='{endpoint}'\" >> \"$HOME/.hermes/.env\"\nfi\n\"\"\"\n init_path = f\"{shared_dir}/ov-hermes-init.sh\"\n write_init(init_path, \"Hermes\", \"integrate.sh\", block.lstrip(\"\\n\"))\n\n sb = (\n \"# ── OpenViking integration (added by huawei-cloud-openviking-agent-integration skill) ──\\n\"\n f\"source {shared_dir}/ov-hermes-init.sh\\n\"\n \"# ── End OpenViking integration ──\\n\\n\"\n )\n m = re.search(r\"(?m)^sleep infinity$\", tpl)\n if not m:\n print(\"ERROR: insertion marker 'sleep infinity' not found in template\", file=sys.stderr)\n sys.exit(1)\n tpl = tpl[: m.start()] + sb + tpl[m.start():]\n write_text(tpl_path, tpl)\n\n\ndef _clean_mcp_servers(content):\n content = re.sub(r\"\\n# OpenViking MCP server\\nmcp_servers:\\n openviking:\\n url: [^\\n]+\\n\", \"\\n\", content)\n content = re.sub(r\"\\nmcp_servers:\\n openviking:\\n url: [^\\n]+\\n\", \"\\n\", content)\n content = re.sub(r\"\\nmcp_servers:\\s*\\n(?=\\n[^ ])\", \"\\n\", content)\n return content\n\n\ndef cmd_clean_legacy_mcp(args):\n path = args[0]\n content = read_text(path)\n content = _clean_mcp_servers(content)\n write_text(path, content)\n\n\ndef cmd_unbind_template(args):\n path, shared_dir = args[0], args[1]\n content = read_text(path)\n content = re.sub(\n r\"# ── OpenViking integration \\(added by huawei-cloud-openviking-agent-integration skill\\) ──\\n\"\n + re.escape(f\"source {shared_dir}/ov-hermes-init.sh\\n\")\n + r\"# ── End OpenViking integration ──\\n\\n?\",\n \"\",\n content,\n )\n write_text(path, content)\n\n\ndef cmd_unbind_live(args):\n path = args[0]\n content = read_text(path)\n content = _clean_mcp_servers(content)\n content = re.sub(\n r\"\\n# OpenViking native memory provider[^\\n]*\\nmemory:\\n provider: openviking\\n openviking:\\n endpoint: [^\\n]+\\n\",\n \"\\n\",\n content,\n )\n content = re.sub(r\"\\nmemory:\\n provider: openviking\\n openviking:\\n endpoint: [^\\n]+\\n\", \"\\n\", content)\n content = re.sub(r\"\\nmemory:\\s*\\n(?=\\n[^ ])\", \"\\n\", content)\n content = content.rstrip() + \"\\n\"\n write_text(path, content)\n\n\ndef cmd_clean_snapshot(args):\n path = args[0]\n with open(path) as f:\n data = json.load(f)\n data[\"manifest\"] = {k: v for k, v in data.get(\"manifest\", {}).items() if \"openviking\" not in k.lower()}\n data[\"skills\"] = [s for s in data.get(\"skills\", []) if \"openviking\" not in s.get(\"skill_name\", \"\").lower()]\n with open(path, \"w\") as f:\n json.dump(data, f, indent=2)\n\n\ndef cmd_clean_usage(args):\n path = args[0]\n with open(path) as f:\n data = json.load(f)\n data = {k: v for k, v in data.items() if \"openviking\" not in k.lower()}\n with open(path, \"w\") as f:\n json.dump(data, f, indent=2)\n\n\ndef cmd_clean_memory(args):\n path = args[0]\n with open(path) as f:\n lines = f.readlines()\n cleaned = [l for l in lines if \"openviking\" not in l.lower() and \"viking\" not in l.lower() and \"1933\" not in l]\n with open(path, \"w\") as f:\n f.writelines(cleaned)\n\n\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"integrate\": cmd_integrate,\n \"clean-legacy-mcp\": cmd_clean_legacy_mcp,\n \"unbind-template\": cmd_unbind_template,\n \"unbind-live\": cmd_unbind_live,\n \"clean-snapshot\": cmd_clean_snapshot,\n \"clean-usage\": cmd_clean_usage,\n \"clean-memory\": cmd_clean_memory,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5240,"content_sha256":"0d4aa3590e8fba9f65fd152ec4e283c50142bd07c4ebfcae0dc11165b14320ef"},{"filename":"scripts/py/agents/ov_kimicode.py","content":"#!/usr/bin/env python3\n\"\"\"ov_kimicode.py — KimiCode agent Python operations CLI.\n\nReplaces all heredoc / inline Python in agents/kimicode.sh.\n\nSubcommands:\n integrate \u003ctpl> \u003cmcp_url> \u003cshared_dir> \u003ctemplate_dir> \u003cruntime_dir>\n Deploy MCP mcp.json injection + session lifecycle hooks (path 3:\n hooks + MCP) into template start.sh; deploy hook dispatcher\n (shared/ov-kimi-hook/) + py helpers (shared/py/) for runtime use.\n write-live-mcp \u003cmcp.json> \u003cmcp_url>\n append-hooks-config \u003cconfig.toml> \u003cshared_dir>\n Append [[hooks]] section to config.toml idempotently (live effect).\n strip-hooks-config \u003cconfig.toml>\n Remove the OpenViking [[hooks]] section from config.toml.\n check-hooks \u003cconfig.toml> exit 0 if OpenViking hooks present\n unbind-hooks \u003cshared_dir> \u003cruntime_dir>\n Remove deployed hook dispatcher + py helpers (OV-owned only).\n unbind-template \u003ctpl> \u003cshared_dir>\n unbind-live-mcp \u003cmcp.json>\n unbind-legacy-toml \u003cconfig.toml>\n check-live-mcp \u003cmcp.json> exit 0 if openviking present\n get-live-mcp-url \u003cmcp.json> print URL (empty if absent)\n\"\"\"\nimport json\nimport os\nimport re\nimport shutil\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\nfrom ov_common import read_text, write_text\n\nwith open(os.path.join(os.path.dirname(os.path.abspath(__file__)), \"..\", \"..\", \"templates\", \"agents-md-snippet.md\")) as _f:\n AGENTS_MD = _f.read()\n\nOV_CONFIG_JSON = \"\"\"{\n \"enabled\": true,\n \"timeoutMs\": 30000,\n \"repoContext\": { \"enabled\": true, \"cacheTtlMs\": 60000 },\n \"autoRecall\": {\n \"enabled\": true,\n \"limit\": 10,\n \"scoreThreshold\": 0.35,\n \"maxContentChars\": 500,\n \"preferAbstract\": true,\n \"tokenBudget\": 2000,\n \"minQueryLength\": 3\n },\n \"recallLimit\": 15,\n \"recallMaxContentChars\": 20000,\n \"commitTokenThreshold\": 20000,\n \"commitKeepRecentCount\": 10,\n \"profileTokenBudget\": 10000,\n \"resumeContextBudget\": 32000\n}\n\"\"\"\n\n# ── Session lifecycle hooks (path 3: hooks + MCP) ──────────────────────────\n# Recovered from git history (5a6634e-era production config): kimi-code reads\n# [[hooks]] from config.toml; each entry dispatches to the shared hook script\n# with the lowercase event as argv[1]. Event names are CamelCase per the\n# kimi-code hook protocol.\nHOOKS_MARKER = \"# ── OpenViking hooks\"\nHOOKS_FIRST_LINE = \"# ── OpenViking hooks (session lifecycle, managed by huawei-cloud-openviking-agent-integration skill) ──\"\n\n\ndef _hooks_toml(shared_dir):\n \"\"\"TOML [[hooks]] section for config.toml (kimi-code session lifecycle).\"\"\"\n return (\n HOOKS_FIRST_LINE + \"\\n\"\n \"[[hooks]]\\n\"\n 'event = \"SessionStart\"\\n'\n f'command = \"{shared_dir}/ov-kimi-hook/ov-kimi-hook.sh session-start\"\\n'\n \"timeout = 10\\n\"\n \"[[hooks]]\\n\"\n 'event = \"UserPromptSubmit\"\\n'\n f'command = \"{shared_dir}/ov-kimi-hook/ov-kimi-hook.sh user-prompt\"\\n'\n \"timeout = 15\\n\"\n \"[[hooks]]\\n\"\n 'event = \"Stop\"\\n'\n f'command = \"{shared_dir}/ov-kimi-hook/ov-kimi-hook.sh stop\"\\n'\n \"timeout = 30\\n\"\n \"[[hooks]]\\n\"\n 'event = \"SessionEnd\"\\n'\n f'command = \"{shared_dir}/ov-kimi-hook/ov-kimi-hook.sh session-end\"\\n'\n \"timeout = 30\\n\"\n \"[[hooks]]\\n\"\n 'event = \"PreCompact\"\\n'\n f'command = \"{shared_dir}/ov-kimi-hook/ov-kimi-hook.sh pre-compact\"\\n'\n \"timeout = 30\\n\"\n )\n\n\ndef _strip_ov_hooks(content):\n \"\"\"Idempotent: drop any previous OpenViking hooks section (always appended last).\"\"\"\n if HOOKS_MARKER in content:\n content = content.split(HOOKS_MARKER)[0].rstrip()\n return content\n\n\ndef _scripts_dir():\n \"\"\"Absolute path of the skill's scripts/ directory (…/scripts/lib, …/scripts/py live there).\"\"\"\n return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n\n\ndef cmd_deploy_hook_files(args):\n \"\"\"Deploy the hook dispatcher + py helpers for runtime use.\n\n Layout MUST live under \u003cshared_dir> (ro-bound into the bwrap sandbox):\n \u003cshared_dir>/ov-kimi-hook/ov-kimi-hook.sh dispatcher\n \u003cshared_dir>/py/ov_kimi_hook.py ... helpers\n Dispatcher resolves OV_PY_DIR = $(dirname $0)/../py → \u003cshared_dir>/py,\n i.e. inside the shared mount — visible to hooks running in the sandbox.\n \"\"\"\n shared_dir, runtime_dir = args[0], args[1]\n scripts_dir = _scripts_dir()\n\n bundle_dir = os.path.join(shared_dir, \"ov-kimi-hook\")\n os.makedirs(bundle_dir, exist_ok=True)\n dst_sh = os.path.join(bundle_dir, \"ov-kimi-hook.sh\")\n shutil.copy2(os.path.join(scripts_dir, \"lib\", \"ov_kimi_hook.sh\"), dst_sh)\n os.chmod(dst_sh, 0o755)\n print(f\"deployed {dst_sh}\")\n\n py_dir = os.path.join(shared_dir, \"py\")\n os.makedirs(py_dir, exist_ok=True)\n src_py = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"..\")\n for name in (\"ov_kimi_hook.py\", \"ov_redact.py\", \"ov_common.py\"):\n dst = os.path.join(py_dir, name)\n shutil.copy2(os.path.join(src_py, name), dst)\n print(f\"deployed {dst}\")\n\n\ndef cmd_append_hooks_config(args):\n \"\"\"Append the [[hooks]] section to a config.toml (idempotent). Live effect.\"\"\"\n config_path, shared_dir = args[0], args[1]\n if not os.path.exists(config_path):\n print(\"config.toml not found — skipped\")\n return\n with open(config_path, encoding=\"utf-8\") as f:\n content = f.read()\n content = _strip_ov_hooks(content)\n with open(config_path, \"w\", encoding=\"utf-8\") as f:\n f.write(content + \"\\n\" + _hooks_toml(shared_dir))\n print(\"OpenViking hooks injected into config.toml\")\n\n\ndef cmd_strip_hooks_config(args):\n config_path = args[0]\n if not os.path.exists(config_path):\n print(\"config.toml not found\")\n return\n with open(config_path, encoding=\"utf-8\") as f:\n content = f.read()\n if HOOKS_MARKER in content:\n new = _strip_ov_hooks(content) + \"\\n\"\n with open(config_path, \"w\", encoding=\"utf-8\") as f:\n f.write(new)\n print(\"OpenViking hooks removed from config.toml\")\n else:\n print(\"no OpenViking hooks in config.toml\")\n\n\ndef cmd_check_hooks(args):\n config_path = args[0]\n if os.path.exists(config_path):\n with open(config_path, encoding=\"utf-8\") as f:\n if HOOKS_MARKER in f.read():\n sys.exit(0)\n sys.exit(1)\n\n\ndef _remove_ov_py_helpers(py_dir, scripts_dir):\n \"\"\"Remove OV-owned py helpers from a py dir; ov_common.py is byte-compared\n with the skill copy before removal (defensive against user-modified files).\"\"\"\n if not os.path.isdir(py_dir):\n return\n for name in (\"ov_kimi_hook.py\", \"ov_redact.py\"):\n p = os.path.join(py_dir, name)\n if os.path.exists(p):\n os.remove(p)\n print(f\"removed {py_dir}/{name}\")\n common_src = os.path.join(scripts_dir, \"py\", \"ov_common.py\")\n common_dst = os.path.join(py_dir, \"ov_common.py\")\n if os.path.exists(common_dst):\n with open(common_dst, \"rb\") as f:\n dst_data = f.read()\n with open(common_src, \"rb\") as f:\n src_data = f.read()\n if dst_data == src_data:\n os.remove(common_dst)\n print(f\"removed {py_dir}/ov_common.py\")\n else:\n print(f\"WARN: {py_dir}/ov_common.py differs from skill copy — left untouched\")\n cache_dir = os.path.join(py_dir, \"__pycache__\")\n if os.path.isdir(cache_dir):\n for fn in os.listdir(cache_dir):\n if fn.startswith((\"ov_kimi_hook.\", \"ov_redact.\", \"ov_common.\")) and fn.endswith((\".pyc\", \".pyo\")):\n os.remove(os.path.join(cache_dir, fn))\n if not os.listdir(cache_dir):\n os.rmdir(cache_dir)\n if not os.listdir(py_dir):\n os.rmdir(py_dir)\n print(f\"removed empty {py_dir}\")\n\n\ndef cmd_unbind_hooks(args):\n \"\"\"Remove deployed hook dispatcher + py helpers (OV-owned only).\"\"\"\n shared_dir, runtime_dir = args[0], args[1]\n scripts_dir = _scripts_dir()\n\n # New layout: \u003cshared_dir>/ov-kimi-hook/ (entirely OV-owned — safe to remove)\n bundle_dir = os.path.join(shared_dir, \"ov-kimi-hook\")\n if os.path.isdir(bundle_dir):\n shutil.rmtree(bundle_dir)\n print(f\"removed {bundle_dir}/\")\n else:\n print(\"ov-kimi-hook/ not deployed\")\n\n # New helpers dir (sandbox-visible): \u003cshared_dir>/py/\n _remove_ov_py_helpers(os.path.join(shared_dir, \"py\"), scripts_dir)\n\n # Legacy layout (pre-2026-09-21): dispatcher at shared root + helpers in runtime/py\n legacy_sh = os.path.join(shared_dir, \"ov-kimi-hook.sh\")\n if os.path.exists(legacy_sh):\n os.remove(legacy_sh)\n print(f\"removed legacy {legacy_sh}\")\n _remove_ov_py_helpers(os.path.join(runtime_dir, \"py\"), scripts_dir)\n\n\ndef cmd_integrate(args):\n tpl_path, url, shared_dir, template_dir, runtime_dir = args[0], args[1], args[2], args[3], args[4]\n content = read_text(tpl_path)\n marker = \"# ── OpenViking MCP injection (added by huawei-cloud-openviking-agent-integration skill) ──\"\n if \"ov-kimicode-init.sh\" in content or marker in content:\n print(\"already\")\n return\n\n hooks_toml = _hooks_toml(shared_dir)\n\n block = f\"\"\"# ── OpenViking MCP injection (added by huawei-cloud-openviking-agent-integration skill) ──\n# kimi-code reads MCP config from mcp.json (NOT config.toml, which is recreated on each start).\n_ov_py=\"\"\nfor _py in python3.12 python3.11 python3.10 python3; do command -v \"$_py\" >/dev/null 2>&1 && _ov_py=\"$_py\" && break; done\n: \"${{_ov_py:=python3}}\"\nMCP_FILE=\"$KIMI_CODE_HOME/mcp.json\"\n$_ov_py - \"$MCP_FILE\" \u003c\u003c'MCPEOF'\nimport json, sys, os\npath = sys.argv[1]\nentry = {{\"url\": \"{url}\"}}\nif os.path.exists(path):\n with open(path) as f:\n cfg = json.load(f)\nelse:\n cfg = {{}}\nservers = cfg.setdefault(\"mcpServers\", {{}})\nif \"openviking\" not in servers:\n servers[\"openviking\"] = entry\n with open(path, \"w\") as f:\n json.dump(cfg, f, indent=2)\n print(\"OpenViking MCP injected into mcp.json\")\nelse:\n print(\"OpenViking MCP already in mcp.json\")\nMCPEOF\nmkdir -p /workspace\ncat > /workspace/AGENTS.md \u003c\u003c 'AGENTSMD'\n{AGENTS_MD}AGENTSMD\nOV_CONF_DIR=\"$HOME/.config/opencode\"\nmkdir -p \"$OV_CONF_DIR\"\nif [[ ! -f \"$OV_CONF_DIR/openviking-config.json\" ]]; then\n cat > \"$OV_CONF_DIR/openviking-config.json\" \u003c\u003c'OVCONF'\n{OV_CONFIG_JSON}OVCONF\nfi\n# ── OpenViking session lifecycle hooks (path 3: hooks + MCP) ──\n# Every boot regenerates config.toml → re-append [[hooks]] so session hooks\n# (auto-recall, auto-capture, auto-commit) persist across restarts.\nCONFIG_FILE=\"$KIMI_CODE_HOME/config.toml\"\nif [ -f \"$CONFIG_FILE\" ]; then\n $_ov_py - \"$CONFIG_FILE\" \u003c\u003c'HOOKEOF'\nimport sys\npath = sys.argv[1]\nwith open(path) as f:\n content = f.read()\nif \"# ── OpenViking hooks\" in content:\n content = content.split(\"# ── OpenViking hooks\")[0].rstrip()\nwith open(path, \"w\") as f:\n f.write(content.rstrip() + \"\\\\n\")\nHOOKEOF\n cat >> \"$CONFIG_FILE\" \u003c\u003c'HOOKTOML'\n{hooks_toml}HOOKTOML\nfi\n\"\"\"\n\n init_path = f\"{shared_dir}/ov-kimicode-init.sh\"\n with open(init_path, \"w\") as f:\n f.write(\"#!/usr/bin/env bash\\n\")\n f.write(\"# ── OpenViking integration for KimiCode ──\\n\")\n f.write(f\"# Sourced by {template_dir}/kimicode/start.sh (single source line).\\n\")\n f.write(\"# Managed by huawei-cloud-openviking-agent-integration skill.\\n\\n\")\n f.write(block)\n os.chmod(init_path, 0o755)\n\n source_block = (\n \"# ── OpenViking integration (added by huawei-cloud-openviking-agent-integration skill) ──\\n\"\n f\"source {shared_dir}/ov-kimicode-init.sh\\n\"\n \"# ── End OpenViking integration ──\\n\"\n )\n\n lines = content.split(\"\\n\")\n inserted = False\n for i, line in enumerate(lines):\n if line.strip() == \"sleep infinity\" or line.strip().startswith(\"sleep infinity\"):\n lines.insert(i, source_block.rstrip())\n lines.insert(i + 1, \"\")\n inserted = True\n break\n if not inserted:\n lines.append(source_block.rstrip())\n lines.append(\"\")\n write_text(tpl_path, \"\\n\".join(lines))\n print(\"injected\")\n\n # Deploy hook dispatcher + py helpers (runtime side)\n cmd_deploy_hook_files([shared_dir, runtime_dir])\n\n\ndef cmd_write_live_mcp(args):\n path, url = args[0], args[1]\n entry = {\"url\": url}\n if os.path.exists(path):\n with open(path) as f:\n cfg = json.load(f)\n else:\n cfg = {}\n servers = cfg.setdefault(\"mcpServers\", {})\n if \"openviking\" not in servers:\n servers[\"openviking\"] = entry\n with open(path, \"w\") as f:\n json.dump(cfg, f, indent=2)\n print(\"injected\")\n else:\n print(\"already exists\")\n\n\ndef cmd_unbind_template(args):\n path, shared_dir = args[0], args[1]\n content = read_text(path)\n # First try new-style: 3-line source block\n new_pattern = (\n r\"# ── OpenViking integration \\(added by huawei-cloud-openviking-agent-integration skill\\) ──\\n\"\n + re.escape(f\"source {shared_dir}/ov-kimicode-init.sh\\n\")\n + r\"# ── End OpenViking integration ──\\n\\n?\"\n )\n new_content = re.sub(new_pattern, \"\", content)\n if new_content != content:\n write_text(path, new_content)\n print(\"removed new-style source block\")\n return\n # Fall back to old-style: full inline block\n lines = content.splitlines(keepends=True)\n marker = \"# ── OpenViking MCP injection (added by huawei-cloud-openviking-agent-integration skill) ──\"\n marker_legacy = \"# ── OpenViking MCP injection (added by openviking-agent-integration skill) ──\"\n start = None\n for i, line in enumerate(lines):\n if (marker in line or marker_legacy in line) and start is None:\n start = i\n break\n end = None\n if start is not None:\n found_config = False\n for i in range(start, len(lines)):\n if \"# Create openviking-config.json\" in lines[i]:\n found_config = True\n if found_config and lines[i].strip() == \"fi\":\n end = i + 1\n break\n if end is None:\n for i in range(start, len(lines)):\n if lines[i].strip() == \"AGENTSMD\":\n end = i + 1\n break\n if start is not None and end is not None:\n if end \u003c len(lines) and lines[end].strip() == \"\":\n end += 1\n del lines[start:end]\n write_text(path, \"\".join(lines))\n print(\"removed MCP injection block\")\n else:\n write_text(path, \"\".join(lines))\n print(\"MCP injection block not found\")\n\n\ndef cmd_unbind_live_mcp(args):\n path = args[0]\n with open(path) as f:\n d = json.load(f)\n servers = d.get(\"mcpServers\", {})\n if \"openviking\" in servers:\n del servers[\"openviking\"]\n if not servers:\n del d[\"mcpServers\"]\n with open(path, \"w\") as f:\n json.dump(d, f, indent=2)\n\n\ndef cmd_unbind_legacy_toml(args):\n path = args[0]\n lines = []\n skip = False\n with open(path) as f:\n for line in f:\n if line.strip().startswith(\"[mcp_servers.openviking]\"):\n skip = True\n continue\n if skip and (line.strip().startswith(\"[\") or line.strip() == \"\"):\n if line.strip().startswith(\"[\"):\n skip = False\n else:\n continue\n if not skip:\n lines.append(line)\n while lines and lines[-1].strip() == \"\":\n lines.pop()\n with open(path, \"w\") as f:\n f.writelines(lines)\n f.write(\"\\n\")\n\n\ndef cmd_check_live_mcp(args):\n path = args[0]\n with open(path) as f:\n d = json.load(f)\n sys.exit(0 if \"openviking\" in d.get(\"mcpServers\", {}) else 1)\n\n\ndef cmd_get_live_mcp_url(args):\n path = args[0]\n with open(path) as f:\n d = json.load(f)\n print(d.get(\"mcpServers\", {}).get(\"openviking\", {}).get(\"url\", \"\"))\n\n\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"integrate\": cmd_integrate,\n \"deploy-hook-files\": cmd_deploy_hook_files,\n \"append-hooks-config\": cmd_append_hooks_config,\n \"strip-hooks-config\": cmd_strip_hooks_config,\n \"check-hooks\": cmd_check_hooks,\n \"unbind-hooks\": cmd_unbind_hooks,\n \"write-live-mcp\": cmd_write_live_mcp,\n \"unbind-template\": cmd_unbind_template,\n \"unbind-live-mcp\": cmd_unbind_live_mcp,\n \"unbind-legacy-toml\": cmd_unbind_legacy_toml,\n \"check-live-mcp\": cmd_check_live_mcp,\n \"get-live-mcp-url\": cmd_get_live_mcp_url,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()","content_type":"text/x-python; charset=utf-8","language":"python","size":16992,"content_sha256":"ce508aff0eec7b87bf6849382669593238140d386a38019ab2c6fcc36d446396"},{"filename":"scripts/py/agents/ov_openclaw.py","content":"#!/usr/bin/env python3\n\"\"\"ov_openclaw.py — OpenClaw agent subclass CLI.\n\nReplaces all embedded Python (heredocs and inline ``python3 -c`` calls) in\n``agents/openclaw.sh``. Each subcommand corresponds 1:1 to an original Python\nblock and implements the exact same logic, using shared helpers from\n``ov_common`` where appropriate.\n\nSubcommands:\n slot-owner \u003ccfg_file>\n Print plugins.slots.contextEngine owner (empty string on error).\n\n inject \u003cpath> \u003cendpoint> \u003cforce_slot> \u003callow_offline> \u003cnpm_registry> \u003cshared_dir> \u003cruntime_dir> \u003ctemplate_dir>\n Inject the official OpenViking plugin install block into template\n start.sh and write the standalone ov-openclaw-init.sh.\n\n clean-legacy-cfg \u003cpath>\n Remove legacy direct-config-write Step 5 block from template.\n\n clean-legacy-mcp \u003cpath>\n Remove legacy MCP injection Step 5 block from template.\n\n has-mcp-server \u003ccfg_file>\n Exit 0 if 'openviking' in mcp.servers, else exit 1.\n\n remove-mcp-server \u003ccfg_file>\n Remove openviking from mcp.servers and save.\n\n has-config \u003ccfg_file>\n Exit 0 if openviking is configured (plugin entry, contextEngine slot,\n or mcp server), else exit 1.\n\n unbind \u003cpath> \u003cshared_dir>\n Remove all OpenViking injection blocks from template start.sh.\n\n clean-config \u003ccfg_file>\n Remove openviking plugin entries, MCP servers, contextEngine slot,\n allow list, and tool policy. Prints 'cleaned' or 'skip'.\n\"\"\"\nimport os\nimport re\nimport sys\n\ndef _ov_mark_key_consumed():\n \"\"\"ISSUE-003: notify the parent bash entry point that --api-key was used.\"\"\"\n f = os.environ.get(\"OV_API_KEY_CONSUMED_FILE\")\n if not f:\n return\n try:\n os.makedirs(os.path.dirname(f), exist_ok=True)\n open(f, \"a\").close()\n except OSError:\n pass\n\n_HERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, _HERE)\nsys.path.insert(0, os.path.dirname(_HERE)) # parent dir holds ov_common.py\nfrom ov_common import (\n read_text, write_text, write_init, source_block,\n load_json, save_json,\n)\n\n\n# ── slot-owner ────────────────────────────────────────────────\ndef cmd_slot_owner(args):\n \"\"\"Print the plugins.slots.contextEngine owner (empty string on error).\n\n Replaces the two inline ``python3 -c`` calls at lines 98-109 of\n openclaw.sh that both print the contextEngine slot owner.\n \"\"\"\n cfg_file = args[0]\n try:\n d = load_json(cfg_file)\n owner = d.get(\"plugins\", {}).get(\"slots\", {}).get(\"contextEngine\", \"\")\n print(owner)\n except Exception:\n print(\"\")\n\n\n# ── inject ────────────────────────────────────────────────────\ndef cmd_inject(args):\n \"\"\"Inject the official OpenViking plugin install block into template start.sh.\n\n Replaces the PYINJECT heredoc (lines 124-323) in openclaw.sh.\n \"\"\"\n (path, endpoint, force_slot, allow_offline, npm_registry,\n shared_dir, runtime_dir, template_dir) = args[0:8]\n\n content = read_text(path)\n if \"ov-openclaw-init.sh\" in content or \"OpenViking plugin install\" in content:\n print(\"already\")\n sys.exit(0)\n\n block = \"\"\"# ── OpenViking plugin install (official ClawHub, added by huawei-cloud-openviking-agent-integration skill) ──\n# Cache-first: check if plugin already installed; if not, install from runtime cache (no network),\n# then ClawHub/npm on first integrate or cache miss. Survives undeploy+deploy via __OV_RUNTIME_DIR__/.\necho \"[openclaw] Provisioning OpenViking plugin (cache-first)...\"\nOV_PLUGIN_INSTALLED=0\nOV_VENDOR_SRC=\"__OV_RUNTIME_DIR__/openclaw/openviking-plugin-source\"\nif \"$NODE\" \"$CLI\" plugins list 2>/dev/null | grep -q \"openviking\"; then\n OV_PLUGIN_INSTALLED=1\n echo \"[openclaw] Plugin already installed (cache hit).\"\nfi\nOV_PREBUILT=\"__OV_RUNTIME_DIR__/openclaw/openviking-plugin-built\"\nif [[ \"$OV_PLUGIN_INSTALLED\" = \"0\" && -d \"$OV_PREBUILT\" && -d \"$OV_PREBUILT/dist\" ]]; then\n echo \"[openclaw] Installing from pre-built cache (Tier-0 fast path)...\"\n if \"$NODE\" \"$CLI\" plugins install --force --accept-capabilities \"$OV_PREBUILT\" 2>&1; then\n OV_PLUGIN_INSTALLED=1\n echo \"[openclaw] Installed from pre-built cache (no build needed).\"\n else\n echo \"[openclaw] WARNING: pre-built install failed — falling back to source build\"\n fi\nfi\nif [[ \"$OV_PLUGIN_INSTALLED\" = \"0\" && -d \"$OV_VENDOR_SRC\" ]]; then\n echo \"[openclaw] Installing from runtime source cache (Tier-1)...\"\n OV_BUILD_DIR=$(mktemp -d /tmp/openclaw-ov-build.XXXXXX)\n cp -a \"$OV_VENDOR_SRC/.\" \"$OV_BUILD_DIR/\"\n export NPM_CONFIG_REGISTRY=__OV_NPM_REGISTRY__\n # npm package ships with dist/ pre-compiled; only install runtime deps\n if [[ -d \"$OV_BUILD_DIR/dist\" ]]; then\n (cd \"$OV_BUILD_DIR\" && npm install --production --no-audit --no-fund 2>&1 | tail -3) || true\n if \"$NODE\" \"$CLI\" plugins install --force --accept-capabilities \"$OV_BUILD_DIR\" 2>&1; then\n OV_PLUGIN_INSTALLED=1\n echo \"[openclaw] Installed from runtime source cache (dist/ pre-compiled).\"\n else\n echo \"[openclaw] WARNING: local install failed — trying online\"\n fi\n else\n echo \"[openclaw] WARNING: no dist/ in source cache — trying online\"\n fi\n unset NPM_CONFIG_REGISTRY\n rm -rf \"$OV_BUILD_DIR\"\nfi\nif [[ \"$OV_PLUGIN_INSTALLED\" = \"0\" ]]; then\n echo \"[openclaw] Cache miss — trying online (ClawHub → npm mirrors)...\"\n if \"$NODE\" \"$CLI\" plugins install --accept-capabilities clawhub:@openviking/openclaw-plugin 2>&1; then\n OV_PLUGIN_INSTALLED=1\n echo \"[openclaw] Installed from ClawHub\"\n else\n echo \"[openclaw] WARNING: ClawHub failed — trying npm mirror\"\n export NPM_CONFIG_REGISTRY=__OV_NPM_REGISTRY__\n if \"$NODE\" \"$CLI\" plugins install --force --accept-capabilities @openviking/openclaw-plugin 2>&1; then\n OV_PLUGIN_INSTALLED=1\n echo \"[openclaw] Installed (npm Huawei Cloud mirror)\"\n else\n export NPM_CONFIG_REGISTRY=https://registry.npmmirror.com/\n if \"$NODE\" \"$CLI\" plugins install --force --accept-capabilities @openviking/openclaw-plugin 2>&1; then\n OV_PLUGIN_INSTALLED=1\n echo \"[openclaw] Installed (npm npmmirror)\"\n else\n echo \"[openclaw] WARNING: all online sources failed\"\n fi\n fi\n unset NPM_CONFIG_REGISTRY\n fi\nfi\n# Fix plugin permissions (gateway blocks world-writable paths)\nif [[ \"$OV_PLUGIN_INSTALLED\" = \"1\" ]]; then\n for _ext_dir in \"$HOME/.openclaw/extensions/openviking\" \"${OPENCLAW_STATE_DIR:-/tmp/.openclaw}/extensions/openviking\"; do\n if [[ -d \"$_ext_dir\" ]]; then\n chmod -R go-w \"$_ext_dir\" 2>/dev/null || true\n echo \"[openclaw] Fixed permissions: $_ext_dir\"\n fi\n done\nfi\n# Configure OpenViking endpoint via official JSON contract\nexport OPENVIKING_BASE_URL=\"__ENDPOINT__\"\nexport OPENVIKING_ENDPOINT=\"__ENDPOINT__\"\nif [ \"$OV_PLUGIN_INSTALLED\" = \"1\" ]; then\n OV_SETUP_ARGS=(--base-url \"__ENDPOINT__\" --json)\n [ -n \"__API_KEY__\" ] && OV_SETUP_ARGS+=(--api-key \"__API_KEY__\")\n [ \"${OV_FORCE_SLOT:-OV_FORCE_SLOT_DEFAULT}\" = \"1\" ] && OV_SETUP_ARGS+=(--force-slot)\n [ \"${OV_ALLOW_OFFLINE:-OV_ALLOW_OFFLINE_DEFAULT}\" = \"1\" ] && OV_SETUP_ARGS+=(--allow-offline)\n\n OV_SETUP_OUT=$(\"$NODE\" \"$CLI\" openviking setup \"${OV_SETUP_ARGS[@]}\" 2>/dev/null || true)\n if echo \"$OV_SETUP_OUT\" | grep -q '\"success\":true'; then\n echo \"[openclaw] Configured via setup (JSON): __ENDPOINT__\"\n elif echo \"$OV_SETUP_OUT\" | grep -q '\"action\":\"slot_blocked\"'; then\n if [ \"${OV_FORCE_SLOT:-OV_FORCE_SLOT_DEFAULT}\" = \"1\" ]; then\n echo \"[openclaw] WARNING: slot blocked; --force-slot retry already attempted\"\n else\n echo \"[openclaw] WARNING: contextEngine slot owned by another plugin; use --force-slot to override\"\n fi\n elif echo \"$OV_SETUP_OUT\" | grep -q '\"action\":\"error\"'; then\n echo \"[openclaw] ERROR: setup validation failed: $OV_SETUP_OUT\"\n elif echo \"$OV_SETUP_OUT\" | grep -q '\"health\":{\"ok\":false'; then\n if [ \"${OV_ALLOW_OFFLINE:-OV_ALLOW_OFFLINE_DEFAULT}\" != \"1\" ]; then\n echo \"[openclaw] WARNING: server unreachable and --allow-offline not approved\"\n fi\n elif echo \"$OV_SETUP_OUT\" | grep -q 'root_key'; then\n echo \"[openclaw] ERROR: setup requires --account-id/--user-id for root API keys\"\n else\n echo \"[openclaw] WARNING: unexpected setup output: $OV_SETUP_OUT\"\n fi\nelse\n echo \"[openclaw] Plugin not installed — skipping setup\"\nfi\n# Enable OpenViking plugin (sets contextEngine slot)\nif [ \"$OV_PLUGIN_INSTALLED\" = \"1\" ]; then\n \"$NODE\" \"$CLI\" plugins enable --accept-capabilities openviking 2>/dev/null || true\n \"$NODE\" \"$CLI\" config set 'plugins.allow' '[\"openviking\"]' 2>/dev/null || true\n \"$NODE\" \"$CLI\" config set \"plugins.entries.openviking.config.recallMaxInjectedChars\" 16000 2>/dev/null || true\n \"$NODE\" \"$CLI\" config set \"plugins.entries.openviking.config.recallLimit\" 10 2>/dev/null || true\n # ── Disable local memory to prevent recall quality degradation ──\n # OpenViking contextEngine handles auto-recall + auto-capture;\n # local session-memory hook and memory_search tool are redundant and degrade recall quality.\n \"$NODE\" \"$CLI\" config set \"hooks.internal.entries.session-memory.enabled\" false 2>/dev/null || true\n # Deny local memory_search tool — use OpenViking MCP tools (search/recall/remember) instead\n \"$NODE\" \"$CLI\" config set \"tools.deny\" '[\"memory_search\"]' 2>/dev/null || true\n echo \"[openclaw] Disabled session-memory hook and local memory_search (OpenViking handles recall)\"\n echo \"[openclaw] Enabled OpenViking plugin (contextEngine slot)\"\nfi\n# Append OpenViking instructions to AGENTS.md\nmkdir -p \"$OPENCLAW_STATE_DIR/workspace\"\nOV_AGENTS=\"$OPENCLAW_STATE_DIR/workspace/AGENTS.md\"\nif ! grep -q \"OpenViking Long-Term Memory\" \"$OV_AGENTS\" 2>/dev/null; then\n cat >> \"$OV_AGENTS\" \u003c\u003c 'OVAGENTS'\n## OpenViking Long-Term Memory\nOpenViking is integrated as the contextEngine plugin — it automatically recalls\nrelevant context before each response and captures important information after.\nYou also have direct access to OpenViking MCP tools for explicit operations:\n1. **search**: Deep semantic retrieval with session context and intent analysis.\n2. **recall**: Memory recall across memory types (events, entities, preferences).\n3. **remember**: Store important information — user preferences, project decisions, technical details.\n4. **read**: Read content from viking:// URIs for stored reference materials.\nThe contextEngine handles auto-recall automatically; use these tools for explicit\nor targeted operations when needed.\nOVAGENTS\n echo \"[openclaw] Instructions appended to AGENTS.md\"\nelse\n echo \"[openclaw] Instructions already in AGENTS.md\"\nfi\n# ── Verification: confirm plugin is actually installed ──\nif [[ \"$OV_PLUGIN_INSTALLED\" = \"1\" ]]; then\n if ! \"$NODE\" \"$CLI\" plugins list 2>/dev/null | grep -q \"openviking\"; then\n echo \"[openclaw] ERROR: Plugin install reported success but plugins list does not contain openviking\"\n OV_PLUGIN_INSTALLED=0\n fi\nfi\nif [[ \"$OV_PLUGIN_INSTALLED\" = \"0\" ]]; then\n echo \"[openclaw] ERROR: OpenViking plugin installation FAILED — auto-recall and memory tools will not be available\"\nfi\n\"\"\"\n block = block.replace(\"__ENDPOINT__\", endpoint)\n block = block.replace(\"OV_FORCE_SLOT_DEFAULT\", str(force_slot))\n block = block.replace(\"OV_ALLOW_OFFLINE_DEFAULT\", str(allow_offline))\n api_key = os.environ.get(\"OV_API_KEY\", \"\")\n block = block.replace(\"__API_KEY__\", api_key)\n if api_key:\n _ov_mark_key_consumed()\n block = block.replace(\"__OV_NPM_REGISTRY__\", npm_registry)\n block = block.replace(\"__OV_RUNTIME_DIR__\", runtime_dir)\n\n init_path = f\"{shared_dir}/ov-openclaw-init.sh\"\n write_init(\n init_path, \"OpenClaw\",\n f\"Sourced by {template_dir}/openclaw/start.sh (single source line).\",\n block,\n )\n\n # Ensure OPENCLAW_STATE_DIR is persistent (not ephemeral /tmp).\n # Only rewrite if the old /tmp/.openclaw default is present;\n # skip if start.sh already uses dynamic derivation (${OPENCLAW_DIR}/state).\n if re.search(r'OPENCLAW_STATE_DIR.*(/tmp/\\.|\\$\\{OPENCLAW_STATE_DIR:-/tmp)', content):\n content = re.sub(\n r'# ── State dir:.*──\\nexport OPENCLAW_STATE_DIR=.*',\n '# ── State dir: prefer env.yaml extraEnv, fallback to ${OPENCLAW_DIR}/state ──\\n'\n '# Persistent path survives sandbox restarts; /tmp (old default) is ephemeral tmpfs.\\n'\n 'export OPENCLAW_STATE_DIR=\"${OPENCLAW_STATE_DIR:-${OPENCLAW_DIR}/state}\"',\n content,\n )\n print(\"start.sh state dir rewritten to dynamic persistent path\")\n else:\n print(\"start.sh state dir already persistent (dynamic or explicit) — skip\")\n\n # Also fix env.yaml so CLI commands in terminal use the same persistent path\n env_yaml = f\"{template_dir}/openclaw/env.yaml\"\n if os.path.exists(env_yaml):\n ey = read_text(env_yaml)\n if \"/tmp/.openclaw\" in ey:\n ey = ey.replace(\n 'OPENCLAW_STATE_DIR: \"/tmp/.openclaw\"',\n f'OPENCLAW_STATE_DIR: \"{runtime_dir}/openclaw/state\"',\n )\n write_text(env_yaml, ey)\n print(\"env.yaml fixed\")\n else:\n print(\"env.yaml already fixed\")\n\n sb = source_block(\n shared_dir, \"ov-openclaw-init.sh\",\n extra=\"# OpenViking plugin install is best-effort: if it fails, gateway should still start.\\nset +e\",\n )\n\n gateway_marker = \"# ── Step 5: Start the Gateway\"\n if gateway_marker in content:\n content = content.replace(gateway_marker, sb + gateway_marker)\n else:\n content = re.sub(r'(# ── Step \\d+: Start the Gateway)', sb + r'\\1', content)\n write_text(path, content)\n print(\"injected\")\n\n\n# ── clean-legacy-cfg ──────────────────────────────────────────\ndef cmd_clean_legacy_cfg(args):\n \"\"\"Remove legacy direct-config-write Step 5 block from template.\n\n Replaces the PYCLEANCFG heredoc (lines 341-352) in openclaw.sh.\n \"\"\"\n path = args[0]\n content = read_text(path)\n content = re.sub(\n r'# ── Step 5: OpenViking plugin config \\(added by huawei-cloud-openviking-agent-integration skill\\) ──.*?OVAGENTS\\n',\n '',\n content,\n flags=re.DOTALL,\n )\n write_text(path, content)\n\n\n# ── clean-legacy-mcp ──────────────────────────────────────────\ndef cmd_clean_legacy_mcp(args):\n \"\"\"Remove legacy MCP injection Step 5 block from template.\n\n Replaces the PYCLEANMCP heredoc (lines 356-367) in openclaw.sh.\n \"\"\"\n path = args[0]\n content = read_text(path)\n content = re.sub(\n r'# ── Step 5: Inject OpenViking MCP server.*?OVPATCH\\n',\n '',\n content,\n flags=re.DOTALL,\n )\n write_text(path, content)\n\n\n# ── has-mcp-server ────────────────────────────────────────────\ndef cmd_has_mcp_server(args):\n \"\"\"Exit 0 if 'openviking' in mcp.servers, else exit 1.\n\n Replaces the inline ``python3 -c`` call at line 377 of openclaw.sh.\n \"\"\"\n cfg_file = args[0]\n d = load_json(cfg_file)\n sys.exit(0 if \"openviking\" in d.get(\"mcp\", {}).get(\"servers\", {}) else 1)\n\n\n# ── remove-mcp-server ─────────────────────────────────────────\ndef cmd_remove_mcp_server(args):\n \"\"\"Remove openviking from mcp.servers and save.\n\n Replaces the inline ``python3 -c`` call at lines 378-387 of openclaw.sh.\n \"\"\"\n cfg_file = args[0]\n d = load_json(cfg_file)\n servers = d.get(\"mcp\", {}).get(\"servers\", {})\n if \"openviking\" in servers:\n del servers[\"openviking\"]\n if not servers:\n d.get(\"mcp\", {}).pop(\"servers\", None)\n if not d.get(\"mcp\"):\n d.pop(\"mcp\", None)\n save_json(cfg_file, d)\n\n\n# ── has-config ────────────────────────────────────────────────\ndef cmd_has_config(args):\n \"\"\"Exit 0 if openviking is configured (plugin entry, slot, or mcp server).\n\n Replaces the inline ``python3 -c`` call at line 405 of openclaw.sh.\n \"\"\"\n cfg_file = args[0]\n d = load_json(cfg_file)\n ok = (\n d.get(\"plugins\", {}).get(\"entries\", {}).get(\"openviking\")\n or d.get(\"plugins\", {}).get(\"slots\", {}).get(\"contextEngine\") == \"openviking\"\n or \"openviking\" in d.get(\"mcp\", {}).get(\"servers\", {})\n )\n sys.exit(0 if ok else 1)\n\n\n# ── unbind ────────────────────────────────────────────────────\ndef cmd_unbind(args):\n \"\"\"Remove all OpenViking injection blocks from template start.sh.\n\n Replaces the PYUNBIND heredoc (lines 421-512) in openclaw.sh.\n \"\"\"\n path = args[0]\n shared_dir = args[1]\n content = read_text(path)\n\n # First try new-style: source block with start/end markers (flexible content between)\n new_pattern = (\n rf'# ── OpenViking integration \\(added by huawei-cloud-openviking-agent-integration skill\\) ──\\n'\n rf'.*?' # Match any content between start and end markers\n rf'# ── End OpenViking integration ──\\n\\n?'\n )\n new_content = re.sub(new_pattern, '', content, flags=re.DOTALL)\n if new_content != content:\n write_text(path, new_content)\n print(\"removed 1 OpenViking block(s) [new-style source line]\")\n sys.exit(0)\n\n # Fall back to old-style: full inline Step 5.x blocks\n lines = content.splitlines(keepends=True)\n n = len(lines)\n removed_blocks = 0\n ov_block_start = re.compile(r'# ── Step 5(?:\\.\\d+)?:.*[Oo]pen[Vv]iking')\n any_step_header = re.compile(r'# ── Step \\d')\n ov_ref = re.compile(\n r'openviking|OV_PLUGIN|OV_VENDOR|OV_BUILD|clawhub:@openviking|accept-capabilities|acknowledge-clawhub-risk',\n re.IGNORECASE,\n )\n\n def find_step_end(start, lines):\n j = start + 1\n while j \u003c len(lines):\n if any_step_header.match(lines[j]) and j > start:\n return j\n if 'Starting Gateway' in lines[j] or 'gateway run' in lines[j]:\n return j\n j += 1\n return len(lines)\n\n def has_ov_in_range(lines, start, end):\n for j in range(start, end):\n if ov_ref.search(lines[j]):\n return True\n return False\n\n # Pass 1: Remove Step 5.x OpenViking blocks with look-ahead\n i = 0\n new_lines = []\n while i \u003c n:\n line = lines[i]\n if ov_block_start.match(line):\n removed_blocks += 1\n block_end = find_step_end(i, lines)\n i = block_end\n while i \u003c n:\n if any_step_header.match(lines[i]):\n step_end = find_step_end(i, lines)\n if has_ov_in_range(lines, i, step_end):\n removed_blocks += 1\n i = step_end\n continue\n else:\n break\n elif 'Starting Gateway' in lines[i] or 'gateway run' in lines[i]:\n break\n else:\n if ov_ref.search(lines[i]):\n i += 1\n continue\n stripped = lines[i].strip()\n if stripped == 'fi' or stripped == ' fi':\n i += 1\n continue\n break\n continue\n new_lines.append(line)\n i += 1\n\n # Pass 2: Remove orphaned fi statements\n lines = new_lines\n new_lines = []\n fi_balance = 0\n for line in lines:\n stripped = line.strip()\n is_bash_if = re.match(r'if\\s', stripped) or re.match(r'if\\s', stripped.split('#')[0].strip())\n is_bash_fi = stripped == 'fi' or stripped.startswith('fi ') or stripped.startswith('fi\\t')\n if is_bash_if:\n fi_balance += 1\n new_lines.append(line)\n elif is_bash_fi:\n if fi_balance > 0:\n fi_balance -= 1\n new_lines.append(line)\n else:\n continue\n else:\n new_lines.append(line)\n content = ''.join(new_lines)\n content = content.replace(\"# ── Step 6: Start the Gateway\", \"# ── Step 5: Start the Gateway\")\n write_text(path, content)\n print(f\"removed {removed_blocks} OpenViking block(s)\")\n\n\n# ── clean-config ──────────────────────────────────────────────\ndef cmd_clean_config(args):\n \"\"\"Remove openviking plugin entries, MCP servers, slots, and tool policy.\n\n Replaces the inline ``python3 -c`` call at lines 555-593 of openclaw.sh.\n Prints 'cleaned' if changes were made, 'skip' otherwise.\n \"\"\"\n cfg_file = args[0]\n d = load_json(cfg_file)\n changed = False\n servers = d.get(\"mcp\", {}).get(\"servers\", {})\n if \"openviking\" in servers:\n del servers[\"openviking\"]\n if not servers:\n d.get(\"mcp\", {}).pop(\"servers\", None)\n if not d.get(\"mcp\"):\n d.pop(\"mcp\", None)\n changed = True\n plugins = d.get(\"plugins\", {})\n entries = plugins.get(\"entries\", {})\n if \"openviking\" in entries:\n del entries[\"openviking\"]\n changed = True\n slots = plugins.get(\"slots\", {})\n if slots.get(\"contextEngine\") == \"openviking\":\n del slots[\"contextEngine\"]\n changed = True\n allow = plugins.get(\"allow\", [])\n if \"openviking\" in allow:\n allow.remove(\"openviking\")\n changed = True\n if not entries:\n plugins.pop(\"entries\", None)\n if not slots:\n plugins.pop(\"slots\", None)\n if not allow:\n plugins.pop(\"allow\", None)\n if not plugins:\n d.pop(\"plugins\", None)\n aa = d.get(\"tools\", {}).get(\"alsoAllow\", [])\n aa = [x for x in aa if x != \"group:plugins\"]\n if aa:\n d.setdefault(\"tools\", {})[\"alsoAllow\"] = aa\n else:\n d.get(\"tools\", {}).pop(\"alsoAllow\", None)\n if not d.get(\"tools\"):\n d.pop(\"tools\", None)\n if changed:\n save_json(cfg_file, d)\n print(\"cleaned\")\n else:\n print(\"skip\")\n\n\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"slot-owner\": cmd_slot_owner,\n \"inject\": cmd_inject,\n \"clean-legacy-cfg\": cmd_clean_legacy_cfg,\n \"clean-legacy-mcp\": cmd_clean_legacy_mcp,\n \"has-mcp-server\": cmd_has_mcp_server,\n \"remove-mcp-server\": cmd_remove_mcp_server,\n \"has-config\": cmd_has_config,\n \"unbind\": cmd_unbind,\n \"clean-config\": cmd_clean_config,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":23281,"content_sha256":"50e93187a08fbef39ec6e3a7b905d1db2dc49d19725a3bf25cc547b2aec08938"},{"filename":"scripts/py/agents/ov_opencode.py","content":"#!/usr/bin/env python3\n\"\"\"ov_opencode.py — OpenCode agent Python operations CLI.\n\nReplaces all heredoc Python and inline ``python3 -c`` calls in\nagents/opencode.sh.\n\nSubcommands:\n register-plugin \u003ccf> \u003cplugin_ver> add @openviking/opencode-plugin to opencode.json\n fix-env-yaml \u003cenv_yaml> add nodejs paths to env.yaml sandbox config\n integrate-template \u003ctpl> \u003csdk_ver> \u003cplugin_ver> \u003cnpm_registry> \u003cplugin_cache_dir> \u003cshared_dir> \u003ctemplate_dir>\n write init script + inject source block into template\n check-plugin \u003ccf> exit 0 if @openviking/opencode-plugin present\n unbind-template \u003ctpl> remove OpenViking integration block from template\n restore-env-yaml \u003cenv_yaml> remove nodejs paths from env.yaml\n unbind-live-config \u003ccf> remove OpenViking plugin + mcp + prompt from opencode.json\n\"\"\"\nimport json\nimport os\nimport re\nimport sys\n\n_HERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, _HERE)\nsys.path.insert(0, os.path.dirname(_HERE)) # parent dir holds ov_common.py\nfrom ov_common import (\n read_text,\n write_text,\n write_init,\n load_json,\n)\n\n\n# ── register-plugin (PYREG heredoc) ───────────────────────────\ndef cmd_register_plugin(args):\n path, ver = args[0], args[1]\n cfg = load_json(path)\n plugins = cfg.setdefault(\"plugin\", [])\n plugin_spec = f\"@openviking/opencode-plugin@{ver}\"\n if plugin_spec not in plugins:\n if \"@openviking/opencode-plugin\" in plugins:\n plugins.remove(\"@openviking/opencode-plugin\")\n plugins.append(plugin_spec)\n if \"mcp\" in cfg and \"openviking\" in cfg[\"mcp\"]:\n del cfg[\"mcp\"][\"openviking\"]\n if not cfg[\"mcp\"]:\n del cfg[\"mcp\"]\n if \"agent\" in cfg and \"build\" in cfg[\"agent\"] and \"prompt\" in cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"][\"prompt\"]\n if not cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"]\n if not cfg[\"agent\"]:\n del cfg[\"agent\"]\n with open(path, \"w\") as f:\n json.dump(cfg, f, indent=2, ensure_ascii=False)\n\n\n# ── fix-env-yaml (YAMLFIX heredoc) ────────────────────────────\ndef cmd_fix_env_yaml(args):\n path = args[0]\n yaml = read_text(path)\n changed = False\n if \"/usr/local/nodejs\" not in yaml:\n yaml = re.sub(\n r'(readablePaths:\\n(?:\\s+- \\S+\\n)*?)((?:\\s*\\S))',\n lambda m: m.group(1) + \" - /usr/local/nodejs\\n\" + m.group(2),\n yaml, count=1)\n changed = True\n if \"PATH:\" not in yaml or \"/usr/local/nodejs/bin\" not in yaml:\n path_line = ' PATH: \"/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\\n'\n yaml = re.sub(r'(extraEnv:\\n)', lambda m: m.group(1) + path_line, yaml, count=1)\n changed = True\n if changed:\n write_text(path, yaml)\n print(\"env.yaml updated: added nodejs paths\")\n else:\n print(\"env.yaml already has nodejs paths\")\n\n\n# ── integrate-template (PYTPL heredoc) ────────────────────────\ndef cmd_integrate_template(args):\n tpl_path = args[0]\n sdk_ver = args[1]\n plugin_ver = args[2]\n npm_registry = args[3]\n plugin_cache_dir = args[4]\n shared_dir = args[5]\n template_dir = args[6]\n tpl = read_text(tpl_path)\n block = \"\"\"\nexport NPM_CONFIG_REGISTRY=__OV_NPM_REGISTRY__\nexport NPM_CONFIG_PREFER_OFFLINE=true\nexport OPENCODE_DISABLE_AUTOUPDATE=true\nexport OPENCODE_FAST_BOOT=true\nexport OPENCODE_DISABLE_LSP_DOWNLOAD=true\nexport OPENCODE_DISABLE_DEFAULT_PLUGINS=true\ncat > \"$HOME/.npmrc\" \u003c\u003c'NPMRC'\nregistry=__OV_NPM_REGISTRY__\noffline=true\nprefer-offline=true\nNPMRC\nOV_DEPS_CACHE=\"__OV_SHARED_DIR__/opencode-deps\"\nOV_NPM_CACHE=\"__OV_SHARED_DIR__/opencode-npm-cache\"\nOV_OC_DIR=\"$HOME/.opencode\"\nOV_CACHE_OK=false\nif [[ ! -d \"$OV_OC_DIR/node_modules/@opencode-ai/plugin\" ]] && \\\\\n [[ -d \"$OV_DEPS_CACHE/node_modules/@opencode-ai/plugin\" ]]; then\n mkdir -p \"$OV_OC_DIR\"\n cp -a \"$OV_DEPS_CACHE/node_modules\" \"$OV_OC_DIR/node_modules\"\n cp -a \"$OV_DEPS_CACHE/package.json\" \"$OV_OC_DIR/package.json\"\n cp -a \"$OV_DEPS_CACHE/package-lock.json\" \"$OV_OC_DIR/package-lock.json\"\n OV_CACHE_OK=true\nfi\nif [[ ! -d \"$HOME/.npm/_cacache\" ]] && [[ -d \"$OV_NPM_CACHE/_cacache\" ]]; then\n mkdir -p \"$HOME/.npm\"\n cp -a \"$OV_NPM_CACHE/_cacache\" \"$HOME/.npm/_cacache\"\n OV_CACHE_OK=true\nfi\nif [[ \"$OV_CACHE_OK\" == \"true\" ]]; then\n export NPM_CONFIG_OFFLINE=true\nfi\nOV_CFG_NM=\"$HOME/.config/opencode/node_modules\"\nOV_CFG_NM_CACHE=\"__OV_SHARED_DIR__/opencode-config-nm/node_modules\"\nOV_CFG_NM_RESTORED=false\nif [[ ! -d \"$OV_CFG_NM/@opencode-ai/plugin\" ]] && [[ -d \"$OV_CFG_NM_CACHE/@opencode-ai/plugin\" ]]; then\n rm -rf \"$OV_CFG_NM\"\n mkdir -p \"$(dirname \"$OV_CFG_NM\")\"\n cp -a \"$OV_CFG_NM_CACHE\" \"$OV_CFG_NM\"\n OV_CFG_NM_RESTORED=true\n OV_CACHE_OK=true\n export NPM_CONFIG_OFFLINE=true\nfi\nOV_NPM_DIR=\"$HOME/.config/opencode\"\nmkdir -p \"$OV_NPM_DIR/node_modules/@openviking\"\nOV_PLUGIN_DST=\"$OV_NPM_DIR/node_modules/@openviking/opencode-plugin\"\nOV_PLUGIN_CACHE=\"__OV_PLUGIN_CACHE_DIR__/@openviking/opencode-plugin\"\nOV_SDK_CACHE=\"__OV_PLUGIN_CACHE_DIR__/@opencode-ai\"\nOV_PLUGIN_SOURCE=\"none\"\nif [[ -d \"$OV_PLUGIN_DST\" && -f \"$OV_PLUGIN_DST/package.json\" ]]; then\n OV_PLUGIN_SOURCE=\"existing\"\nfi\nif [[ \"$OV_PLUGIN_SOURCE\" == \"none\" && -d \"$OV_PLUGIN_CACHE\" && -f \"$OV_PLUGIN_CACHE/package.json\" ]]; then\n rm -rf \"$OV_PLUGIN_DST\"\n cp -a \"$OV_PLUGIN_CACHE\" \"$OV_PLUGIN_DST\"\n OV_PLUGIN_SOURCE=\"cache\"\n if [[ ! -d \"$OV_NPM_DIR/node_modules/@opencode-ai\" && -d \"$OV_SDK_CACHE\" ]]; then\n cp -a \"$OV_SDK_CACHE\" \"$OV_NPM_DIR/node_modules/@opencode-ai\"\n fi\nfi\nif [[ \"$OV_PLUGIN_SOURCE\" == \"none\" ]]; then\n if command -v npm &>/dev/null; then\n cat > \"$OV_NPM_DIR/package.json\" \u003c\u003c'PKGEOF'\n{\"dependencies\":{\"@opencode-ai/plugin\":\"__OV_PLUGIN_SDK_VER__\",\"@openviking/opencode-plugin\":\"__OV_OPENCODE_PLUGIN_VER__\"}}\nPKGEOF\n if (cd \"$OV_NPM_DIR\" && npm install --registry=__OV_NPM_REGISTRY__ --no-audit --no-fund 2>&1 | tail -5) && \\\\\n [[ -d \"$OV_PLUGIN_DST\" ]]; then\n OV_PLUGIN_SOURCE=\"npm\"\n mkdir -p \"$(dirname \"$OV_PLUGIN_CACHE\")\"\n rm -rf \"$OV_PLUGIN_CACHE\"\n cp -a \"$OV_PLUGIN_DST\" \"$OV_PLUGIN_CACHE\"\n touch \"$OV_PLUGIN_CACHE\"\n if [[ -d \"$OV_NPM_DIR/node_modules/@opencode-ai\" ]]; then\n rm -rf \"$OV_SDK_CACHE\"\n cp -a \"$OV_NPM_DIR/node_modules/@opencode-ai\" \"$OV_SDK_CACHE\"\n fi\n else\n echo \"WARN: npm install failed — plugin will not load\"\n fi\n else\n echo \"WARN: npm not found and no cache available — plugin will not load\"\n fi\nfi\nif [[ -d \"$OV_PLUGIN_DST\" && -f \"$OV_PLUGIN_DST/package.json\" ]]; then\n OV_CACHE_PKG=\"$HOME/.cache/opencode/packages/@openviking/opencode-plugin@latest\"\n if [[ ! -d \"$OV_CACHE_PKG/node_modules/@openviking/opencode-plugin\" ]]; then\n mkdir -p \"$OV_CACHE_PKG/node_modules/@openviking\"\n cp -a \"$OV_PLUGIN_DST\" \"$OV_CACHE_PKG/node_modules/@openviking/opencode-plugin\"\n OV_CACHE_VER=\"$HOME/.cache/opencode/packages/@openviking/opencode-plugin@__OV_OPENCODE_PLUGIN_VER__\"\n if [[ ! -d \"$OV_CACHE_VER/node_modules/@openviking/opencode-plugin\" ]]; then\n cp -a \"$OV_CACHE_PKG\" \"$OV_CACHE_VER\"\n fi\n fi\nfi\n_ov_py=\"\"\nfor _py in python3.12 python3.11 python3.10 python3; do command -v \"$_py\" >/dev/null 2>&1 && _ov_py=\"$_py\" && break; done\n: \"${_ov_py:=python3}\"\nif [[ -f \"$CONFIG_FILE\" ]]; then\n $_ov_py - \"$CONFIG_FILE\" \u003c\u003c'OVREG'\nimport json,sys; path=sys.argv[1]; cfg=json.load(open(path)); changed=False\nplugins=cfg.setdefault(\"plugin\",[]); plugin_spec=\"@openviking/opencode-plugin@__OV_OPENCODE_PLUGIN_VER__\"\nif plugin_spec not in plugins:\n if \"@openviking/opencode-plugin\" in plugins: plugins.remove(\"@openviking/opencode-plugin\")\n plugins.append(plugin_spec); changed=True\nif \"mcp\" in cfg and \"openviking\" in cfg[\"mcp\"]:\n del cfg[\"mcp\"][\"openviking\"]\n if not cfg[\"mcp\"]: del cfg[\"mcp\"]\n changed=True\nif \"agent\" in cfg and \"build\" in cfg[\"agent\"] and \"prompt\" in cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"][\"prompt\"]\n if not cfg[\"agent\"][\"build\"]: del cfg[\"agent\"][\"build\"]\n if not cfg[\"agent\"]: del cfg[\"agent\"]\n changed=True\nif changed: json.dump(cfg,open(path,\"w\"),indent=2,ensure_ascii=False)\nOVREG\nfi\nOV_CONF_DIR=\"$HOME/.config/opencode\"\nmkdir -p \"$OV_CONF_DIR\"\nif [[ ! -f \"$OV_CONF_DIR/openviking-config.json\" ]]; then\n cat > \"$OV_CONF_DIR/openviking-config.json\" \u003c\u003c'OVCONF'\n{\"enabled\":true,\"timeoutMs\":30000,\"repoContext\":{\"enabled\":true,\"cacheTtlMs\":60000},\"autoRecall\":{\"enabled\":true,\"limit\":10,\"scoreThreshold\":0.35,\"maxContentChars\":500,\"preferAbstract\":true,\"tokenBudget\":2000,\"minQueryLength\":3},\"recallLimit\":15,\"recallMaxContentChars\":20000,\"commitTokenThreshold\":20000,\"commitKeepRecentCount\":10,\"profileTokenBudget\":10000,\"resumeContextBudget\":32000}\nOVCONF\nfi\nif [[ \"${OV_CFG_NM_RESTORED:-false}\" != \"true\" ]]; then\n nohup bash -c 'sleep 25; _py=\"\"; for p in python3.12 python3.11 python3.10 python3; do command -v \"$p\" >/dev/null 2>&1 && _py=\"$p\" && break; done; : \"${_py:=python3}\"; SRC=\"$HOME/.config/opencode/node_modules\"; DST=\"__OV_SHARED_DIR__/opencode-config-nm/node_modules\"; if [[ -d \"$SRC/@opencode-ai/plugin\" ]]; then cur=$(\"$_py\" -c \"import json;print(json.load(open(\\\"$SRC/@opencode-ai/plugin/package.json\\\"))[\\\"version\\\"])\" 2>/dev/null); old=$(\"$_py\" -c \"import json;print(json.load(open(\\\"$DST/@opencode-ai/plugin/package.json\\\"))[\\\"version\\\"])\" 2>/dev/null); if [[ \"$cur\" != \"$old\" ]]; then TMP=\"__OV_SHARED_DIR__/opencode-config-nm.tmp.$\"; rm -rf \"$TMP\"; mkdir -p \"$TMP\"; cp -a \"$SRC\" \"$TMP/node_modules\"; rm -rf \"__OV_SHARED_DIR__/opencode-config-nm\"; mv \"$TMP\" \"__OV_SHARED_DIR__/opencode-config-nm\"; echo \"[ov-cache] Saved plugin deps v${cur} to shared cache.\" >> \"$HOME/.config/opencode/openviking/openviking-memory.log\"; fi; fi' > /dev/null 2>&1 &\nfi\n\"\"\"\n block = block.replace(\"__OV_NPM_REGISTRY__\", npm_registry)\n block = block.replace(\"__OV_PLUGIN_SDK_VER__\", sdk_ver)\n block = block.replace(\"__OV_OPENCODE_PLUGIN_VER__\", plugin_ver)\n block = block.replace(\"__OV_PLUGIN_CACHE_DIR__\", plugin_cache_dir)\n block = block.replace(\"__OV_SHARED_DIR__\", shared_dir)\n init_path = f\"{shared_dir}/ov-opencode-init.sh\"\n write_init(init_path, \"OpenCode\", f\"Sourced by {template_dir}/opencode/start.sh.\", block.lstrip(\"\\n\"))\n source_block_str = (\n \"# ── OpenViking integration (added by huawei-cloud-openviking-agent-integration skill) ──\\n\"\n f\"source {shared_dir}/ov-opencode-init.sh\\n\"\n \"# ── End OpenViking integration ──\\n\\n\"\n )\n markers = [\"export OPENCODE_SERVER_PASSWORD\", \"exec /root/runtime/opencode/opencode\"]\n idx = -1\n for marker in markers:\n idx = tpl.find(marker)\n if idx != -1:\n print(f\"Insertion point: {marker}\")\n break\n if idx == -1:\n print(\"ERROR: no insertion marker found (tried: \" + \", \".join(markers) + \")\", file=sys.stderr)\n sys.exit(1)\n tpl = tpl[:idx] + source_block_str + tpl[idx:]\n if \" --pure\" in tpl:\n tpl = tpl.replace(\" --pure\", \"\")\n print(\"Removed --pure flag (plugins now enabled)\")\n else:\n print(\"--pure flag not present, no change needed\")\n write_text(tpl_path, tpl)\n\n\n# ── check-plugin (inline python3 -c) ──────────────────────────\ndef cmd_check_plugin(args):\n path = args[0]\n cfg = load_json(path)\n sys.exit(0 if any(p.startswith(\"@openviking/opencode-plugin\") for p in cfg.get(\"plugin\", [])) else 1)\n\n\n# ── unbind-template (PYUNBIND heredoc) ────────────────────────\ndef cmd_unbind_template(args):\n path = args[0]\n content = read_text(path)\n marker = \"# ── OpenViking integration\"\n idx = content.find(marker)\n if idx != -1:\n end_marker = \"# ── End OpenViking integration ──\"\n end_idx = content.find(end_marker, idx)\n if end_idx != -1:\n # Remove from start marker to end of end marker line (+newline)\n end_pos = end_idx + len(end_marker)\n # Also consume trailing newline(s)\n while end_pos \u003c len(content) and content[end_pos] == '\\n':\n end_pos += 1\n content = content[:idx] + content[end_pos:]\n write_text(path, content)\n print(\"Removed OpenViking integration block from template\")\n else:\n print(\"WARNING: end marker not found, skipping template removal\")\n else:\n print(\"No OpenViking integration block found in template\")\n if \"opencode serve\" in content and \"--pure\" not in content:\n content = content.replace(\"opencode serve --port 14096 --hostname 127.0.0.1 --print-logs\",\n \"opencode serve --port 14096 --hostname 127.0.0.1 --print-logs --pure\")\n write_text(path, content)\n print(\"Restored --pure flag on exec line\")\n\n\n# ── restore-env-yaml (YAMLRESTORE heredoc) ────────────────────\ndef cmd_restore_env_yaml(args):\n path = args[0]\n yaml = read_text(path)\n changed = False\n if \"/usr/local/nodejs\" in yaml:\n yaml = re.sub(r'\\n\\s+- /usr/local/nodejs\\n', '\\n', yaml, count=1)\n changed = True\n if \"/usr/local/nodejs/bin\" in yaml:\n yaml = re.sub(r'\\n\\s+PATH: \"/usr/local/nodejs/bin:[^\"]*\"\\n', '\\n', yaml, count=1)\n changed = True\n if changed:\n write_text(path, yaml)\n print(\"env.yaml restored: removed nodejs paths\")\n else:\n print(\"env.yaml already clean\")\n\n\n# ── unbind-live-config (PYUNBIND2 heredoc) ────────────────────\ndef cmd_unbind_live_config(args):\n path = args[0]\n cfg = load_json(path)\n changed = False\n if \"plugin\" in cfg:\n removed = False\n for p in list(cfg[\"plugin\"]):\n if p.startswith(\"@openviking/opencode-plugin\"):\n cfg[\"plugin\"].remove(p)\n removed = True\n if removed:\n if not cfg[\"plugin\"]:\n del cfg[\"plugin\"]\n changed = True\n if \"mcp\" in cfg and \"openviking\" in cfg[\"mcp\"]:\n del cfg[\"mcp\"][\"openviking\"]\n if not cfg[\"mcp\"]:\n del cfg[\"mcp\"]\n changed = True\n if \"agent\" in cfg and \"build\" in cfg[\"agent\"] and \"prompt\" in cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"][\"prompt\"]\n if not cfg[\"agent\"][\"build\"]:\n del cfg[\"agent\"][\"build\"]\n if not cfg[\"agent\"]:\n del cfg[\"agent\"]\n changed = True\n if changed:\n with open(path, \"w\") as f:\n json.dump(cfg, f, indent=2, ensure_ascii=False)\n print(\"Removed OpenViking from opencode.json\")\n\n\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"register-plugin\": cmd_register_plugin,\n \"fix-env-yaml\": cmd_fix_env_yaml,\n \"integrate-template\": cmd_integrate_template,\n \"check-plugin\": cmd_check_plugin,\n \"unbind-template\": cmd_unbind_template,\n \"restore-env-yaml\": cmd_restore_env_yaml,\n \"unbind-live-config\": cmd_unbind_live_config,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15569,"content_sha256":"f087f395aa63d0461b91284fa4778ac6db1b8ea96faa5d09d7f265286f259269"},{"filename":"scripts/py/agents/ov_prime_agent.py","content":"#!/usr/bin/env python3\n\"\"\"ov_prime_agent.py — Prime Agent Python operations CLI.\n\nReplaces all heredoc Python in agents/prime_agent.sh.\n\nSubcommands:\n integrate \u003ctpl> \u003cshared_dir> write init script + inject source block into template\n unbind-template \u003ctpl> \u003cshared_dir> remove source block / inline block from template\n\"\"\"\nimport os\nimport re\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\nfrom ov_common import read_text, write_text, write_init, source_block\n\n\n# ── Extension block written into ov-prime-agent-init.sh ──────────\n# Shell variables ($PA_RUNTIME, $PRIME_AGENT_CODING_AGENT_DIR, ${OPENVIKING_URL:-...},\n# etc.) are intentional literals — they are expanded by bash at source time, not by Python.\nEXTENSION_BLOCK = \"\"\"\n# ── OpenViking memory extension (added by huawei-cloud-openviking-agent-integration skill) ──\n# @openviking/pi-coding-agent-extension: TypeScript extension with auto-recall, auto-capture,\n# context takeover, and 7 LLM tools. No build step, no MCP server — direct HTTP API.\nexport OPENVIKING_URL=\"${OPENVIKING_URL:-http://127.0.0.1:1933}\"\nOV_EXT_SRC=\"$PA_RUNTIME/openviking-extension\"\nOV_EXT_DST=\"$PRIME_AGENT_CODING_AGENT_DIR/extensions/openviking\"\nif [ -d \"$OV_EXT_SRC\" ]; then\n _ov_need_copy=false\n if [ ! -f \"$OV_EXT_DST/index.ts\" ] || ! diff -q \"$OV_EXT_SRC/index.ts\" \"$OV_EXT_DST/index.ts\" >/dev/null 2>&1; then\n _ov_need_copy=true\n fi\n if [ -d \"$OV_EXT_SRC/shared\" ] && [ ! -d \"$OV_EXT_DST/shared\" ]; then\n _ov_need_copy=true\n fi\n if [ \"$_ov_need_copy\" = true ]; then\n mkdir -p \"$OV_EXT_DST\"\n cp -a \"$OV_EXT_SRC\"/* \"$OV_EXT_DST/\"\n echo \"OpenViking extension deployed to $OV_EXT_DST\"\n else\n echo \"OpenViking extension already up-to-date\"\n fi\nelse\n echo \"WARN: extension source not found at $OV_EXT_SRC, skipping\"\nfi\n\"\"\"\n\n\ndef cmd_integrate(args):\n \"\"\"Write ov-prime-agent-init.sh and inject a source block into the template start.sh.\n\n Exits 0 early when the template already contains the integration markers.\n Exits 1 when no known anchor (agentwork / acpws) is found in the template.\n \"\"\"\n tpl_path, shared_dir = args[0], args[1]\n tpl = read_text(tpl_path)\n\n if \"ov-prime-agent-init.sh\" in tpl or \"OpenViking memory extension\" in tpl:\n sys.exit(0)\n\n init_path = f\"{shared_dir}/ov-prime-agent-init.sh\"\n write_init(\n init_path,\n \"Prime Agent\",\n \"Sourced by the Prime Agent template start.sh (single source line).\",\n EXTENSION_BLOCK.lstrip(\"\\n\"),\n )\n\n sb = source_block(shared_dir, \"ov-prime-agent-init.sh\")\n\n for anchor in (\"# ── agentwork:\", \"# ── acpws:\"):\n idx = tpl.find(anchor)\n if idx != -1:\n break\n else:\n print(\n \"ERROR: no known anchor found in template start.sh (tried agentwork, acpws)\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n tpl = tpl[:idx] + sb + tpl[idx:]\n write_text(tpl_path, tpl)\n\n\ndef cmd_unbind_template(args):\n \"\"\"Remove the OpenViking integration block from the template start.sh.\n\n Tries three patterns in order:\n 1. Source-line block (current integrate format).\n 2. Inline memory-extension block followed by a known anchor (older format).\n 3. Inline block followed by any comment line (fallback).\n \"\"\"\n path, shared_dir = args[0], args[1]\n content = read_text(path)\n removed = False\n\n # Pattern 1: Source-line block (current integrate format)\n source_block_pattern = (\n rf'# ── OpenViking integration \\(added by huawei-cloud-openviking-agent-integration skill\\) ──\\n'\n rf'source {shared_dir}/ov-prime-agent-init\\.sh\\n'\n r'# ── End OpenViking integration ──\\n\\n'\n )\n new_content = re.sub(source_block_pattern, '', content)\n if new_content != content:\n write_text(path, new_content)\n print(\"Source-line block removed from template start.sh\")\n content = new_content\n removed = True\n\n # Pattern 2: Inline memory-extension block (older integrate format)\n block_header = r'# ── OpenViking memory extension \\(added by huawei-cloud-openviking-agent-integration skill\\) ──'\n for anchor in (\"# ── agentwork:\", \"# ── acpws:\"):\n pattern = block_header + r'.*?\\n\\n(?=' + re.escape(anchor) + r')'\n new_content = re.sub(pattern, '', content, flags=re.DOTALL)\n if new_content != content:\n write_text(path, new_content)\n print(f\"Inline block removed from template start.sh (anchor: {anchor})\")\n content = new_content\n removed = True\n break\n\n if not removed:\n pattern = block_header + r'.*?\\n\\n(?=# ── )'\n new_content = re.sub(pattern, '', content, flags=re.DOTALL)\n if new_content != content:\n write_text(path, new_content)\n print(\"Inline block removed from template start.sh (fallback: next comment)\")\n removed = True\n\n if not removed:\n print(\"No OpenViking block found in template (already clean)\")\n\n\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"integrate\": cmd_integrate,\n \"unbind-template\": cmd_unbind_template,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5511,"content_sha256":"9d9a6d9cec8bbd3874cefba2a79939149c968fe4edae2d093ad898bad4bd0fb3"},{"filename":"scripts/py/agents/ov_workswarm.py","content":"#!/usr/bin/env python3\n\"\"\"ov_workswarm.py — WorkSwarm agent Python operations CLI.\n\nReplaces all heredoc Python and inline python3 -c calls in agents/workswarm.sh.\n\nSubcommands:\n mutate-config \u003capply|revert> \u003cconfig.yaml> [mcp_url]\n Delegate to ov_jw_mutate.py (single source of truth for config.yaml mutation).\n patch-iface-apply \u003cinterface_code.py>\n Add code-mode ExternalMemoryRail call after CodingMemoryRail registration.\n patch-iface-revert \u003cinterface_code.py>\n Remove the code-mode ExternalMemoryRail call.\n patch-prov-apply \u003cprovider.py>\n Patch openviking_memory_provider.py: top_k → limit.\n patch-prov-revert \u003cprovider.py>\n Revert the top_k → limit patch.\n patch-session-apply \u003cprovider.py>\n Patch openviking_memory_provider.py: generate jw- prefixed session ID.\n patch-session-revert \u003cprovider.py>\n Revert the jw- session ID patch.\n patch-commit-apply \u003cprovider.py>\n Patch openviking_memory_provider.py: call commit after sync_turn.\n patch-commit-revert \u003cprovider.py>\n Revert the commit-after-sync_turn patch.\n inject-template \u003ctpl> \u003cendpoint> \u003cmcp_url> \u003cagents_md_path> \u003cshared_dir> \u003ctemplate_dir> \u003cruntime_dir>\n Write ov-jiuwenswarm-init.sh and inject a source block into template start.sh.\n unbind-template \u003ctpl> \u003cshared_dir>\n Remove OpenViking integration blocks from template start.sh.\n\"\"\"\nimport os\nimport re\nimport subprocess\nimport sys\n\ndef _ov_mark_key_consumed():\n \"\"\"ISSUE-003: notify the parent bash entry point that --api-key was used.\"\"\"\n f = os.environ.get(\"OV_API_KEY_CONSUMED_FILE\")\n if not f:\n return\n try:\n os.makedirs(os.path.dirname(f), exist_ok=True)\n open(f, \"a\").close()\n except OSError:\n pass\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # parent dir holds ov_common.py\nfrom ov_common import read_text, write_text\n\n# Path to ov_jw_mutate.py — scripts/lib/ov_jw_mutate.py relative to scripts/py/agents/\n_HERE = os.path.dirname(os.path.abspath(__file__))\n_SCRIPTS_DIR = os.path.dirname(os.path.dirname(_HERE))\nOV_JW_MUTATE = os.path.join(_SCRIPTS_DIR, \"lib\", \"ov_jw_mutate.py\")\n\n\n# ── mutate-config: delegate to ov_jw_mutate.py ──────────────────\ndef cmd_mutate_config(args):\n \"\"\"Delegate to ov_jw_mutate.py — single source of truth for WorkSwarm config.yaml.\"\"\"\n action, cfg = args[0], args[1]\n mcp_url = args[2] if len(args) > 2 else \"\"\n cmd = [sys.executable, OV_JW_MUTATE, action, cfg]\n if mcp_url:\n cmd.append(mcp_url)\n result = subprocess.run(cmd)\n sys.exit(result.returncode)\n\n\n# ── patch-iface-apply / patch-iface-revert ──────────────────────\ndef cmd_patch_iface_apply(args):\n \"\"\"Add code-mode ExternalMemoryRail call to interface_code.py.\"\"\"\n path = args[0]\n content = read_text(path)\n pattern = r'( logger\\.info\\(\\n \"\\[JiuwenSwarmCodeAdapter\\] CodingMemoryRail \\(re\\)registered for %s\",\\n mode,\\n \\)\\n)\\n+( def _build_code_agent_rail)'\n replacement = r'\\1\\n # code-mode ExternalMemoryRail (openviking-agent-integration)\\n await self._handle_external_memory_rail_by_config()\\n\\n\\2'\n if re.search(pattern, content):\n content = re.sub(pattern, replacement, content, count=1)\n write_text(path, content)\n\n\ndef cmd_patch_iface_revert(args):\n \"\"\"Remove code-mode ExternalMemoryRail call from interface_code.py.\"\"\"\n path = args[0]\n content = read_text(path)\n content = re.sub(r'\\n # code-mode ExternalMemoryRail \\(openviking-agent-integration\\)\\n await self\\._handle_external_memory_rail_by_config\\(\\)\\n+', '\\n', content, flags=re.DOTALL)\n write_text(path, content)\n\n\n# ── patch-prov-apply / patch-prov-revert ────────────────────────\ndef cmd_patch_prov_apply(args):\n \"\"\"Patch openviking_memory_provider.py: top_k → limit.\"\"\"\n path = args[0]\n content = read_text(path)\n content = content.replace('{\"query\": query, \"top_k\": 5},', '{\"query\": query, \"limit\": 5}, # ov-fix-limit').replace('payload[\"top_k\"] = args[\"limit\"]', 'payload[\"limit\"] = args[\"limit\"] # ov-fix-limit')\n write_text(path, content)\n\n\ndef cmd_patch_prov_revert(args):\n \"\"\"Revert openviking_memory_provider.py: limit → top_k.\"\"\"\n path = args[0]\n content = read_text(path)\n content = content.replace('{\"query\": query, \"limit\": 5}, # ov-fix-limit', '{\"query\": query, \"top_k\": 5},').replace('payload[\"limit\"] = args[\"limit\"] # ov-fix-limit', 'payload[\"top_k\"] = args[\"limit\"]')\n write_text(path, content)\n\n\n# ── patch-session-apply / patch-session-revert ──────────────────\n_SESSION_OLD = 'self._session_id = kwargs.get(\"session_id\", \"\")\\n try:'\n_SESSION_NEW = (\n 'self._session_id = kwargs.get(\"session_id\", \"\")\\n'\n ' # ov-fix-session: generate jw- prefixed session ID for empty/__default__\\n'\n ' if not self._session_id or self._session_id == \"__default__\":\\n'\n ' import uuid; self._session_id = f\"jw-{uuid.uuid4()}\"\\n'\n ' try:'\n)\n# sync_turn must use self._session_id (set by initialize) instead of\n# kwargs[\"session_id\"] which is always \"__default__\" from the rail\n_SID_OLD = 'sid = kwargs.get(\"session_id\", self._session_id)'\n_SID_NEW = 'sid = self._session_id # ov-fix-session: use initialized session ID'\n\n\ndef cmd_patch_session_apply(args):\n \"\"\"Patch openviking_memory_provider.py: generate jw- session ID + use it in sync_turn.\"\"\"\n path = args[0]\n content = read_text(path)\n changed = False\n if _SESSION_OLD in content and 'ov-fix-session' not in content:\n content = content.replace(_SESSION_OLD, _SESSION_NEW, 1)\n changed = True\n if _SID_OLD in content and 'ov-fix-session: use initialized' not in content:\n content = content.replace(_SID_OLD, _SID_NEW, 1)\n changed = True\n if changed:\n write_text(path, content)\n\n\ndef cmd_patch_session_revert(args):\n \"\"\"Revert openviking_memory_provider.py: jw- session ID patch.\"\"\"\n path = args[0]\n content = read_text(path)\n changed = False\n if _SESSION_NEW in content:\n content = content.replace(_SESSION_NEW, _SESSION_OLD, 1)\n changed = True\n if _SID_NEW in content:\n content = content.replace(_SID_NEW, _SID_OLD, 1)\n changed = True\n if changed:\n write_text(path, content)\n\n\n# ── patch-commit-apply / patch-commit-revert ────────────────────\n# Strategy: threshold commit (every 5 turns) in sync_turn + session-end\n# commit in Rail.uninit() via provider.on_session_end().\n\n# --- Provider: sync_turn() threshold commit ---\n_COMMIT_ORIG = (\n ' )\\n'\n ' except Exception as e:\\n'\n ' logger.debug(\"OpenViking sync failed: %s\", e)'\n)\n_COMMIT_OLD_PER_TURN = (\n ' )\\n'\n ' # ov-fix-commit: trigger memory extraction after each turn\\n'\n ' await asyncio.to_thread(\\n'\n ' self._client.post, f\"/api/v1/sessions/{sid}/commit\", {}\\n'\n ' )\\n'\n ' except Exception as e:\\n'\n ' logger.debug(\"OpenViking sync failed: %s\", e)'\n)\n_COMMIT_NEW = (\n ' )\\n'\n ' # ov-fix-commit: threshold commit every 5 turns + session-end via uninit\\n'\n ' self._turn_count = getattr(self, \"_turn_count\", 0) + 1\\n'\n ' if self._turn_count >= 5:\\n'\n ' await asyncio.to_thread(\\n'\n ' self._client.post, f\"/api/v1/sessions/{sid}/commit\", {}\\n'\n ' )\\n'\n ' self._turn_count = 0\\n'\n ' except Exception as e:\\n'\n ' logger.debug(\"OpenViking sync failed: %s\", e)'\n)\n\n# --- Rail: uninit() — call on_session_end before shutdown ---\n_RAIL_UNINIT_OLD = (\n ' async def _shutdown_with_timeout():\\n'\n ' try:\\n'\n ' await asyncio.wait_for(self._provider.shutdown(), timeout=10.0)'\n)\n_RAIL_UNINIT_NEW = (\n ' async def _shutdown_with_timeout():\\n'\n ' # ov-fix-commit: commit session before shutdown\\n'\n ' try:\\n'\n ' await asyncio.wait_for(self._provider.on_session_end([]), timeout=10.0)\\n'\n ' except Exception:\\n'\n ' pass\\n'\n ' try:\\n'\n ' await asyncio.wait_for(self._provider.shutdown(), timeout=10.0)'\n)\n_RAIL_FALLBACK_OLD = (\n ' except RuntimeError:\\n'\n ' try:\\n'\n ' asyncio.run(self._provider.shutdown())'\n)\n_RAIL_FALLBACK_NEW = (\n ' except RuntimeError:\\n'\n ' try:\\n'\n ' asyncio.run(self._provider.on_session_end([]))\\n'\n ' except Exception:\\n'\n ' pass\\n'\n ' try:\\n'\n ' asyncio.run(self._provider.shutdown())'\n)\n\n\ndef _find_rail_path(provider_path: str) -> str:\n \"\"\"Find external_memory_rail.py from the provider path.\"\"\"\n import os\n # provider: .../openjiuwen/core/memory/external/openviking_memory_provider.py\n # rail: .../openjiuwen/harness/rails/memory/external_memory_rail.py\n pkg_root = os.path.dirname(os.path.dirname(os.path.dirname(\n os.path.dirname(provider_path)))) # .../openjiuwen\n rail = os.path.join(pkg_root, \"harness\", \"rails\", \"memory\",\n \"external_memory_rail.py\")\n return rail if os.path.isfile(rail) else \"\"\n\n\ndef cmd_patch_commit_apply(args):\n \"\"\"Patch provider sync_turn (threshold commit) + rail uninit (session-end commit).\"\"\"\n path = args[0]\n content = read_text(path)\n changed = False\n # Provider: replace per-turn commit or original with threshold commit\n if _COMMIT_OLD_PER_TURN in content:\n content = content.replace(_COMMIT_OLD_PER_TURN, _COMMIT_NEW, 1)\n changed = True\n elif _COMMIT_ORIG in content and 'ov-fix-commit' not in content:\n content = content.replace(_COMMIT_ORIG, _COMMIT_NEW, 1)\n changed = True\n if changed:\n write_text(path, content)\n # Rail: add on_session_end before shutdown\n rail = _find_rail_path(path)\n if rail:\n rc = read_text(rail)\n rchanged = False\n if _RAIL_UNINIT_OLD in rc and 'ov-fix-commit' not in rc:\n rc = rc.replace(_RAIL_UNINIT_OLD, _RAIL_UNINIT_NEW, 1)\n rchanged = True\n if _RAIL_FALLBACK_OLD in rc:\n rc = rc.replace(_RAIL_FALLBACK_OLD, _RAIL_FALLBACK_NEW, 1)\n rchanged = True\n if rchanged:\n write_text(rail, rc)\n\n\ndef cmd_patch_commit_revert(args):\n \"\"\"Revert provider + rail commit patches.\"\"\"\n path = args[0]\n content = read_text(path)\n changed = False\n if _COMMIT_NEW in content:\n content = content.replace(_COMMIT_NEW, _COMMIT_ORIG, 1)\n changed = True\n elif _COMMIT_OLD_PER_TURN in content:\n content = content.replace(_COMMIT_OLD_PER_TURN, _COMMIT_ORIG, 1)\n changed = True\n if changed:\n write_text(path, content)\n rail = _find_rail_path(path)\n if rail:\n rc = read_text(rail)\n rchanged = False\n if _RAIL_UNINIT_NEW in rc:\n rc = rc.replace(_RAIL_UNINIT_NEW, _RAIL_UNINIT_OLD, 1)\n rchanged = True\n if _RAIL_FALLBACK_NEW in rc:\n rc = rc.replace(_RAIL_FALLBACK_NEW, _RAIL_FALLBACK_OLD, 1)\n rchanged = True\n if rchanged:\n write_text(rail, rc)\n\n\n# ── inject-template ─────────────────────────────────────────────\ndef cmd_inject_template(args):\n \"\"\"Write ov-jiuwenswarm-init.sh and inject source block into template start.sh.\"\"\"\n path, endpoint, mcp_url, agents_md_path, shared_dir, template_dir, runtime_dir = (\n args[0], args[1], args[2], args[3], args[4], args[5], args[6]\n )\n with open(path) as f:\n lines = f.readlines()\n marker = \"# ── OpenViking native memory provider injection (added by huawei-cloud-openviking-agent-integration skill) ──\"\n agents_md = open(agents_md_path).read()\n block = marker + \"\"\"\nexport MEMORY_ENGINE=external\nexport MEMORY_EXTERNAL_PROVIDER=openviking\nexport OPENVIKING_ENDPOINT=\"__OV_EP__\"\nexport OPENVIKING_AGENT=jiuwenswarm\nOV_PY=\"${OV_PY:-$(command -v python3.12 2>/dev/null || command -v python3.11 2>/dev/null || command -v python3.10 2>/dev/null || command -v python3 2>/dev/null || echo python3)}\"\nJW_CFG=\"$JIUWENSWARM_DATA_DIR/config/config.yaml\"\nif [ -f \"$JW_CFG\" ]; then\n $OV_PY - \"$JW_CFG\" \"__OV_MCP__\" \u003c\u003c 'PYJW2'\nimport sys, re\npath = sys.argv[1]\nmcp_url = sys.argv[2]\nwith open(path) as f: content = f.read()\nchanged = False\nnew = re.sub(r'(engine:\\\\s*\\\\$\\\\{MEMORY_ENGINE:-)builtin(\\\\})', r'\\\\1external\\\\2', content)\nif new != content: content = new; changed = True\nnew = re.sub(r'(provider:\\\\s*\\\\$\\\\{MEMORY_EXTERNAL_PROVIDER:-)(\\\\})', r'\\\\1openviking\\\\2', content)\nif new != content: content = new; changed = True\nnew = re.sub(r'(auto_memory_enabled:\\\\s*)false', r'\\\\1true', content)\nif new != content: content = new; changed = True\nnew = re.sub(r'(proactive_recommendation:\\\\s*\\\\n\\\\s*enabled:\\\\s*)false', r'\\\\1true', content)\nif new != content: content = new; changed = True\nif \"name: openviking\" not in content:\n _dash_m = re.search(r'servers:[^\\\\n]*\\\\n([ \\t]*)-[ \\t]', content)\n _dash = _dash_m.group(1) if _dash_m else ' '\n _inner = _dash + ' '\n mcp_entry = _dash + '- name: openviking\\\\n' + _inner + 'transport: streamable-http\\\\n' + _inner + 'url: ' + mcp_url + '\\\\n' + _inner + 'enabled: true\\\\n'\n new = re.sub(r'(mcp:\\s*\\\\n\\s*servers:\\s*)\\[\\]', lambda m: m.group(1) + \"\\\\n\" + mcp_entry, content)\n if new != content: content = new; changed = True\n else:\n new = re.sub(r'(mcp:\\s*\\\\n\\s*servers:\\s*\\\\n)(?!\\s*-)', lambda m: m.group(1) + mcp_entry, content)\n if new != content: content = new; changed = True\n else:\n _blkre = re.compile(r'(mcp:\\s*\\\\n\\s*servers:\\\\n(?:[ \\t]*-[^\\\\n]*\\\\n(?:[ \\t]+[^\\\\n]*\\\\n)*)*)')\n _blkm = _blkre.search(content)\n if _blkm:\n content = content[:_blkm.end()] + mcp_entry + content[_blkm.end():]\n changed = True\nif \" openviking:\" not in content:\n ov_block = \" openviking:\\\\n endpoint: ${OPENVIKING_ENDPOINT:-http://127.0.0.1:1933}\\\\n api_key: ${OPENVIKING_API_KEY:-}\\\\n account: ${OPENVIKING_ACCOUNT:-default}\\\\n user: ${OPENVIKING_USER:-default}\\\\n\"\n new = re.sub(r'( external:\\\\s*\\\\n(?: [^\\\\n]*\\\\n)*?)( lakebase:)', lambda m: m.group(1) + ov_block + m.group(2), content)\n if new != content: content = new; changed = True\nnew = re.sub(r\"(account:\\\\s*\\\\$\\\\{OPENVIKING_ACCOUNT:-)root(\\\\})\", r\"\\\\1default\\\\2\", content)\nif new != content: content = new; changed = True\nif \"viking_search: allow\" not in content:\n viking_perms = \" viking_search: allow\\\\n viking_read: allow\\\\n viking_browse: allow\\\\n viking_remember: allow\\\\n viking_add_resource: allow\\\\n\"\n new = re.sub(r'( mem0_conclude: allow\\\\n)', lambda m: m.group(1) + viking_perms, content)\n if new != content: content = new; changed = True\nnew = re.sub(r'(fast:\\\\s*\\\\n\\\\s*memory:\\\\s*\\\\n\\\\s*enabled:\\\\s*true\\\\s*\\\\n\\\\s*is_proactive:\\\\s*)false', r'\\\\1true', content)\nif new != content: content = new; changed = True\nif re.search(r'(code:\\\\s*\\\\n\\\\s*memory:\\\\s*\\\\n\\\\s*enabled:\\\\s*true\\\\s*\\\\n)(?!\\\\s*is_proactive)', content):\n new = re.sub(r'(code:\\\\s*\\\\n\\\\s*memory:\\\\s*\\\\n\\\\s*enabled:\\\\s*true\\\\s*\\\\n)(?!\\\\s*is_proactive)', r'\\\\1 is_proactive: true\\\\n', content)\n if new != content: content = new; changed = True\nnew = re.sub(r'(memory:\\\\s*\\\\n\\\\s*enabled:\\\\s*)false(\\\\s*\\\\n\\\\s*scenario:)', r'\\\\1true\\\\2', content)\nif new != content: content = new; changed = True\nif changed:\n _cmre = re.search(r'mcp:\\s*\\\\n\\s*servers:\\s*\\\\n((?:[ \\t]*-[^\\\\n]*\\\\n(?:[ \\t]+[^\\\\n]*\\\\n)*)+)', content)\n if _cmre:\n _dashes = set(re.findall(r'^([ \\t]*)- ', _cmre.group(1), re.M))\n if len(_dashes) > 1:\n print('ERROR: mcp.servers indent inconsistent - aborting, file NOT written', file=sys.stderr)\n sys.exit(2)\n with open(path, 'w') as f: f.write(content)\n\nPYJW2\nfi\nJW_IFACE=$(find __OV_RT__/jiuwenswarm -path '*/agent_adapter/interface_code.py' ! -path '*__pycache__*' 2>/dev/null | head -1)\nif [ -n \"$JW_IFACE\" ] && ! grep -q 'code-mode ExternalMemoryRail' \"$JW_IFACE\" 2>/dev/null; then\n $OV_PY -c 'p=__import__(\"sys\").argv[1];c=open(p).read();m=\" def _build_code_agent_rail\";n=\"\\\\n # code-mode ExternalMemoryRail (openviking-agent-integration)\\\\n await self._handle_external_memory_rail_by_config()\\\\n\"+m;open(p,\"w\").write(c.replace(m,n,1)) if m in c else None' \"$JW_IFACE\" 2>/dev/null\n find __OV_RT__/jiuwenswarm -path '*__pycache__*interface_code*' -delete 2>/dev/null\nfi\nJW_PROV=$(find __OV_RT__/jiuwenswarm -path '*/memory/external/openviking_memory_provider.py' ! -path '*__pycache__*' 2>/dev/null | head -1)\nif [ -n \"$JW_PROV\" ] && ! grep -q 'ov-fix-limit' \"$JW_PROV\" 2>/dev/null; then\n $OV_PY -c 'p=__import__(\"sys\").argv[1];q=chr(34);c=open(p).read();c=c.replace(q+\"top_k\"+q+\": 5},\",q+\"limit\"+q+\": 5}, # ov-fix-limit\").replace(\"payload[\"+q+\"top_k\"+q+\"] = args[\"+q+\"limit\"+q+\"]\",\"payload[\"+q+\"limit\"+q+\"] = args[\"+q+\"limit\"+q+\"] # ov-fix-limit\");open(p,\"w\").write(c) if c!=open(p).read() else None' \"$JW_PROV\" 2>/dev/null\n find __OV_RT__/jiuwenswarm -path '*__pycache__*openviking_memory_provider*' -delete 2>/dev/null\nfi\nif [ -n \"$JW_PROV\" ] && ! grep -q 'ov-fix-session' \"$JW_PROV\" 2>/dev/null; then\n $OV_PY - \"$JW_PROV\" \u003c\u003c 'PYJW3'\nimport sys\np = sys.argv[1]\nc = open(p).read()\nchanged = False\n# Fix 1: initialize() — generate jw- session ID\nold1 = 'self._session_id = kwargs.get(\"session_id\", \"\")\\\\n try:'\nnew1 = ('self._session_id = kwargs.get(\"session_id\", \"\")\\\\n'\n ' # ov-fix-session: generate jw- prefixed session ID for empty/__default__\\\\n'\n ' if not self._session_id or self._session_id == \"__default__\":\\\\n'\n ' import uuid; self._session_id = f\"jw-{uuid.uuid4()}\"\\\\n'\n ' try:')\nif old1 in c:\n c = c.replace(old1, new1, 1); changed = True\n# Fix 2: sync_turn() — use self._session_id instead of kwargs override\nold2 = 'sid = kwargs.get(\"session_id\", self._session_id)'\nnew2 = 'sid = self._session_id # ov-fix-session: use initialized session ID'\nif old2 in c:\n c = c.replace(old2, new2, 1); changed = True\nif changed:\n open(p, 'w').write(c)\nPYJW3\n find __OV_RT__/jiuwenswarm -path '*__pycache__*openviking_memory_provider*' -delete 2>/dev/null\nfi\nif [ -n \"$JW_PROV\" ] && ! grep -q 'ov-fix-commit' \"$JW_PROV\" 2>/dev/null; then\n $OV_PY - \"$JW_PROV\" \u003c\u003c 'PYJW4'\nimport sys, os\np = sys.argv[1]\nc = open(p).read()\nchanged = False\n# Provider: threshold commit (every 5 turns) in sync_turn\nold_pt = (' )\\\\n'\n ' # ov-fix-commit: trigger memory extraction after each turn\\\\n'\n ' await asyncio.to_thread(\\\\n'\n ' self._client.post, f\"/api/v1/sessions/{sid}/commit\", {}\\\\n'\n ' )\\\\n'\n ' except Exception as e:\\\\n'\n ' logger.debug(\"OpenViking sync failed: %s\", e)')\nold_orig = (' )\\\\n'\n ' except Exception as e:\\\\n'\n ' logger.debug(\"OpenViking sync failed: %s\", e)')\nnew = (' )\\\\n'\n ' # ov-fix-commit: threshold commit every 5 turns + session-end via uninit\\\\n'\n ' self._turn_count = getattr(self, \"_turn_count\", 0) + 1\\\\n'\n ' if self._turn_count >= 5:\\\\n'\n ' await asyncio.to_thread(\\\\n'\n ' self._client.post, f\"/api/v1/sessions/{sid}/commit\", {}\\\\n'\n ' )\\\\n'\n ' self._turn_count = 0\\\\n'\n ' except Exception as e:\\\\n'\n ' logger.debug(\"OpenViking sync failed: %s\", e)')\nif old_pt in c:\n c = c.replace(old_pt, new, 1); changed = True\nelif old_orig in c:\n c = c.replace(old_orig, new, 1); changed = True\nif changed:\n open(p, 'w').write(c)\n# Rail: add on_session_end before shutdown in uninit()\npkg = os.path.dirname(os.path.dirname(os.path.dirname(\n os.path.dirname(p))))\nrail = os.path.join(pkg, \"harness\", \"rails\", \"memory\", \"external_memory_rail.py\")\nif os.path.isfile(rail):\n rc = open(rail).read()\n rchanged = False\n r_old1 = (' async def _shutdown_with_timeout():\\\\n'\n ' try:\\\\n'\n ' await asyncio.wait_for(self._provider.shutdown(), timeout=10.0)')\n r_new1 = (' async def _shutdown_with_timeout():\\\\n'\n ' # ov-fix-commit: commit session before shutdown\\\\n'\n ' try:\\\\n'\n ' await asyncio.wait_for(self._provider.on_session_end([]), timeout=10.0)\\\\n'\n ' except Exception:\\\\n'\n ' pass\\\\n'\n ' try:\\\\n'\n ' await asyncio.wait_for(self._provider.shutdown(), timeout=10.0)')\n r_old2 = (' except RuntimeError:\\\\n'\n ' try:\\\\n'\n ' asyncio.run(self._provider.shutdown())')\n r_new2 = (' except RuntimeError:\\\\n'\n ' try:\\\\n'\n ' asyncio.run(self._provider.on_session_end([]))\\\\n'\n ' except Exception:\\\\n'\n ' pass\\\\n'\n ' try:\\\\n'\n ' asyncio.run(self._provider.shutdown())')\n if r_old1 in rc and 'ov-fix-commit' not in rc:\n rc = rc.replace(r_old1, r_new1, 1); rchanged = True\n if r_old2 in rc:\n rc = rc.replace(r_old2, r_new2, 1); rchanged = True\n if rchanged:\n open(rail, 'w').write(rc)\n import glob\n for f in glob.glob(os.path.join(os.path.dirname(rail), '__pycache__', 'external_memory_rail.*.pyc')):\n os.remove(f)\nPYJW4\n find __OV_RT__/jiuwenswarm -path '*__pycache__*openviking_memory_provider*' -delete 2>/dev/null\n find __OV_RT__/jiuwenswarm -path '*__pycache__*external_memory_rail*' -delete 2>/dev/null\nfi\nmkdir -p /workspace\ncat > /workspace/AGENTS.md \u003c\u003c 'AGENTSMD'\n__AGENTS_MD__\nAGENTSMD\n\"\"\"\n block = block.replace(\"__AGENTS_MD__\", agents_md).replace(\"__OV_EP__\", endpoint).replace(\"__OV_MCP__\", mcp_url).replace(\"__OV_RT__\", runtime_dir)\n # ISSUE-003: when --api-key was passed, embed it literally into the generated\n # config template (was a dead parameter — the runtime env never carried it).\n api_key = os.environ.get(\"OV_API_KEY\", \"\")\n if api_key:\n block = block.replace(\"${OPENVIKING_API_KEY:-}\", api_key)\n _ov_mark_key_consumed()\n init_path = f\"{shared_dir}/ov-jiuwenswarm-init.sh\"\n with open(init_path, \"w\") as f:\n f.write(\"#!/usr/bin/env bash\\n# ── OpenViking integration for WorkSwarm ──\\n\")\n f.write(f\"# Sourced by {template_dir}/jiuwenswarm/start.sh (single source line).\\n\")\n f.write(\"# Managed by huawei-cloud-openviking-agent-integration skill.\\n\\n\")\n f.write(block)\n os.chmod(init_path, 0o755)\n source_block = f\"# ── OpenViking integration (added by huawei-cloud-openviking-agent-integration skill) ──\\nsource {shared_dir}/ov-jiuwenswarm-init.sh\\n# ── End OpenViking integration ──\\n\"\n inserted = False\n for i, line in enumerate(lines):\n if 'nohup' in line and 'jiuwenswarm-start' in line:\n lines.insert(i, source_block)\n inserted = True\n break\n if not inserted:\n for i, line in enumerate(lines):\n if line.strip() == 'sleep infinity' or line.strip().startswith('sleep infinity'):\n lines.insert(i, source_block)\n break\n with open(path, 'w') as f:\n f.writelines(lines)\n\n\n# ── unbind-template ─────────────────────────────────────────────\ndef cmd_unbind_template(args):\n \"\"\"Remove OpenViking integration blocks from template start.sh.\"\"\"\n path, shared_dir = args[0], args[1]\n content = read_text(path)\n new = re.sub(rf'# ── OpenViking integration \\(added by huawei-cloud-openviking-agent-integration skill\\) ──\\n.*?# ── End OpenViking integration ──\\n\\n?', '', content, flags=re.DOTALL)\n new = re.sub(r'# 3\\. Enhanced 4-section AGENTS\\.md.*?fi\\n', '', new, flags=re.DOTALL)\n new = re.sub(r'# ── OpenViking MCP injection.*?fi\\n', '', new, flags=re.DOTALL)\n new = re.sub(r'\\n# 4\\. openviking-config\\.json.*?fi\\n', '\\n', new, flags=re.DOTALL)\n new = re.sub(r\"# 3\\. Enhanced 4-section AGENTS\\.md.*?AGENTSMD\\n\", '', new, flags=re.DOTALL)\n new = re.sub(r'# ── OpenViking native memory provider injection.*?AGENTSMD\\n\\n?', '', new, flags=re.DOTALL)\n if new != content:\n write_text(path, new)\n\n\n# ── dispatch ────────────────────────────────────────────────────\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"mutate-config\": cmd_mutate_config,\n \"patch-iface-apply\": cmd_patch_iface_apply,\n \"patch-iface-revert\": cmd_patch_iface_revert,\n \"patch-prov-apply\": cmd_patch_prov_apply,\n \"patch-prov-revert\": cmd_patch_prov_revert,\n \"patch-session-apply\": cmd_patch_session_apply,\n \"patch-session-revert\": cmd_patch_session_revert,\n \"patch-commit-apply\": cmd_patch_commit_apply,\n \"patch-commit-revert\": cmd_patch_commit_revert,\n \"inject-template\": cmd_inject_template,\n \"unbind-template\": cmd_unbind_template,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":26423,"content_sha256":"986c835079b81c0c0e785d275c2c0215b55a40b8f3ed9b3af6937de9ee15c9d7"},{"filename":"scripts/py/ov_common.py","content":"#!/usr/bin/env python3\n\"\"\"ov_common.py — Shared utilities for all ov_*.py CLI helpers.\n\nEvery standalone Python script under scripts/py/ imports from this module to\navoid duplicating JSON / file / string helpers. The shell scripts call these\n.py files directly via ``\"$OV_PY\" \"$PY_DIR/ov_xxx.py\" \u003csubcommand> ...``.\n\"\"\"\nimport json\nimport os\n\n\n# ── JSON helpers ──────────────────────────────────────────────\ndef load_json(path):\n \"\"\"Load a JSON file and return the parsed object.\"\"\"\n with open(path, encoding=\"utf-8\") as f:\n return json.load(f)\n\n\ndef save_json(path, data, indent=2):\n \"\"\"Write *data* as JSON to *path* with a trailing newline.\n\n ISSUE-014: write to a temp file in the same directory and atomically rename\n — a crash mid-write can no longer leave a truncated/corrupt config.\n \"\"\"\n import tempfile\n\n tmp_fd, tmp_path = tempfile.mkstemp(\n dir=os.path.dirname(path) or \".\", prefix=os.path.basename(path) + \".\",\n suffix=\".tmp\", text=True,\n )\n try:\n with os.fdopen(tmp_fd, \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, indent=indent)\n f.write(\"\\n\")\n f.flush()\n os.fsync(f.fileno())\n os.replace(tmp_path, path)\n except BaseException:\n try:\n os.unlink(tmp_path)\n except OSError:\n pass\n raise\n\n\ndef dotted_get(cfg, key):\n \"\"\"Walk a dotted key path (``a.b.c``) through nested dicts.\n\n Returns ``None`` when any segment is missing.\n \"\"\"\n val = cfg\n for k in key.split(\".\"):\n if isinstance(val, dict) and k in val:\n val = val[k]\n else:\n return None\n return val\n\n\ndef dotted_set(cfg, key, value):\n \"\"\"Set a dotted key path, creating intermediate dicts as needed.\"\"\"\n keys = key.split(\".\")\n d = cfg\n for k in keys[:-1]:\n d = d.setdefault(k, {})\n d[keys[-1]] = value\n\n\ndef dotted_exists(cfg, key):\n \"\"\"Return True iff every segment of the dotted key exists.\"\"\"\n val = cfg\n for k in key.split(\".\"):\n if isinstance(val, dict) and k in val:\n val = val[k]\n else:\n return False\n return True\n\n\ndef dotted_remove(cfg, key):\n \"\"\"Remove a dotted key (no-op if missing).\"\"\"\n keys = key.split(\".\")\n d = cfg\n for k in keys[:-1]:\n if not isinstance(d, dict) or k not in d:\n return\n d = d[k]\n if isinstance(d, dict):\n d.pop(keys[-1], None)\n\n\n# ── Text helpers ──────────────────────────────────────────────\ndef read_text(path):\n with open(path, encoding=\"utf-8\") as f:\n return f.read()\n\n\ndef write_text(path, content):\n \"\"\"Write text atomically (ISSUE-014: tmp file + rename on same filesystem).\"\"\"\n import tempfile\n\n tmp_fd, tmp_path = tempfile.mkstemp(\n dir=os.path.dirname(path) or \".\", prefix=os.path.basename(path) + \".\",\n suffix=\".tmp\", text=True,\n )\n try:\n with os.fdopen(tmp_fd, \"w\", encoding=\"utf-8\") as f:\n f.write(content)\n f.flush()\n os.fsync(f.fileno())\n os.replace(tmp_path, path)\n except BaseException:\n try:\n os.unlink(tmp_path)\n except OSError:\n pass\n raise\n\n\n# ── Init-script generation (shared by all agent subclasses) ──\ndef write_init(init_path, agent_name, sourced_by, block):\n \"\"\"Write an init script with a standard header. Returns *init_path*.\"\"\"\n with open(init_path, \"w\") as f:\n f.write(\"#!/usr/bin/env bash\\n\")\n f.write(f\"# ── OpenViking integration for {agent_name} ──\\n\")\n f.write(f\"# {sourced_by}\\n\")\n f.write(\"# Managed by huawei-cloud-openviking-agent-integration skill.\\n\\n\")\n f.write(block)\n os.chmod(init_path, 0o755)\n return init_path\n\n\ndef source_block(shared_dir, init_filename, extra=\"\"):\n \"\"\"Return the source-block string for template insertion.\"\"\"\n _marker = os.environ.get(\n \"OV_MARKER\", \"added by huawei-cloud-openviking-agent-integration skill\"\n )\n lines = [\n f\"# ── OpenViking integration ({_marker}) ──\",\n ]\n if extra:\n lines.append(extra)\n lines.append(f\"source {shared_dir}/{init_filename}\")\n if extra:\n lines.append(\"set -e\")\n lines.append(\"# ── End OpenViking integration ──\")\n lines.append(\"\")\n return \"\\n\".join(lines) + \"\\n\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4567,"content_sha256":"012c34e49de9493b6656ece66cdd2b3fc590a60370245ef2406855e80c659755"},{"filename":"scripts/py/ov_health.py","content":"#!/usr/bin/env python3\n\"\"\"ov_health.py — OpenViking health-check and status helpers CLI.\n\nReplaces inline ``python3 -c`` calls in lib/base.sh and scripts/status.sh.\n\nSubcommands:\n parse-health \u003cjson-response> print \"status|version|auth_mode\"\n parse-field \u003cjson-response> \u003cfield> print a single field from health JSON\n\"\"\"\nimport json\nimport sys\nimport os\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n\n\ndef cmd_parse_health(args):\n \"\"\"Print status|version|auth_mode from a health endpoint JSON response.\"\"\"\n try:\n d = json.loads(args[0])\n except Exception:\n d = {}\n status = d.get(\"status\", \"unknown\")\n version = d.get(\"version\", \"unknown\")\n auth_mode = d.get(\"auth_mode\", \"unknown\")\n print(f\"{status}|{version}|{auth_mode}\")\n\n\ndef cmd_parse_field(args):\n \"\"\"Print a single field from a JSON string (stdin if first arg is '-').\"\"\"\n raw_arg, field = args[0], args[1]\n if raw_arg == \"-\":\n raw = sys.stdin.read()\n else:\n raw = raw_arg\n try:\n d = json.loads(raw)\n val = d.get(field, \"\")\n if val is None:\n val = \"\"\n print(val)\n except Exception:\n print(\"\")\n\n\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"parse-health\": cmd_parse_health,\n \"parse-field\": cmd_parse_field,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1542,"content_sha256":"4f6143712b2e5f1e95dab498e9c367ff9043726f08246e23398a07ed30ba6681"},{"filename":"scripts/py/ov_json.py","content":"#!/usr/bin/env python3\n\"\"\"ov_json.py — JSON config manipulation CLI.\n\nReplaces all inline ``python3 -c`` calls in lib/json.sh.\n\nSubcommands:\n check-mcp \u003cfile> exit 0 if mcp.openviking.enabled is true\n get-mcp-url \u003cfile> print the openviking MCP URL\n read \u003cfile> \u003cdotted.key> [default] print value or default\n write \u003cfile> \u003cdotted.key> \u003cvalue> set value (JSON literal)\n has-key \u003cfile> \u003cdotted.key> exit 0 if key exists\n remove-key \u003cfile> \u003cdotted.key> remove key\n parse-field \u003cjson-str> \u003cfield> print field from JSON string\n merge-agents \u003cov-json> \u003cagent-json>... build status JSON\n\"\"\"\nimport json\nimport sys\nimport os\n\n# Allow imports from parent dir\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nfrom ov_common import load_json, save_json, dotted_get, dotted_set, dotted_exists, dotted_remove\n\n\ndef cmd_check_mcp(args):\n cfg = load_json(args[0])\n ok = cfg.get(\"mcp\", {}).get(\"openviking\", {}).get(\"enabled\")\n sys.exit(0 if ok else 1)\n\n\ndef cmd_get_mcp_url(args):\n cfg = load_json(args[0])\n print(cfg.get(\"mcp\", {}).get(\"openviking\", {}).get(\"url\", \"\"))\n\n\ndef cmd_read(args):\n file, key = args[0], args[1]\n default = args[2] if len(args) > 2 else \"\"\n cfg = load_json(file)\n val = dotted_get(cfg, key)\n print(val if val is not None else default)\n\n\ndef cmd_write(args):\n file, key, value = args[0], args[1], args[2]\n cfg = load_json(file)\n # value is a JSON literal (string, number, bool, null)\n try:\n parsed = json.loads(value)\n except json.JSONDecodeError:\n parsed = value # treat as plain string\n dotted_set(cfg, key, parsed)\n save_json(file, cfg)\n\n\ndef cmd_has_key(args):\n cfg = load_json(args[0])\n sys.exit(0 if dotted_exists(cfg, args[1]) else 1)\n\n\ndef cmd_remove_key(args):\n file, key = args[0], args[1]\n cfg = load_json(file)\n dotted_remove(cfg, key)\n save_json(file, cfg)\n\n\ndef cmd_parse_field(args):\n \"\"\"Parse a JSON string and print a top-level field.\"\"\"\n raw, field = args[0], args[1]\n try:\n d = json.loads(raw)\n val = d.get(field, \"\")\n if isinstance(val, (dict, list)):\n print(json.dumps(val))\n else:\n print(val if val is not None else \"\")\n except Exception:\n print(\"\")\n\n\ndef cmd_merge_agents(args):\n \"\"\"Build the final status JSON from an ov-health JSON and agent JSON strings.\"\"\"\n ov_json = json.loads(args[0])\n agents = [json.loads(s) for s in args[1:]]\n print(json.dumps({\"openviking\": ov_json, \"agents\": agents}, indent=2))\n\n\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"check-mcp\": cmd_check_mcp,\n \"get-mcp-url\": cmd_get_mcp_url,\n \"read\": cmd_read,\n \"write\": cmd_write,\n \"has-key\": cmd_has_key,\n \"remove-key\": cmd_remove_key,\n \"parse-field\": cmd_parse_field,\n \"merge-agents\": cmd_merge_agents,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3153,"content_sha256":"c76aa0c73b93d1d7b681f47644681a98fe06fdb92b29f639b0cbed6802d0c57a"},{"filename":"scripts/py/ov_kimi_hook.py","content":"#!/usr/bin/env python3\n\"\"\"ov_kimi_hook.py — KimiCode OpenViking hook helper CLI.\n\nReplaces ALL inline ``$OV_PY3 -c \"...\"`` calls in\n``scripts/lib/ov_kimi_hook.sh`` with a single dispatch-based tool.\n\nSubcommands:\n json-field \u003cfield> extract field from JSON on stdin\n hook-output [message] format hook output JSON\n hook-deny \u003creason> format deny-decision JSON\n json-encode \u003cstring> JSON-encode a string\n peer-id [dir] derive peer ID from git config\n sanitize [text] strip injected OpenViking tags\n extract-wire \u003cwire_file> \u003ccursor> \u003cu_max> \u003ca_max> incremental idempotent capture\n filter-signal [text] drop ack/slash/punctuation/too-short\n config-get \u003cconfig_file> \u003cdotted_key> print config value ('' when absent)\n pending-tokens extract pending_tokens from stdin JSON\n archive-overview extract latest_archive_overview from stdin\n session-inject \u003csid> \u003carchive> assemble \u003copenviking-context> (recall on stdin)\n recall-format \u003cmax_chars> \u003cscore_threshold> format recall results from stdin JSON\n pending-enqueue \u003cdir> \u003ckind> \u003cpath> \u003cbody> write dedup'd pending item (0600)\n pending-should-send \u003cfile> \u003cmax_retries> \u003cttl_days> print SEND or DROP\n pending-get \u003cfile> \u003ckey> print a field from a pending item\n pending-body \u003cfile> print the raw POST body of a pending item\n pending-bump \u003cfile> increment attempts + last_attempt_at\n state-update \u003ckey> \u003cvalue> \u003cstate_file> set a key in a JSON state file\n state-get \u003cstate_file> \u003ckey> [default] print a key from a JSON state file\n state-get-bool \u003cstate_file> \u003ckey> print 'true'/'false' for a boolean key\n state-orphan-check \u003cstate_file> print sid if needs_commit is true\n state-mark-crash-recovered \u003cstate_file> mark state committed + crash_recovered\n state-mark-committed \u003cstate_file> mark committed, reset counters\n state-set-prompt \u003cstate_file> \u003cprompt> \u003cts> set last_prompt + last_prompt_time\n state-update-stop \u003cstate_file> \u003cneeds_commit> \u003cturn_count> \u003caccumulated_chars>\n check-viking-uri print BLOCK if stdin JSON contains viking://\n json-stdin-get \u003ckey> [default] print a key from JSON on stdin\n subagent-state-save \u003cstate_file> \u003cparent_sid> \u003csubagent_sid> \u003cagent_name> \u003cpeer_id> \u003cstarted_at>\n extract-subagent-transcript \u003ckc_sid> \u003ckc_dir> extract full subagent transcript from wire.jsonl\n\"\"\"\nimport hashlib\nimport json\nimport os\nimport re\nimport sys\nimport time\n\n# Allow imports from the parent py/ directory\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nfrom ov_common import load_json, save_json, dotted_get\n\n\n# ── json-field: extract a field from JSON on stdin (from _jf) ──────────────\ndef cmd_json_field(args):\n \"\"\"Read JSON from stdin, print the named field.\n\n Dict/list values are printed as JSON; everything else is printed as-is\n (or empty string when falsy). On any error, print empty string.\n \"\"\"\n field = args[0]\n try:\n d = json.load(sys.stdin)\n v = d.get(field, \"\")\n if isinstance(v, (dict, list)):\n print(json.dumps(v))\n else:\n print(v or \"\")\n except Exception:\n print(\"\")\n\n\n# ── hook-output: format hook output JSON (from _ov_out) ────────────────────\ndef cmd_hook_output(args):\n \"\"\"Print {\"message\": message} when message is non-empty, else {}.\"\"\"\n msg = args[0] if args else \"\"\n r = {}\n if msg:\n r[\"message\"] = msg\n print(json.dumps(r))\n\n\n# ── hook-deny: format deny decision JSON (from _ov_deny) ───────────────────\ndef cmd_hook_deny(args):\n \"\"\"Print a hookSpecificOutput deny decision with the given reason.\"\"\"\n reason = args[0] if args else \"\"\n print(json.dumps({\n \"hookSpecificOutput\": {\n \"permissionDecision\": \"deny\",\n \"permissionDecisionReason\": reason,\n }\n }))\n\n\n# ── json-encode: JSON-encode a string (from _json_str) ─────────────────────\ndef cmd_json_encode(args):\n \"\"\"Print json.dumps(args[0]).\"\"\"\n print(json.dumps(args[0]))\n\n\n# ── peer-id: derive peer ID from git config (from _ov_peer_id) ─────────────\ndef cmd_peer_id(args):\n \"\"\"Walk up from *dir* looking for a .git/config URL.\n\n If found, normalise the URL and print its md5[:16].\n Otherwise print md5(dir)[:16].\n \"\"\"\n d = args[0] if args else os.environ.get(\"CWD\", \"\")\n while d != \"/\" and d:\n if os.path.isdir(os.path.join(d, \".git\")):\n cfg = os.path.join(d, \".git\", \"config\")\n if os.path.isfile(cfg):\n with open(cfg) as f:\n for line in f:\n m = re.match(r\"\\s*url\\s*=\\s*(.+)\", line)\n if m:\n url = m.group(1).strip()\n url = re.sub(r\"^(https?://|git://|ssh://)\", \"\", url)\n url = re.sub(r\"^[^@]*@\", \"\", url)\n url = re.sub(r\"\\.git$\", \"\", url)\n url = url.rstrip(\"/\")\n print(hashlib.md5(url.encode()).hexdigest()[:16])\n return\n print(hashlib.md5(d.encode()).hexdigest()[:16])\n return\n d = os.path.dirname(d)\n print(hashlib.md5((args[0] if args else \"\").encode()).hexdigest()[:16])\n\n\n# ── sanitize: strip injected OpenViking tags (from _ov_sanitize) ──────────\ndef cmd_sanitize(args):\n \"\"\"Strip OpenViking recall blocks and injected XML-style tags.\"\"\"\n text = args[0] if args else sys.stdin.read()\n text = re.sub(r\"## OpenViking Recall\\n.*?(?=\\n\\n|\\n\u003c|\\n## |\\Z)\", \"\", text, flags=re.DOTALL)\n text = re.sub(r\"\u003chook_result[^>]*>.*?\u003c/hook_result>\", \"\", text, flags=re.DOTALL)\n text = re.sub(r\"\u003csystem-reminder>.*?\u003c/system-reminder>\", \"\", text, flags=re.DOTALL)\n text = re.sub(r\"\u003copenviking-context>.*?\u003c/openviking-context>\", \"\", text, flags=re.DOTALL)\n text = re.sub(r\"\u003c/?hook_result[^>]*>\", \"\", text)\n text = re.sub(r\"\\n{3,}\", \"\\n\\n\", text)\n print(text.strip())\n\n\n# ── extract-wire: incremental, idempotent capture from wire.jsonl ──────────\ndef cmd_extract_wire(args):\n \"\"\"Parse the NEW span of a KimiCode wire.jsonl since the last cursor.\n\n A byte-offset cursor (stored per-session in ``cursor_file``) makes the\n capture idempotent: each Stop only harvests events written since the last\n Stop, so intermediate turns are never dropped and never re-uploaded.\n\n args: wire_file cursor_file [user_max_chars] [assistant_max_chars]\n \"\"\"\n wf = args[0]\n cursor_file = args[1] if len(args) > 1 else \"\"\n user_max = 8000\n assist_max = 32000\n if len(args) > 2:\n try:\n user_max = int(args[2])\n except Exception:\n pass\n if len(args) > 3:\n try:\n assist_max = int(args[3])\n except Exception:\n pass\n\n cur = {}\n if cursor_file:\n try:\n cur = load_json(cursor_file)\n except Exception:\n cur = {}\n offset = 0\n if cur.get(\"path\") == wf and isinstance(cur.get(\"offset\"), int) and cur[\"offset\"] > 0:\n offset = cur[\"offset\"]\n\n user_text = \"\"\n assistant_parts = []\n last_text = None\n try:\n with open(wf, encoding=\"utf-8\", errors=\"replace\") as f:\n if offset:\n f.seek(offset)\n for line in f:\n line = line.strip()\n if not line:\n continue\n try:\n d = json.loads(line)\n except Exception:\n continue\n t = d.get(\"type\", \"\")\n if t == \"turn.prompt\":\n for p in d.get(\"input\", []) or []:\n if isinstance(p, dict) and p.get(\"type\") == \"text\":\n user_text = p.get(\"text\", \"\") or \"\"\n elif t == \"context.append_message\":\n m = d.get(\"message\", {}) or {}\n o = m.get(\"origin\", {}) or {}\n if m.get(\"role\") == \"user\" and o.get(\"kind\") == \"user\":\n for p in m.get(\"content\", []) or []:\n if isinstance(p, dict) and p.get(\"type\") == \"text\":\n user_text = p.get(\"text\", \"\") or \"\"\n elif t == \"context.append_loop_event\":\n pt = (d.get(\"event\", {}) or {}).get(\"part\", {}) or {}\n if pt.get(\"type\") == \"text\":\n tx = pt.get(\"text\", \"\") or \"\"\n if tx and tx != last_text:\n assistant_parts.append(tx)\n last_text = tx\n except Exception:\n pass\n\n new_offset = 0\n try:\n new_offset = os.path.getsize(wf)\n except Exception:\n pass\n if cursor_file:\n try:\n save_json(cursor_file, {\"path\": wf, \"offset\": new_offset})\n except Exception:\n pass\n\n assistant = \"\\n\".join(assistant_parts)\n print(json.dumps({\n \"user\": user_text[:user_max],\n \"assistant\": assistant[:assist_max],\n }))\n\n\n# ── filter-signal: drop ack / slash-command / punctuation / too-short ──────\n_ACK_TOKENS = {\n \"收到\", \"好的\", \"好\", \"嗯\", \"呃\", \"可以\", \"行\", \"对\", \"是的\", \"明白\", \"了解\",\n \"继续\", \"不用\", \"不需要\", \"没有了\", \"没了\", \"好了\", \"没事\", \"谢谢\", \"thanks\",\n \"thankyou\", \"gotit\", \"sure\", \"ok\", \"okay\", \"yes\", \"no\", \"continue\", \"goahead\",\n}\n\ndef _signal_normalize(s):\n return re.sub(r\"[\\W_]+\", \"\", s, flags=re.UNICODE).lower()\n\n\ndef cmd_filter_signal(args):\n \"\"\"Print *text* unchanged when it carries signal, else print ''.\n\n Drops pure acknowledgements, slash commands, punctuation-only and\n too-short strings — mirroring the official ``shouldCaptureText``.\n \"\"\"\n text = args[0] if args else sys.stdin.read()\n t = (text or \"\").strip()\n if not t:\n print(\"\")\n return\n norm = _signal_normalize(t)\n if not norm:\n print(\"\") # punctuation-only\n return\n if norm in _ACK_TOKENS:\n print(\"\")\n return\n if re.match(r\"^/[A-Za-z]\", t):\n print(\"\")\n return\n if len(norm) \u003c 2:\n print(\"\") # too short to be meaningful\n return\n print(text)\n\n\n# ── config-get: read a (dotted) value from a JSON config file ──────────────\ndef cmd_config_get(args):\n \"\"\"Print config[dotted_key] or '' when the file/key does not exist.\"\"\"\n path, key = args[0], args[1]\n try:\n v = dotted_get(load_json(path), key)\n if v is None:\n print(\"\")\n elif isinstance(v, (dict, list)):\n print(json.dumps(v))\n else:\n print(v)\n except Exception:\n print(\"\")\n\n\n# ── pending-tokens: extract server-reported pending_tokens ─────────────────\ndef cmd_pending_tokens(args):\n \"\"\"Print result.pending_tokens (int) from stdin JSON, else 0.\"\"\"\n try:\n d = json.load(sys.stdin)\n r = d.get(\"result\", {}) or {}\n v = r.get(\"pending_tokens\", d.get(\"pending_tokens\", 0))\n print(int(v) if isinstance(v, (int, float)) else 0)\n except Exception:\n print(\"0\")\n\n\n# ── archive-overview: extract latest_archive_overview ──────────────────────\ndef cmd_archive_overview(args):\n \"\"\"Print result.latest_archive_overview from stdin JSON, else ''.\"\"\"\n try:\n d = json.load(sys.stdin)\n r = d.get(\"result\", {}) or {}\n v = r.get(\"latest_archive_overview\", \"\") or \"\"\n print(v)\n except Exception:\n print(\"\")\n\n\n# ── session-inject: assemble the session-start context block ───────────────\ndef cmd_session_inject(args):\n \"\"\"Read recall text from stdin; build an \u003copenviking-context> block.\n\n args: session_id archive_overview\n \"\"\"\n sid = args[0] if args else \"\"\n archive = args[1] if len(args) > 1 else \"\"\n recall = sys.stdin.read().strip()\n parts = []\n if archive:\n parts.append('\u003csession-archive session=\"%s\">\\n%s\\n\u003c/session-archive>' % (sid, archive))\n if recall:\n parts.append('\u003crecall>\\n%s\\n\u003c/recall>' % recall)\n if not parts:\n print(\"\")\n return\n print('\u003copenviking-context source=\"session-start\" session=\"%s\">\\n%s\\n\u003c/openviking-context>'\n % (sid, \"\\n\".join(parts)))\n\n\n# ── recall-format: format recall search results (from _ov_recall) ─────────\ndef cmd_recall_format(args):\n \"\"\"Read search-result JSON from stdin and print formatted recall text.\n\n args: max_chars score_threshold\n \"\"\"\n max_chars = int(args[0])\n score_threshold = float(args[1])\n try:\n d = json.load(sys.stdin)\n r = d.get(\"result\", \"\")\n if isinstance(r, str) and r.strip():\n print(r[:max_chars])\n elif isinstance(r, dict):\n p = []\n # v0.4.19 search/recall response shape: result.entries[] (uri/category/score/text)\n for i in r.get(\"entries\", [])[:8]:\n a = i.get(\"text\") or i.get(\"detail\") or i.get(\"abstract\") or i.get(\"summary\") or \"\"\n s = i.get(\"score\", 0)\n if a and s >= score_threshold:\n uri = i.get(\"uri\", \"\")\n cat = i.get(\"category\", \"\")\n a = a[:160]\n p.append(f\"- [{s:.2f}] {a} ({uri})\" if not cat else f\"- [{s:.2f}] {a} ({uri}, {cat})\")\n # legacy shapes (memories/resources/skills groups)\n for ct in (\"memories\", \"resources\", \"skills\"):\n for i in r.get(ct, [])[:5]:\n a = i.get(\"abstract\", \"\") or i.get(\"summary\", \"\")\n s = i.get(\"score\", 0)\n if a and s >= score_threshold:\n uri = i.get(\"uri\", \"\")\n p.append(f\"- [{s:.2f}] {a} ({uri})\")\n if p:\n out = \"## OpenViking Recall\\n\" + \"\\n\".join(p)\n print(out[:max_chars])\n except Exception:\n pass\n\n\n# ── pending queue (P1-3) ───────────────────────────────────────────────────\ndef _pending_dedup_key(kind, path, body):\n return hashlib.sha256((\"%s\\n%s\\n%s\" % (kind, path, body)).encode(\"utf-8\")).hexdigest()\n\n\ndef cmd_pending_enqueue(args):\n \"\"\"Write a dedup'd pending item to \u003cdir>/\u003csha256>.json (mode 0600).\"\"\"\n pdir, kind, path, body = args[0], args[1], args[2], args[3]\n try:\n os.makedirs(pdir, exist_ok=True)\n os.chmod(pdir, 0o700)\n except Exception:\n pass\n key = _pending_dedup_key(kind, path, body)\n fp = os.path.join(pdir, key + \".json\")\n if os.path.exists(fp):\n return # idempotent: same payload already queued\n item = {\n \"kind\": kind,\n \"method\": \"POST\",\n \"path\": path,\n \"body\": body,\n \"created_at\": int(time.time()),\n \"attempts\": 0,\n }\n try:\n save_json(fp, item)\n os.chmod(fp, 0o600)\n except Exception:\n pass\n\n\ndef cmd_pending_should_send(args):\n \"\"\"Print 'SEND' when retries/TTL allow, else 'DROP'.\"\"\"\n fp = args[0]\n try:\n max_retries = int(args[1])\n except Exception:\n max_retries = 3\n try:\n ttl_days = int(args[2])\n except Exception:\n ttl_days = 7\n try:\n s = load_json(fp)\n except Exception:\n print(\"DROP\")\n return\n attempts = int(s.get(\"attempts\", 0) or 0)\n if attempts >= max_retries:\n print(\"DROP\")\n return\n created = int(s.get(\"created_at\", 0) or 0)\n if ttl_days > 0 and created and (time.time() - created) > ttl_days * 86400:\n print(\"DROP\")\n return\n print(\"SEND\")\n\n\ndef cmd_pending_get(args):\n \"\"\"Print a field (kind/method/path/attempts/...) from a pending item.\"\"\"\n fp, key = args[0], args[1]\n try:\n s = load_json(fp)\n v = s.get(key, \"\")\n print(v)\n except Exception:\n print(\"\")\n\n\ndef cmd_pending_body(args):\n \"\"\"Print the raw POST body string of a pending item.\"\"\"\n fp = args[0]\n try:\n s = load_json(fp)\n print(s.get(\"body\", \"\"))\n except Exception:\n print(\"\")\n\n\ndef cmd_pending_bump(args):\n \"\"\"Increment attempts and record last_attempt_at.\"\"\"\n fp = args[0]\n try:\n s = load_json(fp)\n s[\"attempts\"] = int(s.get(\"attempts\", 0) or 0) + 1\n s[\"last_attempt_at\"] = int(time.time())\n save_json(fp, s)\n except Exception:\n pass\n\n\n# ── state-update: set a key in a JSON state file (from _ov_state_update) ──\ndef cmd_state_update(args):\n \"\"\"Set state[key] = value (as string) and save.\"\"\"\n key, val, stf = args[0], args[1], args[2]\n try:\n s = load_json(stf)\n except Exception:\n s = {}\n s[key] = val\n save_json(stf, s)\n\n\n# ── state-get: print a key from a JSON state file ─────────────────────────\ndef cmd_state_get(args):\n \"\"\"Print state[key] or default (default = '' unless given).\"\"\"\n stf, key = args[0], args[1]\n default = args[2] if len(args) > 2 else \"\"\n try:\n s = load_json(stf)\n print(s.get(key, default))\n except Exception:\n print(default)\n\n\n# ── state-get-bool: print 'true'/'false' for a boolean key ────────────────\ndef cmd_state_get_bool(args):\n \"\"\"Print 'true' or 'false' for state[key].\"\"\"\n stf, key = args[0], args[1]\n try:\n s = load_json(stf)\n print(\"true\" if s.get(key, False) else \"false\")\n except Exception:\n print(\"false\")\n\n\n# ── state-orphan-check: print sid if needs_commit is true ─────────────────\ndef cmd_state_orphan_check(args):\n \"\"\"Print the session id if the state file has needs_commit=True.\"\"\"\n stf = args[0]\n try:\n s = load_json(stf)\n if s.get(\"needs_commit\", False) and s.get(\"sid\", \"\"):\n print(s[\"sid\"])\n except Exception:\n pass\n\n\n# ── state-mark-crash-recovered: mark state after crash recovery ───────────\ndef cmd_state_mark_crash_recovered(args):\n \"\"\"Set needs_commit=False, committed=True, crash_recovered=True.\"\"\"\n stf = args[0]\n try:\n s = load_json(stf)\n s[\"needs_commit\"] = False\n s[\"committed\"] = True\n s[\"crash_recovered\"] = True\n save_json(stf, s)\n except Exception:\n pass\n\n\n# ── state-mark-committed: mark committed and reset counters ───────────────\ndef cmd_state_mark_committed(args):\n \"\"\"Set needs_commit=False, committed=True, turn_count=0, accumulated_chars=0.\"\"\"\n stf = args[0]\n try:\n s = load_json(stf)\n s[\"needs_commit\"] = False\n s[\"committed\"] = True\n s[\"turn_count\"] = 0\n s[\"accumulated_chars\"] = 0\n save_json(stf, s)\n except Exception:\n pass\n\n\n# ── state-set-prompt: set last_prompt and last_prompt_time ────────────────\ndef cmd_state_set_prompt(args):\n \"\"\"Set state['last_prompt'] = prompt, state['last_prompt_time'] = ts.\"\"\"\n stf, prompt, ts = args[0], args[1], args[2]\n try:\n s = load_json(stf)\n s[\"last_prompt\"] = prompt\n s[\"last_prompt_time\"] = int(ts)\n save_json(stf, s)\n except Exception:\n pass\n\n\n# ── state-update-stop: update stop-event stats ────────────────────────────\ndef cmd_state_update_stop(args):\n \"\"\"Set needs_commit, turn_count, accumulated_chars in the state file.\"\"\"\n stf = args[0]\n needs_commit = args[1] == \"true\"\n turn_count = int(args[2])\n accumulated_chars = int(args[3])\n try:\n s = load_json(stf)\n s[\"needs_commit\"] = needs_commit\n s[\"turn_count\"] = turn_count\n s[\"accumulated_chars\"] = accumulated_chars\n save_json(stf, s)\n except Exception:\n pass\n\n\n# ── check-viking-uri: check if stdin JSON contains viking:// ──────────────\ndef cmd_check_viking_uri(args):\n \"\"\"Print 'BLOCK' if any string in the stdin JSON contains 'viking://'.\"\"\"\n try:\n d = json.load(sys.stdin)\n\n def check(obj):\n if isinstance(obj, str):\n if \"viking://\" in obj:\n return True\n elif isinstance(obj, dict):\n for v in obj.values():\n if check(v):\n return True\n elif isinstance(obj, list):\n for v in obj:\n if check(v):\n return True\n return False\n\n if check(d):\n print(\"BLOCK\")\n except Exception:\n pass\n\n\n# ── json-stdin-get: print a key from JSON on stdin ────────────────────────\ndef cmd_json_stdin_get(args):\n \"\"\"Print stdin_json[key] or default (default = '' unless given).\"\"\"\n key = args[0]\n default = args[1] if len(args) > 1 else \"\"\n try:\n d = json.load(sys.stdin)\n print(d.get(key, default))\n except Exception:\n print(default)\n\n\n# ── subagent-state-save: write subagent state JSON ────────────────────────\ndef cmd_subagent_state_save(args):\n \"\"\"Write a subagent state file with parent/subagent metadata.\"\"\"\n stf, parent_sid, subagent_sid, agent_name, peer_id, started_at = (\n args[0], args[1], args[2], args[3], args[4], args[5]\n )\n with open(stf, \"w\") as f:\n json.dump({\n \"parent_sid\": parent_sid,\n \"subagent_sid\": subagent_sid,\n \"agent_name\": agent_name,\n \"peer_id\": peer_id,\n \"started_at\": int(started_at),\n }, f)\n\n\n# ── extract-subagent-transcript: extract full transcript from wire.jsonl ──\ndef cmd_extract_subagent_transcript(args):\n \"\"\"Find the most recent non-main agent wire.jsonl and extract text parts.\"\"\"\n kc_sid = args[0]\n kc_dir = args[1]\n for root, dirs, files in os.walk(os.path.join(kc_dir, \"sessions\")):\n if os.path.basename(root) == kc_sid:\n agents_dir = os.path.join(root, \"agents\")\n if os.path.isdir(agents_dir):\n # Find most recently modified agent-N wire.jsonl (exclude main)\n candidates = []\n for d in os.listdir(agents_dir):\n if d == \"main\":\n continue\n wf = os.path.join(agents_dir, d, \"wire.jsonl\")\n if os.path.isfile(wf):\n candidates.append((os.path.getmtime(wf), wf))\n if candidates:\n candidates.sort(reverse=True)\n wf = candidates[0][1]\n parts = []\n with open(wf) as f:\n for line in f:\n line = line.strip()\n if not line:\n continue\n try:\n ev = json.loads(line)\n except Exception:\n continue\n t = ev.get(\"type\", \"\")\n if t == \"context.append_message\":\n m = ev.get(\"message\", {})\n role = m.get(\"role\", \"\")\n for p in m.get(\"content\", []):\n if isinstance(p, dict) and p.get(\"type\") == \"text\":\n txt = p.get(\"text\", \"\")\n if txt and len(txt) > 20:\n parts.append(f\"[{role}] {txt[:6000]}\")\n elif t == \"context.append_loop_event\":\n pt = ev.get(\"event\", {}).get(\"part\", {})\n if pt.get(\"type\") == \"text\":\n txt = pt.get(\"text\", \"\")\n if txt and len(txt) > 20:\n parts.append(f\"[assistant] {txt[:6000]}\")\n if parts:\n print(\"\\n\".join(parts[:20])[:20000])\n break\n\n\n# ── Main dispatch ──────────────────────────────────────────────────────────\ndef main():\n if len(sys.argv) \u003c 2:\n print(\"Usage: ov_kimi_hook.py \u003csubcommand> [args...]\", file=sys.stderr)\n sys.exit(2)\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"json-field\": cmd_json_field,\n \"hook-output\": cmd_hook_output,\n \"hook-deny\": cmd_hook_deny,\n \"json-encode\": cmd_json_encode,\n \"peer-id\": cmd_peer_id,\n \"sanitize\": cmd_sanitize,\n \"extract-wire\": cmd_extract_wire,\n \"filter-signal\": cmd_filter_signal,\n \"config-get\": cmd_config_get,\n \"pending-tokens\": cmd_pending_tokens,\n \"archive-overview\": cmd_archive_overview,\n \"session-inject\": cmd_session_inject,\n \"recall-format\": cmd_recall_format,\n \"pending-enqueue\": cmd_pending_enqueue,\n \"pending-should-send\": cmd_pending_should_send,\n \"pending-get\": cmd_pending_get,\n \"pending-body\": cmd_pending_body,\n \"pending-bump\": cmd_pending_bump,\n \"state-update\": cmd_state_update,\n \"state-get\": cmd_state_get,\n \"state-get-bool\": cmd_state_get_bool,\n \"state-orphan-check\": cmd_state_orphan_check,\n \"state-mark-crash-recovered\": cmd_state_mark_crash_recovered,\n \"state-mark-committed\": cmd_state_mark_committed,\n \"state-set-prompt\": cmd_state_set_prompt,\n \"state-update-stop\": cmd_state_update_stop,\n \"check-viking-uri\": cmd_check_viking_uri,\n \"json-stdin-get\": cmd_json_stdin_get,\n \"subagent-state-save\": cmd_subagent_state_save,\n \"extract-subagent-transcript\": cmd_extract_subagent_transcript,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()","content_type":"text/x-python; charset=utf-8","language":"python","size":26970,"content_sha256":"a6ec61145ec2ddec061817edde6da00d8c2e6bd7d2b60734a6e39efa248a9301"},{"filename":"scripts/py/ov_plugins.py","content":"#!/usr/bin/env python3\n\"\"\"ov_plugins.py — Plugin provisioning helpers CLI.\n\nReplaces heredoc Python in lib/plugins.sh.\n\nSubcommands:\n validate-source \u003curl> \u003chost> \u003cpath> exit 0 if URL points at official repo\n diff-blobs \u003cold-tree.json> \u003cnew-tree.json> \u003cexdir> print A/M/D diff lines\n blob-map \u003ctree.json> \u003cexdir> print path\u003cTAB>sha lines\n sha1-verify \u003cfile> \u003cexpected-sha> exit 0 if git-blob SHA-1 matches\n parse-sha \u003cjson-response> print commit SHA from GitHub API JSON\n\"\"\"\nimport hashlib\nimport json\nimport sys\nimport os\nfrom urllib.parse import urlparse\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n\n\ndef cmd_validate_source(args):\n url, host, path = args[0], args[1], args[2]\n u = urlparse(url)\n ok_host = u.hostname in (host, f\"www.{host}\")\n p = u.path.rstrip(\"/\").lstrip(\"/\")\n if p.endswith(\".git\"):\n p = p[: -len(\".git\")]\n ok_path = p == path or p.endswith(\"/\" + path)\n sys.exit(0 if (ok_host and ok_path and u.scheme in (\"http\", \"https\")) else 1)\n\n\ndef _blob_map(tree_json_path, exdir):\n with open(tree_json_path) as f:\n d = json.load(f)\n result = {}\n for e in d.get(\"tree\", []):\n if e.get(\"type\") == \"blob\" and e.get(\"path\", \"\").startswith(exdir + \"/\"):\n result[e[\"path\"]] = e[\"sha\"]\n return result\n\n\ndef cmd_diff_blobs(args):\n old, new, exdir = args[0], args[1], args[2]\n o = _blob_map(old, exdir)\n n = _blob_map(new, exdir)\n for p in sorted(set(o) | set(n)):\n if p not in o:\n print(\"A\", p)\n elif p not in n:\n print(\"D\", p)\n elif o[p] != n[p]:\n print(\"M\", p)\n\n\ndef cmd_blob_map(args):\n tree_path, exdir = args[0], args[1]\n m = _blob_map(tree_path, exdir)\n for path, sha in m.items():\n print(f\"{path}\\t{sha}\")\n\n\ndef cmd_sha1_verify(args):\n file_path, expected = args[0], args[1]\n with open(file_path, \"rb\") as f:\n data = f.read()\n actual = hashlib.sha1(b\"blob %d\\x00\" % len(data) + data).hexdigest()\n sys.exit(0 if actual == expected else 1)\n\n\ndef cmd_parse_sha(args):\n \"\"\"Print the SHA from a GitHub commits/HEAD API response (stdin or arg).\"\"\"\n raw = args[0] if args else sys.stdin.read()\n try:\n d = json.loads(raw)\n print(d.get(\"sha\", \"\"))\n except Exception:\n print(\"\")\n\n\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"validate-source\": cmd_validate_source,\n \"diff-blobs\": cmd_diff_blobs,\n \"blob-map\": cmd_blob_map,\n \"sha1-verify\": cmd_sha1_verify,\n \"parse-sha\": cmd_parse_sha,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2801,"content_sha256":"dc4e36dfe7c3c5f15b0e4b207f2d6caf9d9ef689a27652c3572fbfbd82f354b5"},{"filename":"scripts/py/ov_redact.py","content":"#!/usr/bin/env python3\n\"\"\"ov_redact.py — PII / secret redaction before sending text to OpenViking.\n\nThis module strips personally identifiable information and secrets from text\nbefore it is persisted to the OpenViking long-term memory store. It is\ndesigned to be called as a CLI helper by the agent-integration shell scripts::\n\n python3 ov_redact.py redact \u003cinput_file>\n python3 ov_redact.py redact-string \"\u003ctext>\"\n python3 ov_redact.py redact-stdin # read from stdin\n\nConfiguration:\n OV_REDACT_SECRETS env var — set to \"false\"/\"0\"/\"no\"/\"off\"/\"disable\" to\n disable redaction (default: enabled).\n --no-redact CLI flag — bypasses redaction regardless of env var.\n\nOnly the standard-library ``re`` module is used — no external dependencies.\n\"\"\"\nfrom __future__ import annotations\n\nimport os\nimport re\nimport sys\n\n# ── Redaction patterns ────────────────────────────────────────\n# Order matters: most specific patterns are applied first so that generic\n# patterns (e.g. high-entropy strings) do not clobber more precise matches.\n\n# API keys — most specific, applied first.\n_API_KEY_PATTERNS: list[tuple[str, str]] = [\n (r\"AKIA[0-9A-Z]{16}\", \"[REDACTED_AWS_KEY]\"),\n (r\"ghp_[A-Za-z0-9]{36}\", \"[REDACTED_GITHUB_TOKEN]\"),\n (r\"sk-ant-[A-Za-z0-9]{20,}\", \"[REDACTED_ANTHROPIC_KEY]\"),\n (r\"sk-[A-Za-z0-9]{20,}\", \"[REDACTED_OPENAI_KEY]\"),\n]\n\n# Bearer tokens.\n_BEARER_RE = re.compile(r\"Bearer [A-Za-z0-9._-]+\")\n_BEARER_REPLACEMENT = \"Bearer [REDACTED]\"\n\n# Password / secret assignments — capture the keyword for the replacement.\n_PASSWORD_RE = re.compile(\n r\"(password|passwd|pwd|secret|token|api_key|apikey)\\s*[=:]\\s*\\S+\"\n)\n\n# Email addresses.\n_EMAIL_RE = re.compile(r\"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\")\n_EMAIL_REPLACEMENT = \"[REDACTED_EMAIL]\"\n\n# Phone numbers — international (with + prefix) checked before Chinese mobile.\n_PHONE_INTL_RE = re.compile(r\"\\+\\d{1,3}[\\s-]?\\d{3,}[\\s-]?\\d{3,}[\\s-]?\\d{3,}\")\n_PHONE_CN_RE = re.compile(r\"\\b1[3-9]\\d{9}\\b\")\n_PHONE_REPLACEMENT = \"[REDACTED_PHONE]\"\n\n# IP addresses — non-loopback / non-private (negative lookaheads).\n_IP_RE = re.compile(\n r\"\\b(?!127\\.)(?!10\\.)(?!192\\.168\\.)(?!172\\.)(?!0\\.)\"\n r\"([0-9]{1,3}\\.){3}[0-9]{1,3}\\b\"\n)\n_IP_REPLACEMENT = \"[REDACTED_IP]\"\n\n# High-entropy strings — generic, applied last.\n_HIGH_ENTROPY_RE = re.compile(r\"[A-Za-z0-9]{40,}\")\n_HIGH_ENTROPY_REPLACEMENT = \"[REDACTED_TOKEN]\"\n\n# ISSUE-015: 32-char hex runs were not covered by the 40+ rule and leaked\n# (session tokens / md5-style secrets in text). Skipped in hash contexts\n# (checksum/sha/commit/...) like the generic high-entropy rule.\n_HEX32_RE = re.compile(r\"\\b[0-9a-fA-F]{32}\\b\")\n_HEX32_REPLACEMENT = \"[REDACTED_HEX32]\"\n\n# Keywords that indicate a SHA-hash context (skip high-entropy redaction).\n_HASH_CONTEXT_KEYWORDS: tuple[str, ...] = (\n \"sha\", \"commit\", \"hash\", \"digest\", \"checksum\", \"git\", \"blob\", \"tree\",\n)\n_HEX_RE = re.compile(r\"^[0-9a-fA-F]+$\")\n\n# Pre-compile API-key regexes for reuse.\n_API_KEY_RES: list[tuple[re.Pattern[str], str]] = [\n (re.compile(p), r) for p, r in _API_KEY_PATTERNS\n]\n\n\n# ── Helpers ───────────────────────────────────────────────────\ndef _is_hash_context(text: str, pos: int) -> bool:\n \"\"\"Return True if *pos* in *text* is preceded by a hash-context keyword.\n\n Looks at the 30 characters immediately before *pos* for any keyword\n in :data:`_HASH_CONTEXT_KEYWORDS` (case-insensitive).\n \"\"\"\n window = text[max(0, pos - 30):pos].lower()\n return any(kw in window for kw in _HASH_CONTEXT_KEYWORDS)\n\n\ndef _redact_high_entropy(text: str) -> str:\n \"\"\"Redact high-entropy strings, skipping SHA hashes in known contexts.\n\n A 40+ character alphanumeric run is considered a SHA hash (and skipped)\n only when it is entirely hexadecimal **and** preceded by a hash-context\n keyword such as *commit*, *sha*, *digest*, etc.\n \"\"\"\n def _replace(m: re.Match[str]) -> str:\n token = m.group(0)\n if _HEX_RE.match(token) and _is_hash_context(text, m.start()):\n return token # looks like a SHA hash in context — keep it\n return _HIGH_ENTROPY_REPLACEMENT\n\n return _HIGH_ENTROPY_RE.sub(_replace, text)\n\n\ndef _redact_hex32(text: str) -> str:\n \"\"\"Redact 32-char hex runs (ISSUE-015), keeping hash-context occurrences.\"\"\"\n def _replace(m: re.Match[str]) -> str:\n if _is_hash_context(text, m.start()):\n return m.group(0) # checksum/sha context — looks like a digest\n return _HEX32_REPLACEMENT\n\n return _HEX32_RE.sub(_replace, text)\n\n\n# ── Public API ────────────────────────────────────────────────\ndef redact(text: str) -> str:\n \"\"\"Apply all redaction patterns to *text* and return the result.\n\n Patterns are applied in order from most specific (API keys, bearer tokens)\n to most generic (high-entropy strings) so that precise matches are not\n overwritten by broad ones.\n \"\"\"\n # 1. API keys (most specific).\n for regex, replacement in _API_KEY_RES:\n text = regex.sub(replacement, text)\n\n # 2. Bearer tokens.\n text = _BEARER_RE.sub(_BEARER_REPLACEMENT, text)\n\n # 3. Password / secret assignments.\n text = _PASSWORD_RE.sub(lambda m: m.group(1) + \"=[REDACTED]\", text)\n\n # 4. Email addresses.\n text = _EMAIL_RE.sub(_EMAIL_REPLACEMENT, text)\n\n # 5. Phone numbers (international first, then Chinese mobile).\n text = _PHONE_INTL_RE.sub(_PHONE_REPLACEMENT, text)\n text = _PHONE_CN_RE.sub(_PHONE_REPLACEMENT, text)\n\n # 6. IP addresses (non-loopback / non-private).\n text = _IP_RE.sub(_IP_REPLACEMENT, text)\n\n # 7. High-entropy strings (generic, last — with SHA-hash context skip).\n text = _redact_high_entropy(text)\n\n # 8. 32-char hex runs (ISSUE-015 — hash-context aware).\n text = _redact_hex32(text)\n\n return text\n\n\ndef is_redaction_enabled(no_redact_flag: bool) -> bool:\n \"\"\"Return True if redaction should run given the flag and env var.\n\n Redaction is disabled when *no_redact_flag* is ``True`` or when the\n ``OV_REDACT_SECRETS`` environment variable is set to a falsy value\n (``false``, ``0``, ``no``, ``off``, ``disable``, ``disabled``).\n \"\"\"\n if no_redact_flag:\n return False\n env_val = os.environ.get(\"OV_REDACT_SECRETS\", \"true\").strip().lower()\n return env_val not in (\"false\", \"0\", \"no\", \"off\", \"disable\", \"disabled\")\n\n\ndef process(text: str, no_redact: bool = False) -> str:\n \"\"\"Process *text*: redact if enabled, otherwise return unchanged.\"\"\"\n if is_redaction_enabled(no_redact):\n return redact(text)\n return text\n\n\n# ── CLI ───────────────────────────────────────────────────────\ndef _split_flag(args: list[str]) -> tuple[list[str], bool]:\n \"\"\"Separate ``--no-redact`` from positional arguments.\"\"\"\n no_redact = \"--no-redact\" in args\n positional = [a for a in args if a != \"--no-redact\"]\n return positional, no_redact\n\n\ndef _read_input(path: str) -> str:\n \"\"\"Read text from *path* (``-`` or empty string means stdin).\"\"\"\n if path in (\"-\", \"\"):\n return sys.stdin.read()\n try:\n with open(path, encoding=\"utf-8\") as f:\n return f.read()\n except FileNotFoundError:\n print(f\"Error: file not found: {path}\", file=sys.stderr)\n sys.exit(1)\n except OSError as exc:\n print(f\"Error reading {path}: {exc}\", file=sys.stderr)\n sys.exit(1)\n\n\ndef cmd_redact(args: list[str]) -> None:\n \"\"\"Read text from a file, redact, and write to stdout.\"\"\"\n positional, no_redact = _split_flag(args)\n if not positional:\n print(\"Usage: redact \u003cinput_file> [--no-redact]\", file=sys.stderr)\n sys.exit(2)\n text = _read_input(positional[0])\n sys.stdout.write(process(text, no_redact))\n\n\ndef cmd_redact_string(args: list[str]) -> None:\n \"\"\"Redact a string given as a command-line argument.\"\"\"\n positional, no_redact = _split_flag(args)\n if not positional:\n print(\"Usage: redact-string \u003ctext> [--no-redact]\", file=sys.stderr)\n sys.exit(2)\n # Use print() for string args — adds trailing newline for shell ergonomics.\n print(process(positional[0], no_redact))\n\n\ndef cmd_redact_stdin(args: list[str]) -> None:\n \"\"\"Read text from stdin, redact, and write to stdout.\"\"\"\n _, no_redact = _split_flag(args)\n text = sys.stdin.read()\n sys.stdout.write(process(text, no_redact))\n\n\ndef main() -> None:\n \"\"\"CLI entry point — dispatch to a subcommand.\"\"\"\n if len(sys.argv) \u003c 2:\n print(\n \"Usage: ov_redact.py \u003csubcommand> [args]\\n\"\n \"\\n\"\n \"Subcommands:\\n\"\n \" redact \u003cfile> Redact text from a file\\n\"\n \" redact-string \u003ctext> Redact text from a command-line arg\\n\"\n \" redact-stdin Redact text from stdin\\n\"\n \"\\n\"\n \"Options:\\n\"\n \" --no-redact Bypass redaction\\n\"\n \"\\n\"\n \"Environment:\\n\"\n \" OV_REDACT_SECRETS Set to false/0/no/off to disable\\n\",\n file=sys.stderr,\n )\n sys.exit(2)\n\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"redact\": cmd_redact,\n \"redact-string\": cmd_redact_string,\n \"redact-stdin\": cmd_redact_stdin,\n }\n fn = dispatch.get(cmd)\n if fn is None:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9835,"content_sha256":"5604e9c59ec5c9d57c3f55f7caa28b2ffb17ef70105ab6b5c3b80faecfea5e0e"},{"filename":"scripts/py/ov_verify_mcp_sdk.py","content":"#!/usr/bin/env python3\n\"\"\"ov_verify_mcp_sdk.py — MCP endpoint verification using the official MCP Python SDK.\n\nReplaces the hand-rolled multi-POST curl implementation in verify_mcp.sh with a\nsingle-session Streamable HTTP client (mcp.client.streamable_http.streamable_http_client\n+ mcp.ClientSession). One HTTP session carries initialize -> initialized ->\ntools/list -> tools/call(health), which matches the streamable-http transport\nsemantics the OpenViking server expects.\n\nUsage:\n ov_verify_mcp_sdk.py \u003cmcp_url> [--api-key KEY]\n\nOutput (stdout):\n SERVER:\u003cname>\n VERSION:\u003cversion>\n PROTOCOL:\u003cprotocol>\n TOOLS:\u003ccount>\n TOOL:\u003cname>:\u003cdescription-first-80>\n HEALTH:\u003ctext-result>\n\nExit codes:\n 0 handshake + health tool call succeeded\n 1 handshake/tool failure (details on stderr)\n 2 mcp SDK not installed / unknown cli error\n\"\"\"\nimport argparse\nimport asyncio\nimport sys\n\ndef err(msg):\n print(f\"[ERROR] {msg}\", file=sys.stderr)\n\ndef _flatten_exc(e):\n \"\"\"Recursively unwrap ExceptionGroup to reach leaf errors.\"\"\"\n subs = getattr(e, \"exceptions\", [])\n if subs:\n for s in subs:\n yield from _flatten_exc(s)\n else:\n yield e\n\nasync def run_verify(url, api_key):\n try:\n from mcp import ClientSession\n from mcp.client.streamable_http import streamable_http_client\n import httpx2 # SDK 2.x transport backend\n except ImportError as e:\n err(f\"MCP Python SDK not available: {e}\")\n # ISSUE-016: the streamable-http transport + httpx2 backend used here\n # requires the 2.x SDK line — 1.x never worked with this code path\n err(\"Install it with: pip install --upgrade 'mcp>=2.0'\")\n return 2\n\n http_client = None\n try:\n if api_key:\n http_client = httpx2.AsyncClient(\n headers={\"Authorization\": f\"Bearer {api_key}\"},\n timeout=httpx2.Timeout(30.0),\n )\n async with streamable_http_client(url, http_client=http_client) as streams:\n async with ClientSession(streams[0], streams[1]) as session:\n # 1) initialize\n init = await session.initialize()\n # SDK \u003c2.2 used camelCase (serverInfo/protocolVersion); 2.2+ uses\n # snake_case (server_info/protocol_version). Tolerate both.\n si = getattr(init, \"server_info\", None) or getattr(init, \"serverInfo\", None)\n proto = getattr(init, \"protocol_version\", None) or getattr(init, \"protocolVersion\", None)\n if si is None:\n raise RuntimeError(f\"initialize response has no server_info: {init!r}\")\n print(f\"SERVER:{si.name}\")\n print(f\"VERSION:{si.version}\")\n print(f\"PROTOCOL:{proto}\")\n\n # 2) initialized notification is sent automatically by the SDK.\n\n # 3) tools/list\n tools_result = await session.list_tools()\n tools = list(tools_result.tools)\n print(f\"TOOLS:{len(tools)}\")\n for t in tools:\n desc = (getattr(t, \"description\", \"\") or \"\").splitlines()[0][:80]\n print(f\"TOOL:{t.name}:{desc}\")\n\n # 4) tools/call health\n health = await session.call_tool(\"health\", {})\n if hasattr(health, \"content\"):\n texts = [\n c.text for c in health.content\n if getattr(c, \"type\", None) == \"text\"\n ]\n result_text = \"\\n\".join(texts) if texts else str(health)\n else:\n result_text = str(health)\n print(f\"HEALTH:{result_text[:200]}\")\n return 0\n except Exception as e:\n err(f\"MCP verification failed: {type(e).__name__}: {str(e)[:200]}\")\n for leaf in _flatten_exc(e):\n err(f\" cause: {type(leaf).__name__}: {str(leaf)[:300]}\")\n return 1\n finally:\n if http_client is not None:\n await http_client.aclose()\n\ndef main():\n parser = argparse.ArgumentParser(description=\"Verify OpenViking MCP endpoint with official SDK\")\n parser.add_argument(\"mcp_url\", help=\"MCP endpoint URL, e.g. http://127.0.0.1:1933/mcp\")\n parser.add_argument(\"--api-key\", default=\"\", help=\"Bearer API key (dev mode needs none)\")\n args = parser.parse_args()\n try:\n rc = asyncio.run(run_verify(args.mcp_url, args.api_key or None))\n except KeyboardInterrupt:\n err(\"interrupted\")\n rc = 1\n sys.exit(rc)\n\nif __name__ == \"__main__\":\n main()","content_type":"text/x-python; charset=utf-8","language":"python","size":4624,"content_sha256":"f133a451b49c06afd599e66bca87976317e61b1e44d710f1c2188c44a14a14df"},{"filename":"scripts/py/ov_verify_mcp.py","content":"#!/usr/bin/env python3\n\"\"\"ov_verify_mcp.py — MCP endpoint verification helpers CLI.\n\nReplaces inline ``python3 -c`` calls in scripts/verify_mcp.sh.\n\nSubcommands:\n parse-init \u003csse-data> print \"protocol server version\"\n parse-tools \u003csse-data> print tool count + names\n parse-health-tool \u003csse-data> print health tool text result\n count-results \u003cjson-file> print total result count from REST find\n\"\"\"\nimport json\nimport sys\nimport os\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n\n\ndef _load_sse(data_str):\n \"\"\"Extract the JSON payload from an SSE ``data:`` line or raw JSON.\"\"\"\n for line in data_str.strip().splitlines():\n line = line.strip()\n if line.startswith(\"data: \"):\n return json.loads(line[6:])\n if line.startswith(\"{\"):\n return json.loads(line)\n return {}\n\n\ndef cmd_parse_init(args):\n try:\n data = args[0] if args else sys.stdin.read()\n r = _load_sse(data).get(\"result\", {})\n proto = r.get(\"protocolVersion\", \"unknown\")\n name = r.get(\"serverInfo\", {}).get(\"name\", \"unknown\")\n ver = r.get(\"serverInfo\", {}).get(\"version\", \"unknown\")\n print(f\"{proto} {name} {ver}\")\n except Exception as e:\n print(\"parse-error unknown unknown\")\n\n\ndef cmd_parse_tools(args):\n try:\n data = args[0] if args else sys.stdin.read()\n d = _load_sse(data)\n tools = d.get(\"result\", {}).get(\"tools\", [])\n print(len(tools))\n for t in tools:\n desc = t.get(\"description\", \"\")[:80]\n print(f\" - {t['name']}: {desc}\")\n except Exception as e:\n print(f\"parse-error: {e}\")\n\n\ndef cmd_parse_health_tool(args):\n try:\n data = args[0] if args else sys.stdin.read()\n d = _load_sse(data)\n result = d.get(\"result\", {})\n if isinstance(result, dict) and \"content\" in result:\n for c in result[\"content\"]:\n if c.get(\"type\") == \"text\":\n print(c[\"text\"][:200])\n else:\n print(str(result)[:200])\n except Exception as e:\n print(f\"parse-error: {e}\")\n\n\ndef cmd_count_results(args):\n \"\"\"Count total results from a REST /api/v1/search/find response file.\"\"\"\n try:\n with open(args[0]) as f:\n d = json.load(f)\n r = d.get(\"result\", d)\n total = r.get(\"total\", 0)\n if total == 0:\n total = (\n len(r.get(\"memories\", []))\n + len(r.get(\"resources\", []))\n + len(r.get(\"skills\", []))\n )\n print(total)\n except Exception:\n print(\"parse-error\")\n\n\ndef main():\n cmd = sys.argv[1]\n args = sys.argv[2:]\n dispatch = {\n \"parse-init\": cmd_parse_init,\n \"parse-tools\": cmd_parse_tools,\n \"parse-health-tool\": cmd_parse_health_tool,\n \"count-results\": cmd_count_results,\n }\n fn = dispatch.get(cmd)\n if not fn:\n print(f\"Unknown subcommand: {cmd}\", file=sys.stderr)\n sys.exit(2)\n fn(args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3077,"content_sha256":"6429d17a086bb837d1903f304a72740f1a544880d9283e4432be73f6fd1b2bab"},{"filename":"scripts/status.sh","content":"#!/bin/bash\n# status.sh — OpenViking integration status entry point\n# Usage: ./status.sh [--json] [--agent \u003cname>]\nset -euo pipefail\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/entrypoint.sh\"\n# Register agents early so usage()/error paths can list supported agents (ISSUE-006)\nregistry_init\nregistry_discover \"$SCRIPT_DIR/agents\"\nJSON_OUTPUT=false\nAGENT=\"\"\nusage() {\n echo \"Usage: $0 [--json] [--agent \u003cname>]\"\n echo \"Without --agent: shows status for all agents.\"\n echo \"Supported agents: $(ov_supported_agents 2>/dev/null || echo \"(none)\")\"\n}\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --json) JSON_OUTPUT=true; shift ;;\n --agent)\n if [[ $# -lt 2 ]]; then log_error \"--agent requires a value\"; usage; exit 1; fi\n AGENT=\"$2\"; shift 2 ;;\n --help|-h) usage; exit 0 ;;\n # ISSUE-006: reject unknown options (e.g. --all) with the usage line\n *) log_error \"Unknown option: $1\"; usage; exit 1 ;;\n esac\ndone\nagents=()\nif [[ -n \"$AGENT\" ]]; then\n # ISSUE-006: reject unknown agent names with the supported list up front\n # (previously fell through to a generic \"dispatch failed\" on first use)\n ov_require_supported_agent \"$AGENT\" || exit 1\n agents=(\"$AGENT\")\nelse\n while IFS= read -r a; do agents+=(\"$a\"); done \u003c \u003c(registry_list)\nfi\ncheck_ov() {\n local resp status version auth_mode\n resp=$(\"$OV_CURL_BIN\" -sf --connect-timeout 5 --max-time 10 \"${OV_ENDPOINT}/health\" 2>/dev/null) || {\n if [[ \"$JSON_OUTPUT\" == \"true\" ]]; then\n echo '{\"status\":\"unreachable\",\"endpoint\":\"'\"$OV_ENDPOINT\"'\"}'\n else\n echo -e \"${RED}✗${NC} OpenViking server unreachable at $OV_ENDPOINT\"\n fi\n return 1\n }\n status=$(echo \"$resp\" | _ov_health parse-field - status 2>/dev/null)\n version=$(echo \"$resp\" | _ov_health parse-field - version 2>/dev/null)\n auth_mode=$(echo \"$resp\" | _ov_health parse-field - auth_mode 2>/dev/null)\n if [[ \"$JSON_OUTPUT\" == \"true\" ]]; then\n echo \"{\\\"status\\\":\\\"$status\\\",\\\"version\\\":\\\"$version\\\",\\\"auth_mode\\\":\\\"$auth_mode\\\",\\\"endpoint\\\":\\\"$OV_ENDPOINT\\\"}\"\n else\n echo -e \"${GREEN}✓${NC} OpenViking server: $status (v$version, auth=$auth_mode) at $OV_ENDPOINT\"\n fi\n}\nget_agent_json() {\n local a=\"$1\"\n local result s_name s_status s_detail\n result=$(registry_dispatch \"$a\" status 2>/dev/null || echo \"$a|error|dispatch failed\")\n IFS='|' read -r s_name s_status s_detail \u003c\u003c\u003c \"$result\"\n printf '{\"agent\":\"%s\",\"status\":\"%s\",\"detail\":\"%s\"}' \"$s_name\" \"$s_status\" \"$s_detail\"\n}\nif [[ \"$JSON_OUTPUT\" == \"true\" ]]; then\n # JSON mode — exit code mirrors human mode (non-zero on unreachable / agent errors)\n overall_rc=0\n ov_json=$(check_ov 2>/dev/null) || overall_rc=1\n # Build agents array\n agent_jsons=()\n for a in \"${agents[@]}\"; do\n local_aj=$(get_agent_json \"$a\")\n agent_jsons+=(\"$local_aj\")\n [[ \"$local_aj\" == *'\"status\":\"error\"'* ]] && overall_rc=1\n done\n # Emit JSON\n _ov_json merge-agents \"$ov_json\" \"${agent_jsons[@]}\"\n exit $overall_rc\nelse\n # Human-readable mode\n echo \"━━━ OpenViking Integration Status ━━━\"\n echo \"\"\n overall_rc=0\n check_ov || overall_rc=1\n echo \"\"\n echo \"━━━ Agent Integration Status ━━━\"\n for a in \"${agents[@]}\"; do\n result=$(registry_dispatch \"$a\" status 2>/dev/null || echo \"$a|error|dispatch failed\")\n s_name=\"${result%%|*}\"\n rest=\"${result#*|}\"\n s_status=\"${rest%%|*}\"\n s_detail=\"${rest#*|}\"\n if [[ \"$s_status\" == \"integrated\" ]]; then\n echo -e \" ${GREEN}✓${NC} $s_name: $s_detail\"\n elif [[ \"$s_status\" == \"not_integrated\" ]]; then\n echo -e \" ${RED}✗${NC} $s_name: $s_detail\"\n else\n echo -e \" ${YELLOW}?${NC} $s_name: $s_detail\"\n overall_rc=1\n fi\n done\n exit $overall_rc\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3709,"content_sha256":"c590c475ab339a7bab5bfb6cfc2a717cde6f28ff403f6de0ee20f10d93d918c0"},{"filename":"scripts/templates/agents-md-snippet.md","content":"# Agent Instructions\n## OpenViking Long-Term Memory\nYou have OpenViking long-term memory integrated. Follow these protocols:\n### Auto-Recall (at conversation start)\nBefore responding to the user's first message, check the session for an already-injected\n`\u003copenviking-context>` block; if it answers the question, use it and skip extra tool calls.\nOtherwise call `search` with `mode=\"context\"` for \"what do I know about X\" (the server\nassembles a token-budgeted digest across memory types), or `find` for a fast ranked list.\nUse retrieved context to inform responses; do not mention the retrieval process.\n### Proactive Search (during tasks)\n1. `search` with `mode=\"context\"` — first choice for relevant past knowledge, error solutions, decisions.\n2. `find` — fast ranked list when you want raw hits to triage yourself.\n3. `read` — expand promising hits (viking:// URIs) before relying on them; an abstract may be stale.\n4. `grep` / `glob` — exact text or filename matching when you know the literal string or file name.\n### Auto-Capture (after meaningful exchanges)\nAfter completing a task or learning important information:\n1. `remember` — call this whenever the user shares preferences, important facts, decisions,\n or explicitly asks to remember/keep/save something (e.g. \"记住\", \"记住这个\", \"请记住\",\n \"remember this\", \"帮我记一下\"). This is the ONLY way to persist to long-term memory —\n there is no automatic session capture, so if you don't call `remember`, nothing is saved.\n Do not mirror routine back-and-forth chatter, but DO capture durable knowledge.\n2. `add_resource` — import files, directories, URLs, or repos as durable knowledge.\n Processing is asynchronous; report that ingestion started rather than blocking.\n3. Never echo credentials or surface private memories unrelated to the task.\n### Repo Context\nWhen starting work in a repository:\n1. Call `add_resource` with the repo path to index it for context-aware assistance.\n2. Use `search` to find prior work on the same codebase.\nDo not wait to be asked — proactively use these tools for context-aware responses across sessions and projects.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2151,"content_sha256":"08b7e476320a123a0de6b01a3f0010aa1f42058d52e796224721f32844865b96"},{"filename":"scripts/unbind.sh","content":"#!/bin/bash\n# unbind.sh — OpenViking unbinding entry point\n# Usage: ./unbind.sh --agent \u003cname>|--all [--dry-run] [--yes]\nset -euo pipefail\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nsource \"$SCRIPT_DIR/lib/entrypoint.sh\"\nOV_MCP_URL=\"${OV_ENDPOINT}/mcp\"\n# Register agents early so usage()/error paths can list supported agents (ISSUE-006)\nregistry_init\nregistry_discover \"$SCRIPT_DIR/agents\"\nDRY_RUN=false\nAUTO_YES=false\nAGENT=\"\"\nALL_AGENTS=false\nusage() {\n echo \"Usage: $0 --agent \u003cname>|--all [--dry-run] [--yes]\"\n echo \"Supported agents: $(ov_supported_agents 2>/dev/null || echo \"(none)\")\"\n}\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --agent)\n if [[ $# -lt 2 ]]; then log_error \"--agent requires a value\"; usage; exit 1; fi\n AGENT=\"$2\"; shift 2 ;;\n --all) ALL_AGENTS=true; shift ;;\n --dry-run) DRY_RUN=true; shift ;;\n --yes|-y) AUTO_YES=true; shift ;;\n --help|-h) usage; exit 0 ;;\n *) log_error \"Unknown option: $1\"; usage; exit 1 ;;\n esac\ndone\nif [[ \"$ALL_AGENTS\" == \"true\" && -n \"$AGENT\" ]]; then\n log_error \"--agent and --all are mutually exclusive — specify exactly one\"\n exit 1\nfi\nagents=()\nif [[ \"$ALL_AGENTS\" == \"true\" ]]; then\n while IFS= read -r a; do agents+=(\"$a\"); done \u003c \u003c(registry_list)\nelif [[ -n \"$AGENT\" ]]; then\n # ISSUE-006: reject unknown agent names with the supported list up front\n ov_require_supported_agent \"$AGENT\" || exit 1\n agents=(\"$AGENT\")\nelse\n log_error \"Specify --agent \u003cname> or --all\"\n usage; exit 1\nfi\n# ISSUE-001: never \"successfully\" unbind against an unreachable server — state the\n# health gate up front so a dead endpoint cannot produce false success reports.\n# (Dry-run previews also need live state, so the gate applies to both modes.)\ncheck_ov_health || exit 1\nrc=0\nfor a in \"${agents[@]}\"; do\n display=\"${a//_/-}\"\n log_info \"Processing agent: $display\"\n # ISSUE-004: capture the dispatch exit code explicitly — errexit is disabled\n # inside if-conditions, so `if dispatch; then` masked real failures\n ( registry_dispatch \"$a\" unbind ); dsp_rc=$?\n if [[ $dsp_rc -eq 0 ]]; then\n log_ok \"Agent $display: unbinding complete\"\n else\n log_error \"Agent $display: unbinding failed\"\n rc=1\n fi\ndone\nif [[ $rc -ne 0 ]]; then\n log_warn \"Some unbindings failed. Review output above.\"\nfi\nexit $rc","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2294,"content_sha256":"097eca0247a9894daed982a3591be995d458cca180bab18239491fe6560ce362"},{"filename":"scripts/verify_mcp.sh","content":"#!/bin/bash\n# OpenViking MCP Endpoint Verification\n# Uses the official MCP Python SDK (streamable_http_client + ClientSession) for a\n# single-session handshake: initialize -> initialized -> tools/list -> tools/call(health).\n# REST prefetch checks remain as a secondary validation channel.\nset -euo pipefail\nsource \"$(dirname \"$0\")/lib/entrypoint.sh\"\n: \"${OV_PY_DIR:=$(cd \"$(dirname \"$0\")/py\" && pwd)}\"\nOV_API_KEY=\"${OV_API_KEY:-}\"\nusage() {\n echo \"Usage: $0 [--endpoint URL] [--api-key KEY]\"\n echo \" --endpoint URL OpenViking server base URL (default: \\$OV_ENDPOINT)\"\n echo \" --api-key KEY API key for the MCP handshake (default: \\$OV_API_KEY)\"\n}\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --endpoint)\n if [[ $# -lt 2 ]]; then log_error \"--endpoint requires a value\"; usage; exit 1; fi\n OV_ENDPOINT=\"$2\"; shift 2 ;;\n --api-key)\n if [[ $# -lt 2 ]]; then log_error \"--api-key requires a value\"; usage; exit 1; fi\n OV_API_KEY=\"$2\"; shift 2 ;;\n --help|-h) usage; exit 0 ;;\n *) log_error \"Unknown option: $1\"; usage; exit 1 ;;\n esac\ndone\nMCP_URL=\"${OV_ENDPOINT}/mcp\"\nOV_VERIFY_QUERY=\"${OV_VERIFY_QUERY:-openviking verification}\"\nOV_ACCOUNT=\"${OV_ACCOUNT:-default}\"\n\n# P0-2: Pass API key to the SDK script via env var (not CLI args) to avoid ps/proc exposure\nif [[ -n \"$OV_API_KEY\" ]]; then\n export OV_API_KEY\nfi\n\nOV_TMP_REST=$(mktemp); OV_TMP_TOPK=$(mktemp); OV_TMP_ACCT=$(mktemp)\ntrap 'rm -f \"$OV_TMP_REST\" \"$OV_TMP_TOPK\" \"$OV_TMP_ACCT\"' EXIT\n\necho \"━━━ OpenViking MCP Verification ━━━\"\necho \"\"\nlog_info \"Health check...\"\nlocal_health=$(ov_curl -sf \"${OV_ENDPOINT}/health\" 2>/dev/null) || { log_error \"Server not reachable at $OV_ENDPOINT\"; exit 1; }\nlog_ok \"Server healthy: $local_health\"\necho \"\"\nlog_info \"MCP handshake via official SDK (single session)...\"\n\nSDK_RC=0\n# Capture SDK stderr into temp file, but do NOT let set -e abort: keep exit code in SDK_RC\nSDK_OUT=$(\"$OV_PY\" \"$OV_PY_DIR/ov_verify_mcp_sdk.py\" \"$MCP_URL\" 2>\"$OV_TMP_REST\") || SDK_RC=$?\nif [[ ${SDK_RC:-1} -ne 0 ]]; then\n # Surface the SDK's concrete error (HTTP status / JSON-RPC error / unreachable)\n log_error \"MCP handshake FAILED (exit ${SDK_RC:-1})\"\n cat \"$OV_TMP_REST\" >&2\n exit 1\nfi\n\nSERVER_NAME=\"\"; SERVER_VERSION=\"\"; PROTOCOL_VERSION=\"\"; TOOL_COUNT=\"\"\ndeclare -a TOOL_LINES=()\nHEALTH_RESULT=\"\"\nwhile IFS= read -r line; do\n case \"$line\" in\n SERVER:*) SERVER_NAME=\"${line#SERVER:}\" ;;\n VERSION:*) SERVER_VERSION=\"${line#VERSION:}\" ;;\n PROTOCOL:*) PROTOCOL_VERSION=\"${line#PROTOCOL:}\" ;;\n TOOLS:*) TOOL_COUNT=\"${line#TOOLS:}\" ;;\n TOOL:*) TOOL_LINES+=(\"${line#TOOL:}\") ;;\n HEALTH:*) HEALTH_RESULT=\"${line#HEALTH:}\" ;;\n esac\ndone \u003c\u003c\u003c \"$SDK_OUT\"\n\nif [[ -z \"$PROTOCOL_VERSION\" || -z \"$SERVER_NAME\" ]]; then\n log_error \"MCP initialize handshake failed — SDK returned no server info\"\n log_error \"Raw output (first 200 chars): ${SDK_OUT:0:200}\"\n exit 1\nfi\nlog_ok \"MCP server: $SERVER_NAME v$SERVER_VERSION (protocol $PROTOCOL_VERSION)\"\nlog_ok \"Found $TOOL_COUNT MCP tools\"\nfor tl in \"${TOOL_LINES[@]:0:20}\"; do\n name=\"${tl%%:*}\"\n desc=\"${tl#*:}\"\n echo \" - $name: $desc\"\ndone\nif [[ \"${#TOOL_LINES[@]}\" -gt 20 ]]; then\n echo \" ... and $(( ${#TOOL_LINES[@]} - 20 )) more\"\nfi\nlog_info \"Testing 'health' tool...\"\nif [[ -n \"$HEALTH_RESULT\" ]]; then\n log_ok \"Health tool: $HEALTH_RESULT\"\nelse\n log_error \"Health tool call returned no result\"\n exit 1\nfi\n\necho \"\"\necho \"━━━ REST Prefetch Verification ━━━\"\necho \"\"\n# ISSUE-002: every REST check that deviates from contract now flips the overall\n# result — a green SDK handshake no longer masks failing REST paths.\nOVERALL_RC=0\nlog_info \"REST /api/v1/search/find with 'limit'...\"\nREST_RESP=$(ov_curl -s -o \"$OV_TMP_REST\" -w \"%{http_code}\" \\\n -X POST \"${OV_ENDPOINT}/api/v1/search/find\" \\\n -H \"Content-Type: application/json\" \\\n -H \"X-OpenViking-Account: ${OV_ACCOUNT}\" -H \"X-OpenViking-User: default\" \\\n -d \"{\\\"query\\\":\\\"$OV_VERIFY_QUERY\\\",\\\"limit\\\":5}\" 2>/dev/null || echo \"000\")\nif [[ \"$REST_RESP\" == \"200\" ]]; then\n REST_COUNT=$(\"$OV_PY\" \"$OV_PY_DIR/ov_verify_mcp.py\" count-results \"$OV_TMP_REST\" 2>/dev/null || echo \"?\")\n log_ok \"REST find: HTTP 200, ${REST_COUNT} results (account: ${OV_ACCOUNT})\"\nelse\n log_error \"REST find: HTTP ${REST_RESP} — prefetch will FAIL\"\n cat \"$OV_TMP_REST\" 2>/dev/null | head -5\n OVERALL_RC=1\nfi\necho \"\"\nlog_info \"Confirming 'top_k' rejected...\"\nTOPK_RESP=$(ov_curl -s -o \"$OV_TMP_TOPK\" -w \"%{http_code}\" \\\n -X POST \"${OV_ENDPOINT}/api/v1/search/find\" \\\n -H \"Content-Type: application/json\" \\\n -H \"X-OpenViking-Account: ${OV_ACCOUNT}\" \\\n -d '{\"query\":\"test\",\"top_k\":5}' 2>/dev/null || echo \"000\")\nif [[ \"$TOPK_RESP\" == \"400\" ]]; then\n log_ok \"'top_k' rejected with HTTP 400\"\nelse\n log_warn \"'top_k' returned HTTP ${TOPK_RESP} (expected 400)\"\n OVERALL_RC=1\nfi\necho \"\"\nlog_info \"Checking account '${OV_ACCOUNT}' memories...\"\nACCT_RESP=$(ov_curl -s -o \"$OV_TMP_ACCT\" -w \"%{http_code}\" \\\n -X POST \"${OV_ENDPOINT}/api/v1/search/find\" \\\n -H \"Content-Type: application/json\" \\\n -H \"X-OpenViking-Account: ${OV_ACCOUNT}\" \\\n -d \"{\\\"query\\\":\\\"$OV_VERIFY_QUERY\\\",\\\"limit\\\":10}\" 2>/dev/null || echo \"000\")\nif [[ \"$ACCT_RESP\" == \"200\" ]]; then\n ACCT_COUNT=$(\"$OV_PY\" \"$OV_PY_DIR/ov_verify_mcp.py\" count-results \"$OV_TMP_ACCT\" 2>/dev/null || echo \"0\")\n if [[ \"$ACCT_COUNT\" -gt 0 ]]; then\n log_ok \"Account '${OV_ACCOUNT}' has ${ACCT_COUNT} memories\"\n else\n log_warn \"Account '${OV_ACCOUNT}' returned 0 memories\"\n fi\nelse\n log_error \"Account check failed: HTTP ${ACCT_RESP}\"\n OVERALL_RC=1\nfi\n\necho \"\"\necho \"━━━ Verification Summary ━━━\"\necho \" Endpoint: $MCP_URL\"\necho \" Protocol: $PROTOCOL_VERSION\"\necho \" Server: $SERVER_NAME v$SERVER_VERSION\"\necho \" Tools: $TOOL_COUNT available\"\necho \" Health tool: $HEALTH_RESULT\"\necho \" Session: single-session (official SDK)\"\necho \" REST prefetch: ${REST_RESP:-unknown} (limit, account: ${OV_ACCOUNT})\"\necho \" top_k rejected: ${TOPK_RESP:-unknown} (expected 400)\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\nif [[ \"$OVERALL_RC\" -eq 0 ]]; then\n log_ok \"MCP + REST prefetch verification PASSED\"\nelse\n log_error \"MCP + REST prefetch verification FAILED (${OVERALL_RC} REST contract check(s) below expectations)\"\nfi\nexit $OVERALL_RC","content_type":"application/x-sh; charset=utf-8","language":"bash","size":6342,"content_sha256":"9efdfc8091a85b368f2b13ed4a22f139c26a9761b82ec90838ba6c3cca9d4c37"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Huawei Cloud Agent Integration (OpenViking Long-Term Memory)","type":"text"}]},{"type":"paragraph","content":[{"text":"Integrate / unbind OpenViking long-term memory for coding agents running in bwrap sandboxes (","type":"text"},{"text":"/root/job-envs/sandboxes/","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Every agent uses its ","type":"text"},{"text":"native mechanism","type":"text","marks":[{"type":"strong"}]},{"text":" (MCP, HTTP memory provider, TS extension hooks, settings.json), so integration survives agent upgrades. Writes are ","type":"text"},{"text":"template-level persistent","type":"text","marks":[{"type":"strong"}]},{"text":": config is injected into ","type":"text"},{"text":"/root/template/\u003cagent>/start.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":", so sandbox ","type":"text"},{"text":"stop + start","type":"text","marks":[{"type":"code_inline"}]},{"text":" preserves the integration.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Supported Agents","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Agent","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mechanism","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CodeArts CLI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@openviking/opencode-plugin","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":".codeartsdoer/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenCode","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@openviking/opencode-plugin","type":"text","marks":[{"type":"code_inline"}]},{"text":" (npm mirror → GitHub fallback)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenClaw","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"clawhub:@openviking/openclaw-plugin","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"contextEngine","type":"text","marks":[{"type":"code_inline"}]},{"text":" slot (disables ","type":"text"},{"text":"session-memory","type":"text","marks":[{"type":"code_inline"}]},{"text":" hook, denies ","type":"text"},{"text":"memory_search","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hermes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Built-in ","type":"text"},{"text":"memory.provider: openviking","type":"text","marks":[{"type":"code_inline"}]},{"text":" (HTTP REST, no MCP)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"WorkSwarm","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Dual-channel: native provider + MCP (15 tools) + code-mode patch","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"KimiCode","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"hooks + MCP (session lifecycle) — ","type":"text"},{"text":"config.toml [[hooks]]","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"ov-kimi-hook.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" + MCP via ","type":"text"},{"text":"mcp.json","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DeepSeek Harness","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@openviking/dsh-memory-plugin","type":"text","marks":[{"type":"code_inline"}]},{"text":" bundle (on-demand from GitHub)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prime Agent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@openviking/pi-coding-agent-extension","type":"text","marks":[{"type":"code_inline"}]},{"text":" (on-demand from GitHub)","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Per-agent config paths and details: ","type":"text"},{"text":"references/agent-configs.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-configs.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OpenViking server healthy: ","type":"text"},{"text":"curl -s http://127.0.0.1:1933/health","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"healthy","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Target sandbox exists under ","type":"text"},{"text":"/root/job-envs/sandboxes/","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Host tools: ","type":"text"},{"text":"curl","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"python3","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"bash","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"npm","type":"text","marks":[{"type":"code_inline"}]},{"text":" for OpenCode/OpenClaw; ","type":"text"},{"text":"pip install --upgrade \"mcp>=2.0\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" for ","type":"text"},{"text":"verify_mcp.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No Huawei Cloud IAM required — operates on local bwrap sandboxes only.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"参数 (Parameters)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Flag","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--agent \u003cname>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (unless ","type":"text"},{"text":"--all","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Target agent","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--all","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes (unless ","type":"text"},{"text":"--agent","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Operate on all 8 agents","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--endpoint \u003curl>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenViking URL (default ","type":"text"},{"text":"http://127.0.0.1:1933","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--api-key \u003ckey>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"For auth-mode servers. Never echo in chat. Must be consumed by an agent config or ","type":"text"},{"text":"verify_mcp.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":"; an unconsumed ","type":"text"},{"text":"--api-key","type":"text","marks":[{"type":"code_inline"}]},{"text":" is reported as a warning.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Show changes without applying","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--yes","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"-y","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Skip confirmation (automation only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"status.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":": machine-readable output","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"核心命令","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"功能","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"命令","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"查看状态","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/status.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":"(","type":"text"},{"text":"--json","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"--agent \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"验证 MCP","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/verify_mcp.sh","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"集成","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/integrate.sh --agent \u003cname> [--endpoint URL] [--dry-run] [--yes]","type":"text","marks":[{"type":"code_inline"}]},{"text":"(或 ","type":"text"},{"text":"--all","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"解绑","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/unbind.sh --agent \u003cname> [--dry-run] [--yes]","type":"text","marks":[{"type":"code_inline"}]},{"text":"(或 ","type":"text"},{"text":"--all","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Workflow","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"SKILL_DIR=/root/.agents/skills/huawei-cloud-openviking-agent-integration\n$SKILL_DIR/scripts/status.sh # per-agent: template+live / template only / live only / none\n$SKILL_DIR/scripts/verify_mcp.sh # full MCP handshake (initialize → tools/list → health)\n$SKILL_DIR/scripts/integrate.sh --agent \u003cname> [--yes]\n$SKILL_DIR/scripts/unbind.sh --agent \u003cname> [--yes]","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Status first","type":"text","marks":[{"type":"strong"}]},{"text":" — never claim integrated before ","type":"text"},{"text":"status.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" confirms (do not fabricate state).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify MCP","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"verify_mcp.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" before integration.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Integrate / Unbind","type":"text","marks":[{"type":"strong"}]},{"text":" — each requires explicit ","type":"text"},{"text":"confirm","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or ","type":"text"},{"text":"--yes","type":"text","marks":[{"type":"code_inline"}]},{"text":"); ","type":"text"},{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" previews safely.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"States: ","type":"text"},{"text":"template + live","type":"text","marks":[{"type":"code_inline"}]},{"text":" = active; ","type":"text"},{"text":"template only","type":"text","marks":[{"type":"code_inline"}]},{"text":" = activates on restart; ","type":"text"},{"text":"live only","type":"text","marks":[{"type":"code_inline"}]},{"text":" = ","type":"text"},{"text":"lost on restart","type":"text","marks":[{"type":"strong"}]},{"text":" (warn user).","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Authorization & Safety","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"integrate.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"unbind.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" require explicit confirmation; never edit agent configs directly — go through the skill scripts.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Every config modification creates ","type":"text"},{"text":".bak.\u003ctimestamp>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (keep 5) for rollback.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--api-key","type":"text","marks":[{"type":"code_inline"}]},{"text":" values must never appear in output.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"能力边界(Cannot Do)","type":"text"}]},{"type":"paragraph","content":[{"text":"本技能只做\"OpenViking 记忆 ↔ 编码 agent 的集成/解绑/状态/验证\"。以下操作","type":"text"},{"text":"明确不做","type":"text","marks":[{"type":"strong"}]},{"text":",遇到请改走对应路径或明确报错:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"不直接读写 agent 配置文件、sandbox 内业务数据或用户代码——一律通过 ","type":"text"},{"text":"scripts/integrate.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"unbind.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等入口执行。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"不删除、不修改用户自装的第三方包/目录/数据。卸载只清理 OpenViking 自身产物(","type":"text"},{"text":"@openviking/*","type":"text","marks":[{"type":"code_inline"}]},{"text":" 插件、注入的配置段、OV 专属目录);","type":"text"},{"text":"node_modules","type":"text","marks":[{"type":"code_inline"}]},{"text":" 整目录删除等越界行为是缺陷,不执行。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"服务器不可达/健康检查失败时","type":"text"},{"text":"不报告成功","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"},{"text":"integrate.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"unbind.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"verify_mcp.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" 均先做健康门禁,失败即失败(exit 非 0),绝不假成功。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" 只预览改动,不写任何持久文件;预览后续真实执行仍需确认。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--api-key","type":"text","marks":[{"type":"code_inline"}]},{"text":" 必须被某个 agent 配置或验证流程实际消费;若传入却无处消费,脚本会告警(而非静默忽略)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"不做 embedding 模型切换、向量库重建、服务器自身配置变更——那是 ","type":"text"},{"text":"huawei-cloud-openviking-embedding-switch","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等技能的范围。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"不支持在集成/解绑过程中创建或修改 OpenViking 账号、用户、权限体系。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"不承诺\"查看 → 已集成\":一律以 ","type":"text"},{"text":"status.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" 实测结果为准,禁止凭模板猜测 live 状态。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Document","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"When to read","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent-configs.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/agent-configs.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Per-agent config paths/formats, MCP tools, server info","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"notes.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/notes.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Guardrails, env vars, sandbox restart, plugin sources, troubleshooting, adding a new agent","type":"text"}]}]}]}]}]},"metadata":{"date":"2026-11-02","name":"huawei-cloud-openviking-agent-integration","tags":["openviking","database","agent"],"author":"@skillopedia","source":{"stars":59,"repo_name":"huaweicloud-skills","origin_url":"https://github.com/huaweicloud/huaweicloud-skills/blob/HEAD/skills/devtools/extensions/huawei-cloud-openviking-agent-integration/SKILL.md","repo_owner":"huaweicloud","body_sha256":"eb033efd02b77c19364648c212413878e80ec950d884c9ba34014dc1cdb3f011","cluster_key":"cbe3a739ee5ae4091e19fb223869f1c9113a2ff91f447ea528cd9c3e60590efa","clean_bundle":{"format":"clean-skill-bundle-v1","source":"huaweicloud/huaweicloud-skills/skills/devtools/extensions/huawei-cloud-openviking-agent-integration/SKILL.md","text_bytes":383850,"source_commit":"bad25df61c331db250f0cc79f1fa7acee08b3fc8","skills_sh_rank":1644,"attachment_count":39,"text_attachments":39,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/devtools/extensions/huawei-cloud-openviking-agent-integration/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"integrations-apis","category_label":"Integrations"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"integrations-apis","metadata":{"license":"MIT","version":"1.2.2","category":"devtools"},"import_tag":"skills-sh-top20k-2026-11","description":"Integrate and unbind OpenViking long-term memory with coding agents. Supports 8 agents (CodeArts CLI, OpenCode, OpenClaw, Hermes, WorkSwarm, KimiCode, DeepSeek Harness, Prime Agent) via their native mechanism — MCP, HTTP memory provider, TypeScript extension hooks, or settings.json config. Both integration and unbinding require explicit user authorization.\nUse this skill when the user wants to: (1) integrate OpenViking memory into a coding agent, (2) unbind OpenViking from a coding agent, (3) check the integration status of all agents, (4) verify the OpenViking MCP endpoint, (5) rebuild the OpenClaw sandbox to apply template changes.\nTrigger words: \"OpenViking integration\", \"agent memory binding\", \"MCP setup\", \"OpenViking MCP\", \"integrate OpenViking\", \"unbind OpenViking\", \"记忆集成\", \"记忆解绑\", \"OpenViking 集成\", \"OpenViking 解绑\", \"agent long-term memory\", \"context database\".\n"}},"renderedAt":1793666258254}

Huawei Cloud Agent Integration (OpenViking Long-Term Memory) Integrate / unbind OpenViking long-term memory for coding agents running in bwrap sandboxes ( ). Every agent uses its native mechanism (MCP, HTTP memory provider, TS extension hooks, settings.json), so integration survives agent upgrades. Writes are template-level persistent : config is injected into , so sandbox preserves the integration. Supported Agents | Agent | Mechanism | |-------|-----------| | CodeArts CLI | → | | OpenCode | (npm mirror → GitHub fallback) | | OpenClaw | + slot (disables hook, denies ) | | Hermes | Built-in (…