Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 10 additions & 40 deletions bridges/kimaki.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
# + /etc/systemd/system/kimaki.service (ExecStartPre runs post-upgrade.sh)
# Local: $KIMAKI_DATA_DIR/kimaki-config/ for plugins, post-upgrade.sh +
# kill list (executed inline at upgrade time — no launchd
# ExecStartPre hook). Plugins are mirrored into the npm package as a
# compatibility target, but opencode.json loads the durable data-dir
# copy because `npm update -g kimaki` wipes package-local files.
# ExecStartPre hook). opencode.json loads plugins directly from this
# durable data-dir copy because `npm update -g kimaki` wipes package-
# local files.
# + $HOME/.local/bin/datamachine-kimaki-session
# + $HOME/.local/bin/datamachine-kimaki
# + $HOME/Library/LaunchAgents/com.wp.kimaki.plist on macOS.
Expand Down Expand Up @@ -196,39 +196,29 @@ bridge_sync_config() {
# and by ExecStartPre in kimaki.service). Config dir holds plugins +
# post-upgrade.sh + skills-kill-list.txt.
# Local: opencode.json points at $KIMAKI_DATA_DIR/kimaki-config/plugins, the
# durable source that survives `npm update -g kimaki`. We still mirror
# plugins into $(npm root -g)/kimaki/plugins for compatibility with old
# configs, and run post-upgrade.sh inline because launchd has no
# ExecStartPre hook.
# durable source that survives `npm update -g kimaki`. Existing configs
# that still reference package-local plugin paths are migrated by the
# opencode.json repair helper.
local KIMAKI_CONFIG_DIR
local KIMAKI_PLUGINS_DIR
local KIMAKI_NPM_PLUGINS_DIR=""
local BACKUP_DIR
if [ "$LOCAL_MODE" = true ]; then
KIMAKI_CONFIG_DIR="${KIMAKI_DATA_DIR}/kimaki-config"
local NPM_ROOT
NPM_ROOT="$(npm root -g 2>/dev/null || true)"
if [ -n "$NPM_ROOT" ]; then
KIMAKI_NPM_PLUGINS_DIR="${NPM_ROOT}/kimaki/plugins"
fi
KIMAKI_PLUGINS_DIR="${KIMAKI_CONFIG_DIR}/plugins"
BACKUP_DIR="${KIMAKI_DATA_DIR}/backups/kimaki-config.$TIMESTAMP"
log "Phase 2: Syncing kimaki config (local mode)..."
log " Config dir: $KIMAKI_CONFIG_DIR"
log " Plugins dir: $KIMAKI_PLUGINS_DIR (durable opencode target)"
if [ -n "$KIMAKI_NPM_PLUGINS_DIR" ]; then
log " NPM mirror: $KIMAKI_NPM_PLUGINS_DIR (compatibility)"
fi
else
KIMAKI_CONFIG_DIR="/opt/kimaki-config"
KIMAKI_PLUGINS_DIR="/opt/kimaki-config/plugins"
BACKUP_DIR="/opt/kimaki-config.backup.$TIMESTAMP"
log "Phase 2: Syncing /opt/kimaki-config..."
fi

# Local opencode loads from the durable kimaki-config dir. If the npm package
# is unavailable, skip only the compatibility mirror; do not skip installing
# the policy plugins themselves.
# Local opencode loads from the durable kimaki-config dir. Do not mirror these
# plugins into the npm package; `npm update -g kimaki` wipes that directory and
# the repair helper migrates older opencode.json files away from it.

# VPS: if /opt/kimaki-config is missing, this install predates v0.4.0 (when
# setup.sh started creating it). We're in the kimaki dispatch branch, so
Expand Down Expand Up @@ -259,9 +249,7 @@ bridge_sync_config() {
fi
fi

# Copy plugins to the durable target that opencode.json loads. On local,
# additionally mirror to the npm package for older configs that still point
# there; the mirror is best-effort because npm updates wipe it.
# Copy plugins to the durable target that opencode.json loads.
if [ -d "$SCRIPT_DIR/bridges/kimaki/plugins" ]; then
if [ "$DRY_RUN" = false ]; then
mkdir -p "$KIMAKI_CONFIG_DIR/plugins" 2>/dev/null || true
Expand All @@ -283,24 +271,6 @@ bridge_sync_config() {
UPDATED_ITEMS+=("kimaki-config/plugins/$name")
fi
fi
# Compatibility mirror for older local opencode.json files. VPS uses the
# durable target directly, so there is no separate mirror there.
if [ "$LOCAL_MODE" = true ] && [ -n "$KIMAKI_NPM_PLUGINS_DIR" ]; then
if [ "$DRY_RUN" = true ]; then
if ! cmp -s "$plugin_file" "$KIMAKI_NPM_PLUGINS_DIR/$name" 2>/dev/null; then
echo -e "${BLUE}[dry-run]${NC} Would update $KIMAKI_NPM_PLUGINS_DIR/$name"
else
echo -e "${BLUE}[dry-run]${NC} $name npm mirror: unchanged"
fi
else
mkdir -p "$KIMAKI_NPM_PLUGINS_DIR" 2>/dev/null || true
if ! cmp -s "$plugin_file" "$KIMAKI_NPM_PLUGINS_DIR/$name" 2>/dev/null; then
cp "$plugin_file" "$KIMAKI_NPM_PLUGINS_DIR/$name"
log " Updated $KIMAKI_NPM_PLUGINS_DIR/$name (compatibility mirror)"
UPDATED_ITEMS+=("kimaki npm mirror/$name")
fi
fi
fi
done
fi

Expand Down
92 changes: 44 additions & 48 deletions bridges/kimaki/post-upgrade.sh
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
#!/usr/bin/env bash
# post-upgrade.sh — Enforce kimaki skill + plugin state on every restart.
# post-upgrade.sh — Enforce kimaki skill state and validate plugin state.
#
# Three symmetric passes run against the npm-installed kimaki package:
# Three passes run against the npm-installed kimaki package and persistent
# kimaki-config directory:
# 1. KILL — remove unwanted bundled kimaki skills listed in
# skills-kill-list.txt (target: $(npm root -g)/kimaki/skills/).
# 2. RESTORE skills — re-copy wp-coding-agents skills from the persistent
# source dir (kimaki-config/skills/) into kimaki/skills/.
# 3. RESTORE plugins — re-copy wp-coding-agents opencode plugins from the
# persistent source dir (kimaki-config/plugins/) into
# kimaki/plugins/. opencode.json references the plugin .ts
# files at $(npm root -g)/kimaki/plugins/<file>.ts; without
# this restore pass dm-context-filter.ts and dm-agent-sync.ts
# silently disappear after every `npm update -g kimaki` and
# Discord agents lose their context-filter / agent-sync
# policies until the next manual upgrade.sh run.
# 3. VERIFY plugins — confirm required wp-coding-agents opencode plugins
# exist at the persistent kimaki-config/plugins path loaded by
# opencode.json. Local installs no longer restore plugins into
# $(npm root -g)/kimaki/plugins because package-local files are
# wiped by `npm update -g kimaki`.
#
# `npm update -g kimaki` wipes both kimaki/skills/ AND kimaki/plugins/, so
# the persistent kimaki-config/ dir is the source of truth and this script
# rehydrates the npm install on every kimaki restart.
# `npm update -g kimaki` still wipes kimaki/skills/, so skills are rehydrated
# from persistent kimaki-config/ on every restart. Plugins are loaded directly
# from persistent kimaki-config/plugins and only need validation here.
#
# Invoked two ways:
# VPS: ExecStartPre in kimaki.service (runs on every service start).
Expand All @@ -28,10 +26,9 @@
# 2. $(npm root -g)/kimaki/skills (works on macOS + Linux when npm is on PATH)
# 3. /usr/lib/node_modules/kimaki/skills (Linux VPS fallback when npm absent)
#
# Plugins dir resolution priority (mirrors skills resolution):
# Plugin target dir resolution priority:
# 1. KIMAKI_PLUGINS_DIR env var (explicit override)
# 2. $(npm root -g)/kimaki/plugins
# 3. /usr/lib/node_modules/kimaki/plugins (Linux VPS fallback when npm absent)
# 2. Persistent plugin source dir below (default for local and VPS)
#
# Persistent skill source dir resolution priority:
# 1. KIMAKI_SKILL_SOURCE_DIR env var (explicit override)
Expand Down Expand Up @@ -64,14 +61,6 @@ else
SKILLS_DIR="/usr/lib/node_modules/kimaki/skills"
fi

if [[ -n "${KIMAKI_PLUGINS_DIR:-}" ]]; then
PLUGINS_DIR="$KIMAKI_PLUGINS_DIR"
elif [[ -n "$NPM_ROOT" ]]; then
PLUGINS_DIR="$NPM_ROOT/kimaki/plugins"
else
PLUGINS_DIR="/usr/lib/node_modules/kimaki/plugins"
fi

KILL_LIST="$(dirname "$0")/skills-kill-list.txt"
REQUIRED_PLUGINS=(dm-context-filter.ts dm-agent-sync.ts)

Expand Down Expand Up @@ -151,13 +140,11 @@ else
fi

# ----------------------------------------------------------------------------
# Pass 3: RESTORE plugins — re-copy wp-coding-agents opencode plugins from
# the persistent source dir into the npm-managed plugins dir.
#
# opencode.json references each plugin by absolute path at
# $(npm root -g)/kimaki/plugins/<file>.ts. The kimaki npm package does NOT
# ship a plugins/ dir, so this directory only ever exists because we put it
# there. `npm update -g kimaki` wipes it clean every time.
# Pass 3: VERIFY plugins — opencode.json now loads wp-coding-agents plugins
# directly from persistent kimaki-config/plugins. When the target and source are
# the same directory (the default), there is nothing to restore. An explicit
# KIMAKI_PLUGINS_DIR override still receives a best-effort copy for operator
# controlled compatibility scenarios.
# ----------------------------------------------------------------------------

if [[ -n "${KIMAKI_PLUGIN_SOURCE_DIR:-}" ]]; then
Expand All @@ -171,27 +158,36 @@ else
fi

plugins_restored=0
if [[ -n "${KIMAKI_PLUGINS_DIR:-}" ]]; then
PLUGINS_DIR="$KIMAKI_PLUGINS_DIR"
else
PLUGINS_DIR="$PLUGIN_SOURCE_DIR"
fi

if [[ -d "$PLUGIN_SOURCE_DIR" ]]; then
# Ensure the npm-managed plugins dir exists before copying.
mkdir -p "$PLUGINS_DIR" 2>/dev/null || true
if [[ ! -d "$PLUGINS_DIR" ]]; then
echo "kimaki-config: could not create plugins dir at $PLUGINS_DIR, skipping plugin restore"
if [[ "$PLUGINS_DIR" == "$PLUGIN_SOURCE_DIR" ]]; then
echo "kimaki-config: plugin restore not needed; opencode loads persistent plugins at $PLUGIN_SOURCE_DIR"
else
shopt -s nullglob
for plugin_file in "$PLUGIN_SOURCE_DIR"/*.ts; do
plugin_name="$(basename "$plugin_file")"
target="$PLUGINS_DIR/$plugin_name"
# Idempotent: only copy if missing or different. cmp returns 0 on match.
if ! cmp -s "$plugin_file" "$target" 2>/dev/null; then
cp "$plugin_file" "$target"
echo "kimaki-config: restored plugin $plugin_name"
plugins_restored=$((plugins_restored + 1))
fi
done
shopt -u nullglob
mkdir -p "$PLUGINS_DIR" 2>/dev/null || true
if [[ ! -d "$PLUGINS_DIR" ]]; then
echo "kimaki-config: could not create plugins dir at $PLUGINS_DIR, skipping plugin restore"
else
shopt -s nullglob
for plugin_file in "$PLUGIN_SOURCE_DIR"/*.ts; do
plugin_name="$(basename "$plugin_file")"
target="$PLUGINS_DIR/$plugin_name"
# Idempotent: only copy if missing or different. cmp returns 0 on match.
if ! cmp -s "$plugin_file" "$target" 2>/dev/null; then
cp "$plugin_file" "$target"
echo "kimaki-config: restored plugin $plugin_name"
plugins_restored=$((plugins_restored + 1))
fi
done
shopt -u nullglob
fi
fi
else
echo "kimaki-config: WARNING: persistent plugin source dir not found at $PLUGIN_SOURCE_DIR; dm-context-filter.ts and dm-agent-sync.ts cannot be restored"
echo "kimaki-config: WARNING: persistent plugin source dir not found at $PLUGIN_SOURCE_DIR; dm-context-filter.ts and dm-agent-sync.ts cannot be loaded"
fi

missing_required_plugins=0
Expand Down
58 changes: 54 additions & 4 deletions lib/repair-opencode-json.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@
import os
import shutil
import sys
from typing import List
from typing import List, Tuple


MANAGED_KIMAKI_PLUGIN_NAMES = {"dm-context-filter.ts", "dm-agent-sync.ts"}


def expected_plugins(
Expand Down Expand Up @@ -113,6 +116,38 @@ def diff_plugins(current: List[str], expected: List[str]) -> dict:
}


def normalize_managed_kimaki_plugin_paths(
current: List[str], kimaki_plugins_dir: str
) -> Tuple[List[str], List[dict]]:
"""Rewrite managed Kimaki plugin paths to the durable configured dir.

Older local installs pointed opencode.json at npm package-local plugin files
under ``$(npm root -g)/kimaki/plugins``. Those files disappear on
``npm update -g kimaki``. Treat any managed plugin basename outside the
configured persistent dir as a stale wp-coding-agents-owned entry and rewrite
it in place, preserving user-added plugins.
"""
normalized: List[str] = []
rewrites: List[dict] = []
seen = set()
plugins_dir = kimaki_plugins_dir.rstrip("/")

for plugin in current:
basename = os.path.basename(plugin)
replacement = plugin
if basename in MANAGED_KIMAKI_PLUGIN_NAMES:
expected = f"{plugins_dir}/{basename}"
if os.path.dirname(plugin).rstrip("/") != plugins_dir:
replacement = expected
rewrites.append({"from": plugin, "to": replacement})

if replacement not in seen:
normalized.append(replacement)
seen.add(replacement)

return normalized, rewrites


def repair(
data: dict, expected: List[str], preserve_extras: bool = False
) -> List[str]:
Expand Down Expand Up @@ -336,6 +371,13 @@ def main() -> int:
)

current: List[str] = list(data.get("plugin", []))
normalized_current = current
plugin_rewrites: List[dict] = []
if args.runtime == "opencode" and args.chat_bridge == "kimaki":
normalized_current, plugin_rewrites = normalize_managed_kimaki_plugin_paths(
current,
args.kimaki_plugins_dir,
)

# Claude Code / Studio Code: no plugin array concept here. Report ok
# if current is empty or absent; otherwise let user know we skipped.
Expand All @@ -354,8 +396,8 @@ def main() -> int:
)
return 0

diff = diff_plugins(current, expected)
has_plugin_drift = bool(diff["missing"] or diff["unexpected"])
diff = diff_plugins(normalized_current, expected)
has_plugin_drift = bool(diff["missing"] or diff["unexpected"] or plugin_rewrites)
has_prompt_drift = prompt_result["status"] == "needed"
has_agent_cleanup_drift = agent_cleanup_result["status"] == "needed"
has_any_drift = has_plugin_drift or has_prompt_drift or has_agent_cleanup_drift
Expand Down Expand Up @@ -383,6 +425,8 @@ def main() -> int:
"prompt_migration": prompt_result["status"],
"agent_cleanup": agent_cleanup_result["status"],
}
if plugin_rewrites:
result["rewritten"] = plugin_rewrites
if has_plugin_drift:
result["missing"] = diff["missing"]
result["unexpected"] = diff["unexpected"]
Expand All @@ -408,7 +452,9 @@ def main() -> int:
if has_plugin_drift and not plugin_skipped:
# --apply: replace with exactly `expected` (removes unexpected).
# --additive: merge missing entries, preserving user additions.
data["plugin"] = repair(data, expected, preserve_extras=args.additive)
repaired_data = dict(data)
repaired_data["plugin"] = normalized_current
data["plugin"] = repair(repaired_data, expected, preserve_extras=args.additive)

prompt_migration_status = "ok"
if has_prompt_drift:
Expand Down Expand Up @@ -438,6 +484,8 @@ def main() -> int:
"prompt_migration": prompt_migration_status,
"agent_cleanup": "removed" if removed_agent_blocks else "ok",
}
if plugin_rewrites:
result["rewritten"] = plugin_rewrites
if removed_agent_blocks:
result["agent_cleanup_removed"] = removed_agent_blocks
if still_unexpected:
Expand All @@ -455,6 +503,8 @@ def main() -> int:
"prompt_migration": prompt_migration_status,
"agent_cleanup": "removed" if removed_agent_blocks else "ok",
}
if plugin_rewrites:
result["rewritten"] = plugin_rewrites
if removed_agent_blocks:
result["agent_cleanup_removed"] = removed_agent_blocks
print(json.dumps(result))
Expand Down
9 changes: 6 additions & 3 deletions tests/effective-prompt/__snapshots__/default.baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ kimaki session archive --session ses_EFFECTIVE_PROMPT_TEST

Only do this when the user explicitly asks to close or archive the thread, and only after your final message.

## searching discord users
## discord user mentions

To search for Discord users in a guild (needed for mentions like <@userId>), run:
Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use `<@userId>` in message text or pass the ID to `--user`.
The current user's ID is available in the per-turn `<discord-user ... user-id="..." />` metadata.

To search for Discord users in a guild as a best-effort fallback, run:

kimaki user list --guild 1493321868415996064 --query "username"

This returns user IDs you can use for Discord mentions.
This returns user IDs you can use for Discord mentions. It can fail when Server Members Intent is disabled, so prefer IDs from existing Discord metadata or raw mentions when possible.

# List all registered projects with their channel IDs
kimaki project list --json # machine-readable output
Expand Down
9 changes: 6 additions & 3 deletions tests/effective-prompt/__snapshots__/default.filtered.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ kimaki session archive --session ses_EFFECTIVE_PROMPT_TEST

Only do this when the user explicitly asks to close or archive the thread, and only after your final message.

## searching discord users
## discord user mentions

To search for Discord users in a guild (needed for mentions like <@userId>), run:
Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use `<@userId>` in message text or pass the ID to `--user`.
The current user's ID is available in the per-turn `<discord-user ... user-id="..." />` metadata.

To search for Discord users in a guild as a best-effort fallback, run:

kimaki user list --guild 1493321868415996064 --query "username"

This returns user IDs you can use for Discord mentions.
This returns user IDs you can use for Discord mentions. It can fail when Server Members Intent is disabled, so prefer IDs from existing Discord metadata or raw mentions when possible.

## submodules

Expand Down
Loading
Loading