handoff-mcp-server 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -4
- package/bin/handoff-mcp.js +56 -13
- package/bin/resolve-binary.js +122 -0
- package/package.json +14 -9
- package/Cargo.lock +0 -686
- package/Cargo.toml +0 -30
- package/scripts/cargo-env.sh +0 -29
- package/scripts/handoff-memory-hook.py +0 -208
- package/scripts/install-local.sh +0 -109
- package/scripts/postinstall.js +0 -50
- package/scripts/sync-plugin-skills.sh +0 -35
- package/scripts/sync-plugin-version.sh +0 -85
- package/scripts/sync-workflow-inline.sh +0 -138
- package/src/cli.rs +0 -551
- package/src/context/injection.rs +0 -276
- package/src/context/mod.rs +0 -129
- package/src/lib.rs +0 -5
- package/src/main.rs +0 -157
- package/src/mcp/handlers/assignees.rs +0 -254
- package/src/mcp/handlers/auto_schedule.rs +0 -489
- package/src/mcp/handlers/bulk_update.rs +0 -155
- package/src/mcp/handlers/calendar.rs +0 -196
- package/src/mcp/handlers/capacity.rs +0 -318
- package/src/mcp/handlers/check_criterion.rs +0 -70
- package/src/mcp/handlers/config.rs +0 -402
- package/src/mcp/handlers/config_crud.rs +0 -183
- package/src/mcp/handlers/dashboard.rs +0 -214
- package/src/mcp/handlers/docs.rs +0 -2288
- package/src/mcp/handlers/docs_query.rs +0 -1335
- package/src/mcp/handlers/fork_session.rs +0 -91
- package/src/mcp/handlers/get_session.rs +0 -48
- package/src/mcp/handlers/get_task.rs +0 -53
- package/src/mcp/handlers/import_context.rs +0 -470
- package/src/mcp/handlers/init.rs +0 -28
- package/src/mcp/handlers/list_sessions.rs +0 -187
- package/src/mcp/handlers/list_tasks.rs +0 -308
- package/src/mcp/handlers/load_context.rs +0 -361
- package/src/mcp/handlers/log_time.rs +0 -67
- package/src/mcp/handlers/memory.rs +0 -961
- package/src/mcp/handlers/merge_sessions.rs +0 -103
- package/src/mcp/handlers/metrics.rs +0 -196
- package/src/mcp/handlers/milestones.rs +0 -102
- package/src/mcp/handlers/mod.rs +0 -140
- package/src/mcp/handlers/refer.rs +0 -307
- package/src/mcp/handlers/referrals.rs +0 -74
- package/src/mcp/handlers/save_context.rs +0 -354
- package/src/mcp/handlers/task_checklist.rs +0 -507
- package/src/mcp/handlers/timer.rs +0 -529
- package/src/mcp/handlers/update_session.rs +0 -197
- package/src/mcp/handlers/update_task.rs +0 -452
- package/src/mcp/mod.rs +0 -6
- package/src/mcp/protocol.rs +0 -41
- package/src/mcp/resources.rs +0 -57
- package/src/mcp/router.rs +0 -154
- package/src/mcp/tools.rs +0 -1522
- package/src/mcp/types.rs +0 -108
- package/src/setup.rs +0 -1212
- package/src/storage/config.rs +0 -578
- package/src/storage/docs/frontmatter.rs +0 -509
- package/src/storage/docs/mod.rs +0 -835
- package/src/storage/docs/model.rs +0 -708
- package/src/storage/docs/reassemble.rs +0 -167
- package/src/storage/docs/split.rs +0 -377
- package/src/storage/git.rs +0 -47
- package/src/storage/memory/injected.rs +0 -340
- package/src/storage/memory/mod.rs +0 -236
- package/src/storage/memory/model.rs +0 -127
- package/src/storage/mod.rs +0 -96
- package/src/storage/referrals.rs +0 -248
- package/src/storage/sessions.rs +0 -859
- package/src/storage/tasks.rs +0 -957
- package/templates/claude-md-section.md +0 -12
package/scripts/cargo-env.sh
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# shellcheck shell=sh
|
|
2
|
-
# cargo-env.sh — make cargo commands work across heterogeneous dev environments.
|
|
3
|
-
#
|
|
4
|
-
# Some environments (e.g. a Docker image where CARGO_HOME=/usr/local/cargo is
|
|
5
|
-
# owned by root) expose a CARGO_HOME that the current user cannot write to.
|
|
6
|
-
# cargo then fails to download/cache crates or fetch the advisory-db, breaking
|
|
7
|
-
# the git hooks even though nothing is wrong with the code.
|
|
8
|
-
#
|
|
9
|
-
# This script detects an unwritable CARGO_HOME and transparently falls back to
|
|
10
|
-
# the per-user $HOME/.cargo, which every supported environment can write to.
|
|
11
|
-
# When CARGO_HOME is already writable (local macOS, a synced Mac, or simply
|
|
12
|
-
# unset so cargo uses its default ~/.cargo) it leaves the environment untouched.
|
|
13
|
-
#
|
|
14
|
-
# Usage (from a hook or shell):
|
|
15
|
-
# . scripts/cargo-env.sh && cargo <args>
|
|
16
|
-
|
|
17
|
-
# Resolve the directory cargo would use right now.
|
|
18
|
-
_cargo_home="${CARGO_HOME:-$HOME/.cargo}"
|
|
19
|
-
|
|
20
|
-
# Probe writability with a temp file; fall back to $HOME/.cargo if we can't write.
|
|
21
|
-
if ! ( mkdir -p "$_cargo_home" 2>/dev/null && touch "$_cargo_home/.cargo-env-wtest" 2>/dev/null ); then
|
|
22
|
-
export CARGO_HOME="$HOME/.cargo"
|
|
23
|
-
mkdir -p "$CARGO_HOME" 2>/dev/null || true
|
|
24
|
-
else
|
|
25
|
-
rm -f "$_cargo_home/.cargo-env-wtest" 2>/dev/null || true
|
|
26
|
-
export CARGO_HOME="$_cargo_home"
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
unset _cargo_home
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Claude Code hook wrapper for handoff-mcp project memory.
|
|
3
|
-
|
|
4
|
-
This is the **fallback path** for memory auto-injection. The preferred wiring is
|
|
5
|
-
a native ``mcp_tool`` hook that calls ``handoff_memory_query`` / ``handoff_memory_cleanup``
|
|
6
|
-
directly (see README "Project Memory"); this script exists for Claude Code
|
|
7
|
-
versions that lack the ``mcp_tool`` hook type, by translating a Claude Code hook
|
|
8
|
-
event into a single JSON-RPC ``tools/call`` against the handoff-mcp server and
|
|
9
|
-
emitting the result as ``hookSpecificOutput.additionalContext``.
|
|
10
|
-
|
|
11
|
-
It speaks the server's line-delimited JSON-RPC over stdio: one request object on
|
|
12
|
-
one line in, one response object on one line out. ``handoff_memory_query`` /
|
|
13
|
-
``handoff_memory_cleanup`` return their payload as a JSON *string* inside
|
|
14
|
-
``result.content[0].text`` (so both this wrapper and the native hook path parse
|
|
15
|
-
it identically), which this script re-parses.
|
|
16
|
-
|
|
17
|
-
Wiring (in ``~/.claude/settings.json`` — do NOT commit this into a repo):
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
"hooks": {
|
|
21
|
-
"UserPromptSubmit": [
|
|
22
|
-
{ "hooks": [ { "type": "command",
|
|
23
|
-
"command": "handoff-mcp-memory-hook" } ] }
|
|
24
|
-
],
|
|
25
|
-
"PreToolUse": [
|
|
26
|
-
{ "matcher": "Edit|Write|MultiEdit",
|
|
27
|
-
"hooks": [ { "type": "command",
|
|
28
|
-
"command": "handoff-mcp-memory-hook" } ] }
|
|
29
|
-
],
|
|
30
|
-
"SessionStart": [
|
|
31
|
-
{ "hooks": [ { "type": "command",
|
|
32
|
-
"command": "handoff-mcp-memory-hook" } ] }
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
The same script handles all three events; it picks the tool from
|
|
38
|
-
``hook_event_name`` on stdin.
|
|
39
|
-
|
|
40
|
-
Environment:
|
|
41
|
-
- ``HANDOFF_MCP_BIN`` — path to the ``handoff-mcp`` binary (default: ``handoff-mcp``
|
|
42
|
-
resolved on ``PATH``).
|
|
43
|
-
|
|
44
|
-
Design contract: this script must **never break the session**. On any error
|
|
45
|
-
(binary missing, bad JSON, timeout) it prints nothing and exits 0 — a memory
|
|
46
|
-
miss is silent, never a blocked prompt.
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
# `X | None` annotations are evaluated lazily so this runs on Python 3.7+
|
|
50
|
-
# (a hook host may not have 3.10). The annotations are never evaluated at runtime.
|
|
51
|
-
from __future__ import annotations
|
|
52
|
-
|
|
53
|
-
import json
|
|
54
|
-
import os
|
|
55
|
-
import shutil
|
|
56
|
-
import subprocess
|
|
57
|
-
import sys
|
|
58
|
-
|
|
59
|
-
# Map a Claude Code hook event to the memory tool it drives.
|
|
60
|
-
QUERY_EVENTS = {"UserPromptSubmit", "PreToolUse"}
|
|
61
|
-
CLEANUP_EVENTS = {"SessionStart"}
|
|
62
|
-
|
|
63
|
-
# How long to wait for the server to answer one request, in seconds. The query
|
|
64
|
-
# is in-memory and sub-millisecond; this is just a safety net so a hung binary
|
|
65
|
-
# can never stall the prompt.
|
|
66
|
-
TIMEOUT_SECONDS = 5.0
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _server_bin() -> str | None:
|
|
70
|
-
"""Resolve the handoff-mcp binary path, or None if it can't be found."""
|
|
71
|
-
explicit = os.environ.get("HANDOFF_MCP_BIN")
|
|
72
|
-
if explicit:
|
|
73
|
-
return explicit if os.path.exists(explicit) else None
|
|
74
|
-
return shutil.which("handoff-mcp")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _call(bin_path: str, tool: str, arguments: dict) -> dict | None:
|
|
78
|
-
"""Send one ``tools/call`` line and parse the inner JSON payload.
|
|
79
|
-
|
|
80
|
-
Returns the decoded inner object (e.g. ``{"memories": [...]}`), or None on
|
|
81
|
-
any transport / decode failure. The server is stateless per line, so no
|
|
82
|
-
``initialize`` handshake is needed — a bare ``tools/call`` is answered.
|
|
83
|
-
"""
|
|
84
|
-
request = {
|
|
85
|
-
"jsonrpc": "2.0",
|
|
86
|
-
"id": 1,
|
|
87
|
-
"method": "tools/call",
|
|
88
|
-
"params": {"name": tool, "arguments": arguments},
|
|
89
|
-
}
|
|
90
|
-
try:
|
|
91
|
-
proc = subprocess.run(
|
|
92
|
-
[bin_path],
|
|
93
|
-
input=json.dumps(request) + "\n",
|
|
94
|
-
capture_output=True,
|
|
95
|
-
text=True,
|
|
96
|
-
timeout=TIMEOUT_SECONDS,
|
|
97
|
-
)
|
|
98
|
-
except (OSError, subprocess.TimeoutExpired):
|
|
99
|
-
return None
|
|
100
|
-
|
|
101
|
-
# The response is the first non-empty stdout line (stderr carries the
|
|
102
|
-
# startup banner and is ignored).
|
|
103
|
-
line = next((ln for ln in proc.stdout.splitlines() if ln.strip()), None)
|
|
104
|
-
if not line:
|
|
105
|
-
return None
|
|
106
|
-
try:
|
|
107
|
-
envelope = json.loads(line)
|
|
108
|
-
text = envelope["result"]["content"][0]["text"]
|
|
109
|
-
return json.loads(text)
|
|
110
|
-
except (json.JSONDecodeError, KeyError, IndexError, TypeError):
|
|
111
|
-
return None
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _file_paths_from_tool_input(tool_input) -> list[str]:
|
|
115
|
-
"""Pull file paths out of a PreToolUse ``tool_input`` (Edit/Write/MultiEdit)."""
|
|
116
|
-
if not isinstance(tool_input, dict):
|
|
117
|
-
return []
|
|
118
|
-
paths = []
|
|
119
|
-
fp = tool_input.get("file_path")
|
|
120
|
-
if isinstance(fp, str) and fp:
|
|
121
|
-
paths.append(fp)
|
|
122
|
-
# MultiEdit and similar may carry an edits array; each can name a file.
|
|
123
|
-
for edit in tool_input.get("edits", []) or []:
|
|
124
|
-
if isinstance(edit, dict):
|
|
125
|
-
efp = edit.get("file_path")
|
|
126
|
-
if isinstance(efp, str) and efp:
|
|
127
|
-
paths.append(efp)
|
|
128
|
-
return paths
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _emit(event: str, context: str) -> None:
|
|
132
|
-
"""Print a Claude Code hook output object carrying additionalContext."""
|
|
133
|
-
if not context:
|
|
134
|
-
return
|
|
135
|
-
out = {
|
|
136
|
-
"hookSpecificOutput": {
|
|
137
|
-
"hookEventName": event,
|
|
138
|
-
"additionalContext": context,
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
print(json.dumps(out))
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _format_memories(payload: dict) -> str:
|
|
145
|
-
"""Turn a ``handoff_memory_query`` payload into an injectable context block."""
|
|
146
|
-
memories = payload.get("memories") or []
|
|
147
|
-
if not memories:
|
|
148
|
-
return ""
|
|
149
|
-
lines = ["Relevant project memories (handoff-mcp):"]
|
|
150
|
-
for m in memories:
|
|
151
|
-
text = (m.get("text") or "").strip()
|
|
152
|
-
if not text:
|
|
153
|
-
continue
|
|
154
|
-
kind = m.get("kind") or "memory"
|
|
155
|
-
lines.append(f"- [{kind}] {text}")
|
|
156
|
-
# Only a header with no bullets means nothing useful to inject.
|
|
157
|
-
return "\n".join(lines) if len(lines) > 1 else ""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def main() -> int:
|
|
161
|
-
raw = sys.stdin.read()
|
|
162
|
-
try:
|
|
163
|
-
hook = json.loads(raw) if raw.strip() else {}
|
|
164
|
-
except json.JSONDecodeError:
|
|
165
|
-
return 0 # malformed hook input — stay silent, never block
|
|
166
|
-
|
|
167
|
-
event = hook.get("hook_event_name") or ""
|
|
168
|
-
bin_path = _server_bin()
|
|
169
|
-
if not bin_path:
|
|
170
|
-
return 0 # server not installed — nothing to inject
|
|
171
|
-
|
|
172
|
-
project_dir = hook.get("cwd") or os.getcwd()
|
|
173
|
-
session_id = hook.get("session_id")
|
|
174
|
-
|
|
175
|
-
if event in QUERY_EVENTS:
|
|
176
|
-
# UserPromptSubmit → match the prompt. PreToolUse → match the file(s).
|
|
177
|
-
prompt = hook.get("prompt") or ""
|
|
178
|
-
tool_name = hook.get("tool_name")
|
|
179
|
-
file_paths = _file_paths_from_tool_input(hook.get("tool_input"))
|
|
180
|
-
# Text fed to BM25: the prompt for UserPromptSubmit, else the file paths
|
|
181
|
-
# (the server also adds basenames + tool_name to the query).
|
|
182
|
-
text = prompt if prompt else " ".join(file_paths)
|
|
183
|
-
arguments = {"project_dir": project_dir, "text": text}
|
|
184
|
-
if session_id:
|
|
185
|
-
arguments["session_id"] = session_id
|
|
186
|
-
if tool_name:
|
|
187
|
-
arguments["tool_name"] = tool_name
|
|
188
|
-
if file_paths:
|
|
189
|
-
arguments["file_paths"] = file_paths
|
|
190
|
-
|
|
191
|
-
payload = _call(bin_path, "handoff_memory_query", arguments)
|
|
192
|
-
if payload:
|
|
193
|
-
_emit(event, _format_memories(payload))
|
|
194
|
-
return 0
|
|
195
|
-
|
|
196
|
-
if event in CLEANUP_EVENTS:
|
|
197
|
-
# Housekeeping only: merge exact duplicates and gc sidecars. We do not
|
|
198
|
-
# inject the cleanup recommendations as context (that is for an explicit
|
|
199
|
-
# AI-driven pass), so there is no additionalContext to emit here.
|
|
200
|
-
_call(bin_path, "handoff_memory_cleanup", {"project_dir": project_dir})
|
|
201
|
-
return 0
|
|
202
|
-
|
|
203
|
-
# Unknown event — nothing to do.
|
|
204
|
-
return 0
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if __name__ == "__main__":
|
|
208
|
-
sys.exit(main())
|
package/scripts/install-local.sh
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Install handoff-mcp locally: binary, skills, and plugin caches.
|
|
3
|
-
# Usage: ./scripts/install-local.sh
|
|
4
|
-
#
|
|
5
|
-
# SCOPE: this script rebuilds the binary and refreshes the plugin CACHE only.
|
|
6
|
-
# It does NOT register the marketplace (known_marketplaces.json) or enable the
|
|
7
|
-
# plugin (enabledPlugins in settings.json), and it only updates
|
|
8
|
-
# installed_plugins.json entries that already exist. So on a fresh machine it
|
|
9
|
-
# does not activate the plugin by itself — bootstrap once with:
|
|
10
|
-
# /plugin marketplace add /absolute/path/to/handoff-mcp
|
|
11
|
-
# /plugin install handoff-mcp@handoff-mcp-marketplace
|
|
12
|
-
# /reload-plugins
|
|
13
|
-
# Thereafter, re-run this script to pick up code changes, then restart Claude
|
|
14
|
-
# Code (or /reload-plugins). See README "Installing the local development
|
|
15
|
-
# version instead".
|
|
16
|
-
set -euo pipefail
|
|
17
|
-
|
|
18
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
-
ROOT="$(dirname "$SCRIPT_DIR")"
|
|
20
|
-
CLAUDE_DIR="${HOME}/.claude"
|
|
21
|
-
CACHE_DIR="${CLAUDE_DIR}/plugins/cache/handoff-mcp-marketplace"
|
|
22
|
-
INSTALLED_JSON="${CLAUDE_DIR}/plugins/installed_plugins.json"
|
|
23
|
-
|
|
24
|
-
cd "$ROOT"
|
|
25
|
-
|
|
26
|
-
# ---------- 1. Binary ----------
|
|
27
|
-
echo "==> Building release binary..."
|
|
28
|
-
cargo build --release
|
|
29
|
-
|
|
30
|
-
mkdir -p "${HOME}/.local/bin"
|
|
31
|
-
rm -f "${HOME}/.local/bin/handoff-mcp"
|
|
32
|
-
cp target/release/handoff-mcp "${HOME}/.local/bin/handoff-mcp"
|
|
33
|
-
echo " Installed: $(${HOME}/.local/bin/handoff-mcp --version)"
|
|
34
|
-
|
|
35
|
-
# ---------- 2. Bundled skills ----------
|
|
36
|
-
echo "==> Syncing skills..."
|
|
37
|
-
skill_count=0
|
|
38
|
-
for skill in skills/*/; do
|
|
39
|
-
name=$(basename "$skill")
|
|
40
|
-
mkdir -p "${CLAUDE_DIR}/skills/${name}"
|
|
41
|
-
cp "${skill}SKILL.md" "${CLAUDE_DIR}/skills/${name}/SKILL.md"
|
|
42
|
-
skill_count=$((skill_count + 1))
|
|
43
|
-
done
|
|
44
|
-
echo " ${skill_count} skills synced"
|
|
45
|
-
|
|
46
|
-
# ---------- 3. Sync plugin distribution skills ----------
|
|
47
|
-
echo "==> Syncing plugin distribution skills..."
|
|
48
|
-
"${SCRIPT_DIR}/sync-plugin-skills.sh"
|
|
49
|
-
|
|
50
|
-
# ---------- 4. Plugin caches ----------
|
|
51
|
-
echo "==> Syncing plugin caches..."
|
|
52
|
-
|
|
53
|
-
sync_plugin() {
|
|
54
|
-
local src_dir="$1"
|
|
55
|
-
local plugin_json="${src_dir}/.claude-plugin/plugin.json"
|
|
56
|
-
|
|
57
|
-
if [ ! -f "$plugin_json" ]; then
|
|
58
|
-
echo " SKIP: ${src_dir} (no .claude-plugin/plugin.json)"
|
|
59
|
-
return
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
local name version cache_dest
|
|
63
|
-
name=$(python3 -c "import json,sys; print(json.load(sys.stdin)['name'])" < "$plugin_json")
|
|
64
|
-
version=$(python3 -c "import json,sys; print(json.load(sys.stdin)['version'])" < "$plugin_json")
|
|
65
|
-
cache_dest="${CACHE_DIR}/${name}/${version}"
|
|
66
|
-
|
|
67
|
-
rm -rf "$cache_dest"
|
|
68
|
-
mkdir -p "$cache_dest"
|
|
69
|
-
|
|
70
|
-
# Copy everything except .claude-plugin/ first, then copy .claude-plugin/
|
|
71
|
-
find "$src_dir" -mindepth 1 -maxdepth 1 -not -name '.claude-plugin' -exec cp -a {} "$cache_dest/" \;
|
|
72
|
-
cp -a "${src_dir}/.claude-plugin" "$cache_dest/.claude-plugin"
|
|
73
|
-
|
|
74
|
-
echo " ${name}@${version} -> ${cache_dest}"
|
|
75
|
-
|
|
76
|
-
# Update installed_plugins.json timestamp
|
|
77
|
-
if [ -f "$INSTALLED_JSON" ] && command -v python3 >/dev/null 2>&1; then
|
|
78
|
-
local git_sha
|
|
79
|
-
git_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
80
|
-
local now
|
|
81
|
-
now=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
82
|
-
python3 -c "
|
|
83
|
-
import json, sys
|
|
84
|
-
key = '${name}@handoff-mcp-marketplace'
|
|
85
|
-
with open('${INSTALLED_JSON}', 'r') as f:
|
|
86
|
-
data = json.load(f)
|
|
87
|
-
if key in data.get('plugins', {}):
|
|
88
|
-
for entry in data['plugins'][key]:
|
|
89
|
-
entry['installPath'] = '${cache_dest}'
|
|
90
|
-
entry['version'] = '${version}'
|
|
91
|
-
entry['lastUpdated'] = '${now}'
|
|
92
|
-
entry['gitCommitSha'] = '${git_sha}'
|
|
93
|
-
with open('${INSTALLED_JSON}', 'w') as f:
|
|
94
|
-
json.dump(data, f, indent=4)
|
|
95
|
-
f.write('\n')
|
|
96
|
-
" 2>/dev/null || true
|
|
97
|
-
fi
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
sync_plugin "${ROOT}/plugin"
|
|
101
|
-
sync_plugin "${ROOT}/plugin-hooks"
|
|
102
|
-
sync_plugin "${ROOT}/plugin-task-loop"
|
|
103
|
-
|
|
104
|
-
# ---------- 5. Verify ----------
|
|
105
|
-
echo ""
|
|
106
|
-
echo "==> Done. Restart Claude Code to pick up changes."
|
|
107
|
-
echo " Binary: ${HOME}/.local/bin/handoff-mcp"
|
|
108
|
-
echo " Skills: ${CLAUDE_DIR}/skills/"
|
|
109
|
-
echo " Plugins: ${CACHE_DIR}/"
|
package/scripts/postinstall.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const { execSync } = require("child_process");
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
|
|
8
|
-
const ROOT = path.resolve(__dirname, "..");
|
|
9
|
-
const BIN_DIR = path.join(ROOT, "bin");
|
|
10
|
-
const BINARY = path.join(BIN_DIR, "handoff-mcp-bin");
|
|
11
|
-
|
|
12
|
-
if (fs.existsSync(BINARY)) {
|
|
13
|
-
try {
|
|
14
|
-
execSync(`"${BINARY}" --help`, { stdio: "ignore", timeout: 5000 });
|
|
15
|
-
process.exit(0);
|
|
16
|
-
} catch {
|
|
17
|
-
// prebuilt binary exists but can't run on this platform — rebuild
|
|
18
|
-
fs.unlinkSync(BINARY);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
execSync("cargo --version", { stdio: "ignore" });
|
|
24
|
-
} catch {
|
|
25
|
-
console.error(
|
|
26
|
-
"Error: Rust toolchain not found.\n" +
|
|
27
|
-
"handoff-mcp-server requires Rust to build from source.\n" +
|
|
28
|
-
"Install Rust: https://rustup.rs/"
|
|
29
|
-
);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
console.log("Building handoff-mcp from source...");
|
|
34
|
-
try {
|
|
35
|
-
execSync("cargo build --release", { cwd: ROOT, stdio: "inherit" });
|
|
36
|
-
} catch {
|
|
37
|
-
console.error("Error: cargo build failed.");
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const built = path.join(ROOT, "target", "release", "handoff-mcp");
|
|
42
|
-
if (!fs.existsSync(built)) {
|
|
43
|
-
console.error("Error: binary not found after build.");
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
48
|
-
fs.copyFileSync(built, BINARY);
|
|
49
|
-
fs.chmodSync(BINARY, 0o755);
|
|
50
|
-
console.log("handoff-mcp installed successfully.");
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Sync skills/ -> plugin/skills/ for plugin distribution.
|
|
3
|
-
#
|
|
4
|
-
# plugin/skills/ is generated, but it is COMMITTED: the marketplace serves the
|
|
5
|
-
# plugin straight from the repository, so a skill that is not committed there
|
|
6
|
-
# never reaches an installed plugin. Run this after editing skills/, and commit
|
|
7
|
-
# the result.
|
|
8
|
-
#
|
|
9
|
-
# sync-plugin-skills.sh sync skills/ -> plugin/skills/
|
|
10
|
-
# sync-plugin-skills.sh --check exit non-zero if they differ (no writes)
|
|
11
|
-
set -euo pipefail
|
|
12
|
-
|
|
13
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
-
ROOT="$(dirname "$SCRIPT_DIR")"
|
|
15
|
-
|
|
16
|
-
if [ "${1:-}" = "--check" ]; then
|
|
17
|
-
if [ ! -d "$ROOT/plugin/skills" ]; then
|
|
18
|
-
echo "ERROR: plugin/skills/ is missing. Run './scripts/sync-plugin-skills.sh'." >&2
|
|
19
|
-
exit 1
|
|
20
|
-
fi
|
|
21
|
-
if ! diff -rq "$ROOT/skills/" "$ROOT/plugin/skills/" >/dev/null 2>&1; then
|
|
22
|
-
echo "ERROR: plugin/skills/ is out of sync with skills/:" >&2
|
|
23
|
-
diff -rq "$ROOT/skills/" "$ROOT/plugin/skills/" >&2 || true
|
|
24
|
-
echo "Run './scripts/sync-plugin-skills.sh' and commit the result." >&2
|
|
25
|
-
exit 1
|
|
26
|
-
fi
|
|
27
|
-
echo "OK: plugin/skills/ is in sync with skills/."
|
|
28
|
-
exit 0
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
rm -rf "$ROOT/plugin/skills"
|
|
32
|
-
cp -a "$ROOT/skills" "$ROOT/plugin/skills"
|
|
33
|
-
|
|
34
|
-
echo "Synced skills/ -> plugin/skills/"
|
|
35
|
-
diff -rq "$ROOT/skills/" "$ROOT/plugin/skills/" && echo "No differences." || echo "WARNING: differences remain after sync."
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Sync plugin/marketplace manifest versions to package.json's version.
|
|
3
|
-
#
|
|
4
|
-
# package.json is the source of truth (kept in sync with Cargo.toml manually
|
|
5
|
-
# per CLAUDE.md "Version sync" rule). This script propagates that version to:
|
|
6
|
-
# - plugin/.claude-plugin/plugin.json
|
|
7
|
-
# - plugin-hooks/.claude-plugin/plugin.json
|
|
8
|
-
# - plugin-task-loop/.claude-plugin/plugin.json
|
|
9
|
-
# - .claude-plugin/marketplace.json (all three "version" entries)
|
|
10
|
-
#
|
|
11
|
-
# Uses regex line replacement (not json.dump) so untouched formatting in
|
|
12
|
-
# these hand-maintained manifests is preserved byte-for-byte.
|
|
13
|
-
#
|
|
14
|
-
# Usage:
|
|
15
|
-
# ./scripts/sync-plugin-version.sh # fix mismatches in place
|
|
16
|
-
# ./scripts/sync-plugin-version.sh --check # exit 1 if anything is out of sync, fix nothing
|
|
17
|
-
set -euo pipefail
|
|
18
|
-
|
|
19
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
20
|
-
ROOT="$(dirname "$SCRIPT_DIR")"
|
|
21
|
-
cd "$ROOT"
|
|
22
|
-
|
|
23
|
-
MODE="fix"
|
|
24
|
-
if [ "${1:-}" = "--check" ]; then
|
|
25
|
-
MODE="check"
|
|
26
|
-
fi
|
|
27
|
-
|
|
28
|
-
python3 - "$MODE" <<'PYEOF'
|
|
29
|
-
import json
|
|
30
|
-
import re
|
|
31
|
-
import sys
|
|
32
|
-
|
|
33
|
-
mode = sys.argv[1]
|
|
34
|
-
|
|
35
|
-
with open("package.json") as f:
|
|
36
|
-
root_version = json.load(f)["version"]
|
|
37
|
-
|
|
38
|
-
version_line_re = re.compile(r'^(\s*"version"\s*:\s*)"[^"]*"(,?\s*(?:\r?\n)?)$')
|
|
39
|
-
|
|
40
|
-
def sync_file(path, mismatches):
|
|
41
|
-
with open(path) as f:
|
|
42
|
-
lines = f.readlines()
|
|
43
|
-
changed = False
|
|
44
|
-
out = []
|
|
45
|
-
for line in lines:
|
|
46
|
-
m = version_line_re.match(line)
|
|
47
|
-
if m:
|
|
48
|
-
current = re.search(r'"version"\s*:\s*"([^"]*)"', line).group(1)
|
|
49
|
-
if current != root_version:
|
|
50
|
-
mismatches.append((path, current))
|
|
51
|
-
changed = True
|
|
52
|
-
line = f'{m.group(1)}"{root_version}"{m.group(2)}'
|
|
53
|
-
out.append(line)
|
|
54
|
-
if changed and mode == "fix":
|
|
55
|
-
with open(path, "w") as f:
|
|
56
|
-
f.writelines(out)
|
|
57
|
-
return changed
|
|
58
|
-
|
|
59
|
-
mismatches = []
|
|
60
|
-
plugin_files = [
|
|
61
|
-
"plugin/.claude-plugin/plugin.json",
|
|
62
|
-
"plugin-hooks/.claude-plugin/plugin.json",
|
|
63
|
-
"plugin-task-loop/.claude-plugin/plugin.json",
|
|
64
|
-
".claude-plugin/marketplace.json",
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
for path in plugin_files:
|
|
68
|
-
sync_file(path, mismatches)
|
|
69
|
-
|
|
70
|
-
if mode == "check":
|
|
71
|
-
if mismatches:
|
|
72
|
-
print(f"Version mismatch: package.json is {root_version}, but found:")
|
|
73
|
-
for path, version in mismatches:
|
|
74
|
-
print(f" {path}: {version}")
|
|
75
|
-
print("Run ./scripts/sync-plugin-version.sh to fix.")
|
|
76
|
-
sys.exit(1)
|
|
77
|
-
print(f"All plugin/marketplace manifests match package.json ({root_version}).")
|
|
78
|
-
else:
|
|
79
|
-
if mismatches:
|
|
80
|
-
print(f"Synced {len(mismatches)} version occurrence(s) to {root_version}:")
|
|
81
|
-
for path, old_version in mismatches:
|
|
82
|
-
print(f" {path}: {old_version} -> {root_version}")
|
|
83
|
-
else:
|
|
84
|
-
print(f"Already in sync at version {root_version}. No changes made.")
|
|
85
|
-
PYEOF
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# sync-workflow-inline.sh — keep workflow scripts self-contained.
|
|
4
|
-
#
|
|
5
|
-
# The Workflow runtime cannot `import` (probed: "import() is not available in
|
|
6
|
-
# workflow scripts"; `require` is undefined), and workflow scripts have a
|
|
7
|
-
# top-level `return` so Node cannot import *them* either. Neither side can reach
|
|
8
|
-
# the other, yet we still want the shared logic unit-tested.
|
|
9
|
-
#
|
|
10
|
-
# Resolution: the pure logic lives in testable ES modules (the source of truth).
|
|
11
|
-
# Each module's INLINE region is mirrored verbatim into the workflow script
|
|
12
|
-
# between GENERATED markers. This script performs — or verifies — that mirror.
|
|
13
|
-
#
|
|
14
|
-
# ./scripts/sync-workflow-inline.sh # rewrite the generated blocks
|
|
15
|
-
# ./scripts/sync-workflow-inline.sh --check # fail if out of sync (CI/hook)
|
|
16
|
-
#
|
|
17
|
-
# Mirrors the contract of scripts/sync-plugin-version.sh.
|
|
18
|
-
|
|
19
|
-
set -euo pipefail
|
|
20
|
-
|
|
21
|
-
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
22
|
-
cd "$REPO_ROOT"
|
|
23
|
-
|
|
24
|
-
TARGET="plugin-task-loop/workflows/session-execute.js"
|
|
25
|
-
|
|
26
|
-
# Modules mirrored into TARGET, in the order they must appear.
|
|
27
|
-
# Each entry is a bare module name; source is lib/<name>.js.
|
|
28
|
-
#
|
|
29
|
-
# Order is load-bearing where one module's inline region calls another's:
|
|
30
|
-
# `context-injection` calls resolveProfile()/profileStages() from `profile`, so
|
|
31
|
-
# `profile` must be mirrored ahead of it (const bindings are in the temporal dead
|
|
32
|
-
# zone until their block is evaluated).
|
|
33
|
-
# `task-graph` is self-contained (pure union-find over the assignment arrays), so
|
|
34
|
-
# its position is free; it sits last to keep the dependency-ordered prefix intact.
|
|
35
|
-
MODULES=(verdict-logic profile context-injection task-graph)
|
|
36
|
-
|
|
37
|
-
CHECK_MODE=0
|
|
38
|
-
if [ "${1:-}" = "--check" ]; then
|
|
39
|
-
CHECK_MODE=1
|
|
40
|
-
elif [ $# -gt 0 ]; then
|
|
41
|
-
echo "usage: $0 [--check]" >&2
|
|
42
|
-
exit 2
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
[ -f "$TARGET" ] || { echo "ERROR: missing $TARGET" >&2; exit 1; }
|
|
46
|
-
|
|
47
|
-
count_marker() { grep -cFx "$1" "$2" || true; }
|
|
48
|
-
|
|
49
|
-
WORK="$(mktemp -d)"
|
|
50
|
-
trap 'rm -rf "$WORK"' EXIT
|
|
51
|
-
|
|
52
|
-
# Start from the current target; each module rewrites its own block in turn.
|
|
53
|
-
cp "$TARGET" "$WORK/rendered"
|
|
54
|
-
|
|
55
|
-
for module in "${MODULES[@]}"; do
|
|
56
|
-
SOURCE="plugin-task-loop/workflows/lib/${module}.js"
|
|
57
|
-
[ -f "$SOURCE" ] || { echo "ERROR: missing $SOURCE" >&2; exit 1; }
|
|
58
|
-
|
|
59
|
-
SRC_BEGIN="// --- BEGIN INLINE: ${module} ---"
|
|
60
|
-
SRC_END="// --- END INLINE: ${module} ---"
|
|
61
|
-
DST_BEGIN="// --- BEGIN GENERATED: ${module} (source: lib/${module}.js) ---"
|
|
62
|
-
DST_END="// --- END GENERATED: ${module} ---"
|
|
63
|
-
|
|
64
|
-
for marker in "$SRC_BEGIN" "$SRC_END"; do
|
|
65
|
-
n=$(count_marker "$marker" "$SOURCE")
|
|
66
|
-
[ "$n" -eq 1 ] || { echo "ERROR: '$marker' appears $n time(s) in $SOURCE (want 1)" >&2; exit 1; }
|
|
67
|
-
done
|
|
68
|
-
for marker in "$DST_BEGIN" "$DST_END"; do
|
|
69
|
-
n=$(count_marker "$marker" "$WORK/rendered")
|
|
70
|
-
[ "$n" -eq 1 ] || { echo "ERROR: '$marker' appears $n time(s) in $TARGET (want 1)" >&2; exit 1; }
|
|
71
|
-
done
|
|
72
|
-
|
|
73
|
-
EXTRACTED="$WORK/${module}.body"
|
|
74
|
-
awk -v b="$SRC_BEGIN" -v e="$SRC_END" '
|
|
75
|
-
$0 == b { inblk = 1; next }
|
|
76
|
-
$0 == e { inblk = 0; next }
|
|
77
|
-
inblk { print }
|
|
78
|
-
' "$SOURCE" > "$EXTRACTED"
|
|
79
|
-
|
|
80
|
-
if [ ! -s "$EXTRACTED" ]; then
|
|
81
|
-
echo "ERROR: extracted inline region from $SOURCE is empty" >&2
|
|
82
|
-
exit 1
|
|
83
|
-
fi
|
|
84
|
-
|
|
85
|
-
# The mirrored region must be self-contained.
|
|
86
|
-
if grep -Eq '^[[:space:]]*(import|export)[[:space:]]' "$EXTRACTED"; then
|
|
87
|
-
echo "ERROR: the inline region of $SOURCE contains import/export." >&2
|
|
88
|
-
echo " Workflow scripts cannot import. Keep those outside the markers." >&2
|
|
89
|
-
exit 1
|
|
90
|
-
fi
|
|
91
|
-
|
|
92
|
-
# A destination marker inside the copied body would corrupt the target — the
|
|
93
|
-
# awk pass would end the generated block early and duplicate the remainder,
|
|
94
|
-
# after which every --check fails with a confusing "appears 2 time(s)".
|
|
95
|
-
for marker in "$DST_BEGIN" "$DST_END"; do
|
|
96
|
-
if grep -qFx "$marker" "$EXTRACTED"; then
|
|
97
|
-
echo "ERROR: the inline region of $SOURCE contains the destination marker:" >&2
|
|
98
|
-
echo " $marker" >&2
|
|
99
|
-
echo " Remove it — it would corrupt $TARGET on the next sync." >&2
|
|
100
|
-
exit 1
|
|
101
|
-
fi
|
|
102
|
-
done
|
|
103
|
-
|
|
104
|
-
awk -v b="$DST_BEGIN" -v e="$DST_END" -v body="$EXTRACTED" -v src="$SOURCE" '
|
|
105
|
-
$0 == b {
|
|
106
|
-
print
|
|
107
|
-
print "// AUTO-GENERATED — DO NOT EDIT BY HAND."
|
|
108
|
-
print "// Source: " src
|
|
109
|
-
print "// Regenerate: ./scripts/sync-workflow-inline.sh"
|
|
110
|
-
while ((getline line < body) > 0) print line
|
|
111
|
-
close(body)
|
|
112
|
-
skip = 1
|
|
113
|
-
next
|
|
114
|
-
}
|
|
115
|
-
$0 == e { skip = 0; print; next }
|
|
116
|
-
!skip { print }
|
|
117
|
-
' "$WORK/rendered" > "$WORK/next"
|
|
118
|
-
mv "$WORK/next" "$WORK/rendered"
|
|
119
|
-
done
|
|
120
|
-
|
|
121
|
-
if [ "$CHECK_MODE" -eq 1 ]; then
|
|
122
|
-
if ! diff -u "$TARGET" "$WORK/rendered" > /dev/null 2>&1; then
|
|
123
|
-
echo "ERROR: $TARGET is out of sync with its source modules" >&2
|
|
124
|
-
echo " Run ./scripts/sync-workflow-inline.sh to regenerate." >&2
|
|
125
|
-
echo >&2
|
|
126
|
-
diff -u "$TARGET" "$WORK/rendered" >&2 || true
|
|
127
|
-
exit 1
|
|
128
|
-
fi
|
|
129
|
-
echo "OK: $TARGET is in sync with ${MODULES[*]}"
|
|
130
|
-
exit 0
|
|
131
|
-
fi
|
|
132
|
-
|
|
133
|
-
if diff -q "$TARGET" "$WORK/rendered" > /dev/null 2>&1; then
|
|
134
|
-
echo "OK: $TARGET already in sync (no change)"
|
|
135
|
-
else
|
|
136
|
-
cat "$WORK/rendered" > "$TARGET"
|
|
137
|
-
echo "Synced: ${MODULES[*]} -> $TARGET"
|
|
138
|
-
fi
|