@yugabytedb/meko-mcp 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +59 -25
  2. package/assets/hooks-handlers/before-submit-prompt.sh +2 -2
  3. package/assets/hooks-handlers/codex-session-end.sh +78 -0
  4. package/assets/hooks-handlers/lib/capture.js +366 -152
  5. package/assets/hooks-handlers/lib/checkpoint-timer.js +67 -13
  6. package/assets/hooks-handlers/session-end.sh +10 -0
  7. package/assets/hooks-handlers/session-start.sh +14 -3
  8. package/assets/hooks-handlers/test_checkpoint_timer.sh +162 -0
  9. package/assets/scripts/meko-statusline.sh +119 -0
  10. package/assets/scripts/package-desktop-skill.sh +64 -30
  11. package/assets/skills/meko-mcp-tools/SKILL.md +84 -86
  12. package/assets/skills/meko-mcp-tools/references/_sections.md +2 -4
  13. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +29 -25
  14. package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +5 -7
  15. package/assets/skills/meko-mcp-tools/references/tools-conversation-reasoning.md +50 -0
  16. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +126 -184
  17. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +31 -37
  18. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +10 -26
  19. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +26 -30
  20. package/assets/skills/meko-mcp-tools/references/tools-overview.md +60 -66
  21. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -93
  22. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +5 -99
  23. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +9 -8
  24. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +138 -138
  25. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +46 -16
  26. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +2 -2
  27. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +28 -24
  28. package/assets/skills/meko-mcp-tools-desktop/references/tools-conversation-reasoning.md +50 -0
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +120 -174
  30. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +16 -22
  31. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +28 -34
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +60 -66
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +28 -93
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +4 -98
  35. package/assets/skills/meko-select-datapack-desktop/SKILL.md +208 -0
  36. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
  37. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +41 -0
  38. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
  39. package/bin/create-meko-setup.mjs +74 -6
  40. package/lib/clients/claude-code.mjs +137 -6
  41. package/lib/clients/claude-desktop.mjs +51 -12
  42. package/lib/clients/codex.mjs +329 -13
  43. package/lib/clients/common.mjs +43 -1
  44. package/lib/clients/cursor.mjs +82 -21
  45. package/lib/config.mjs +26 -0
  46. package/lib/detect.mjs +7 -2
  47. package/lib/installer.mjs +24 -0
  48. package/lib/mcp-transport.mjs +5 -0
  49. package/lib/prompt.mjs +1 -1
  50. package/lib/url-selection.mjs +55 -0
  51. package/lib/validate.mjs +0 -1
  52. package/package.json +1 -1
  53. package/scripts/bundle-assets.mjs +8 -0
  54. package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
  55. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +0 -75
  56. package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
  57. package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +0 -75
@@ -12,16 +12,29 @@
12
12
  *
13
13
  * Environment:
14
14
  * MEKO_CHECKPOINT_INTERVAL Seconds between ticks (default: 600)
15
+ * MEKO_TIMER_MAX_AGE Hard self-exit age in seconds (default: 86400 = 24h)
16
+ * MEKO_TIMER_IDLE_EXIT Self-exit after the transcript is untouched this
17
+ * long, in seconds (default: 7200 = 2h)
18
+ * MEKO_TIMER_STARTUP_GRACE Wait this long for the transcript to appear
19
+ * before self-exiting (default: 1800 = 30min)
15
20
  * MEKO_WATERMARK_DIR Watermark directory (default: ~/.claude/meko-capture)
16
21
  * MEKO_MCP_URL Inherited by capture.js
17
22
  * MEKO_API_KEY Inherited by capture.js
18
23
  */
19
24
 
20
- const { execFileSync } = require("child_process");
25
+ const { execFile } = require("child_process");
21
26
  const fs = require("fs");
22
27
  const path = require("path");
23
28
 
24
29
  const INTERVAL_SECS = parseInt(process.env.MEKO_CHECKPOINT_INTERVAL || "600", 10);
30
+ // Self-exit backstops so an orphaned daemon (its pid file removed out of band,
31
+ // a missed SessionEnd, a blocked SIGTERM) cannot tick forever. Both are
32
+ // generous enough not to interrupt a genuinely long, active session.
33
+ const MAX_AGE_SECS = parseInt(process.env.MEKO_TIMER_MAX_AGE, 10) || 86400;
34
+ const IDLE_EXIT_SECS = parseInt(process.env.MEKO_TIMER_IDLE_EXIT, 10) || 7200;
35
+ const STARTUP_GRACE_SECS =
36
+ parseInt(process.env.MEKO_TIMER_STARTUP_GRACE, 10) || 1800;
37
+ const startedAt = Date.now();
25
38
  const WATERMARK_DIR =
26
39
  process.env.MEKO_WATERMARK_DIR ||
27
40
  path.join(process.env.HOME || "~", ".claude", "meko-capture");
@@ -49,24 +62,65 @@ process.on("SIGTERM", cleanup);
49
62
  process.on("SIGINT", cleanup);
50
63
 
51
64
  function tick() {
52
- // Skip if transcript doesn't exist (session ended but we weren't killed yet)
53
- if (!fs.existsSync(transcriptPath)) {
65
+ // Self-exit backstop: never outlive the hard max age. Guards against orphans
66
+ // whose pid file was removed out of band (so session-end.sh can't kill them)
67
+ // or whose SessionEnd never fired (force-quit / terminal closed).
68
+ if (Date.now() - startedAt >= MAX_AGE_SECS * 1000) {
69
+ process.stderr.write(
70
+ `[meko-timer] Max age (${MAX_AGE_SECS}s) reached; self-exiting to avoid orphan.\n`,
71
+ );
54
72
  cleanup();
55
73
  return;
56
74
  }
57
75
 
58
- const hookInput = JSON.stringify({ transcript_path: transcriptPath });
59
-
76
+ // Exit if the transcript is gone (session ended but we weren't killed yet).
77
+ // SessionStart can precede transcript creation by up to 20 minutes, so a
78
+ // missing file during the startup grace period is expected.
79
+ let stat;
60
80
  try {
61
- execFileSync("node", [captureScript, "checkpoint"], {
62
- input: hookInput,
63
- timeout: 30000,
64
- stdio: ["pipe", "pipe", "pipe"],
65
- });
66
- } catch (err) {
67
- // Best-effort: log and continue
68
- process.stderr.write(`[meko-timer] Checkpoint tick failed: ${err.message}\n`);
81
+ stat = fs.statSync(transcriptPath);
82
+ } catch {
83
+ if (Date.now() - startedAt < STARTUP_GRACE_SECS * 1000) {
84
+ return;
85
+ }
86
+ cleanup();
87
+ return;
69
88
  }
89
+
90
+ // Exit if the transcript has been idle far longer than a checkpoint window —
91
+ // the session is effectively over even though we were never signalled.
92
+ if (Date.now() - stat.mtimeMs >= IDLE_EXIT_SECS * 1000) {
93
+ process.stderr.write(
94
+ `[meko-timer] Transcript idle > ${IDLE_EXIT_SECS}s; self-exiting.\n`,
95
+ );
96
+ cleanup();
97
+ return;
98
+ }
99
+
100
+ const hookInput = JSON.stringify({ transcript_path: transcriptPath });
101
+
102
+ // Run capture ASYNCHRONOUSLY. execFileSync would block this daemon's event
103
+ // loop for up to 30s, during which SIGTERM/SIGINT (clean shutdown) can't be
104
+ // handled. execFile keeps the loop responsive so cleanup() fires promptly.
105
+ const child = execFile(
106
+ "node",
107
+ [captureScript, "checkpoint"],
108
+ { timeout: 30000 },
109
+ (err) => {
110
+ if (err) {
111
+ // Best-effort: log and continue to the next tick.
112
+ process.stderr.write(`[meko-timer] Checkpoint tick failed: ${err.message}\n`);
113
+ }
114
+ },
115
+ );
116
+ // The stdin stream can emit EPIPE if the child exits or fails to spawn before
117
+ // we finish writing. Without a listener that error is unhandled and crashes
118
+ // the daemon — so swallow it here; the execFile callback above reports the
119
+ // real failure.
120
+ child.stdin.on("error", (err) => {
121
+ process.stderr.write(`[meko-timer] Checkpoint stdin error: ${err.message}\n`);
122
+ });
123
+ child.stdin.end(hookInput);
70
124
  }
71
125
 
72
126
  setInterval(tick, INTERVAL_SECS * 1000);
@@ -21,6 +21,16 @@ if [ -n "$TRANSCRIPT_PATH" ]; then
21
21
  kill "$(cat "$PID_FILE")" 2>/dev/null || true
22
22
  rm -f "$PID_FILE"
23
23
  fi
24
+ # Fallback: the pid file can go missing while the daemon lives (removed out of
25
+ # band, a missed/blocked signal). Match the running checkpoint-timer.js by the
26
+ # clean session id rather than the absolute transcript path, which may contain
27
+ # regex metacharacters that make pgrep fail or match incorrectly.
28
+ if command -v pgrep >/dev/null 2>&1; then
29
+ for pid in $(pgrep -f "checkpoint-timer.js .*${SESSION_ID}" 2>/dev/null); do
30
+ [ "$pid" = "$$" ] && continue
31
+ kill "$pid" 2>/dev/null || true
32
+ done
33
+ fi
24
34
  fi
25
35
 
26
36
  # Run final capture
@@ -13,10 +13,21 @@ TRANSCRIPT_PATH="$(printf '%s' "$HOOK_INPUT" | node -e '
13
13
  process.stdout.write(p);
14
14
  ' 2>/dev/null)"
15
15
 
16
- # Spawn background checkpoint timer (periodic capture without interrupting the agent)
17
- if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
16
+ # Spawn background checkpoint timer (periodic capture without interrupting the agent).
17
+ # Send the daemon's stderr to a log file instead of /dev/null so startup
18
+ # failures and crashes are diagnosable; stdout is still discarded.
19
+ #
20
+ # NOTE: we deliberately do NOT gate on [ -f "$TRANSCRIPT_PATH" ]. SessionStart
21
+ # fires before Claude Code writes the transcript .jsonl, so that file usually
22
+ # does not exist yet at hook time (measured gaps of +3s to +20min). Gating on
23
+ # its existence meant the timer never spawned and the whole session went
24
+ # uncaptured. checkpoint-timer.js self-guards a missing transcript on each
25
+ # tick(), so spawning unconditionally (when the path is known) is safe.
26
+ if [ -n "$TRANSCRIPT_PATH" ]; then
27
+ MEKO_LOG_DIR="${MEKO_WATERMARK_DIR:-$HOME/.claude/meko-capture}"
28
+ mkdir -p "$MEKO_LOG_DIR"
18
29
  nohup node "$SCRIPT_DIR/lib/checkpoint-timer.js" "$TRANSCRIPT_PATH" \
19
- </dev/null >/dev/null 2>&1 &
30
+ </dev/null >/dev/null 2>>"$MEKO_LOG_DIR/timer.log" &
20
31
  disown 2>/dev/null || true
21
32
  fi
22
33
 
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # test_checkpoint_timer.sh — Regression tests for issue #159:
4
+ # - Defect 1: the checkpoint timer must spawn even when the transcript file
5
+ # does not exist yet at SessionStart time (spawn race).
6
+ # - Defect 3: the timer must self-exit (max-age / idle backstop) and
7
+ # session-end.sh must kill it by transcript path when the pid file is gone,
8
+ # so orphaned daemons cannot tick forever.
9
+ #
10
+ # These tests do NOT require a live Meko MCP server: the timer's self-guard and
11
+ # self-exit paths run before any capture.js checkpoint would reach MCP, and we
12
+ # assert on process liveness / pid-file presence only.
13
+ #
14
+ # Usage: ./test_checkpoint_timer.sh
15
+
16
+ set -uo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
+ TIMER_JS="$SCRIPT_DIR/lib/checkpoint-timer.js"
20
+ SESSION_START="$SCRIPT_DIR/session-start.sh"
21
+ SESSION_END="$SCRIPT_DIR/session-end.sh"
22
+
23
+ WM_DIR="$(mktemp -d -t meko-timer-test-XXXXXX)"
24
+ TX_ROOT="$(mktemp -d -t meko-timer-tx-XXXXXX)"
25
+ # Exercise session-end matching when the absolute transcript path contains
26
+ # characters that have special meaning in a regular expression.
27
+ TX_DIR="$TX_ROOT/path with [regex] chars"
28
+ mkdir -p "$TX_DIR"
29
+ SESSION_ID="timer-test-$$"
30
+ TRANSCRIPT_PATH="$TX_DIR/${SESSION_ID}.jsonl"
31
+ PID_FILE="$WM_DIR/${SESSION_ID}.timer.pid"
32
+
33
+ PASS=0
34
+ FAIL=0
35
+ SPAWNED_PIDS=()
36
+
37
+ cleanup() {
38
+ for p in "${SPAWNED_PIDS[@]:-}"; do kill "$p" 2>/dev/null || true; done
39
+ rm -rf "$WM_DIR" "$TX_ROOT"
40
+ }
41
+ trap cleanup EXIT
42
+
43
+ log() { printf " %-55s" "$1"; }
44
+ pass() { printf "\033[32mPASS\033[0m %s\n" "${1:-}"; PASS=$((PASS + 1)); }
45
+ fail() { printf "\033[31mFAIL\033[0m %s\n" "${1:-}"; FAIL=$((FAIL + 1)); }
46
+
47
+ # Poll up to N deciseconds for a process to die. Returns 0 if it died.
48
+ wait_gone() {
49
+ local pid="$1" tries="${2:-50}"
50
+ while [ "$tries" -gt 0 ]; do
51
+ kill -0 "$pid" 2>/dev/null || return 0
52
+ sleep 0.1
53
+ tries=$((tries - 1))
54
+ done
55
+ return 1
56
+ }
57
+
58
+ export MEKO_WATERMARK_DIR="$WM_DIR"
59
+
60
+ echo "============================================================"
61
+ echo "Test: checkpoint-timer regression (issue #159)"
62
+ echo "============================================================"
63
+ echo " Watermark dir: $WM_DIR"
64
+ echo " Transcript: $TRANSCRIPT_PATH"
65
+ echo ""
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Defect 1 — timer spawns even though transcript does not exist yet.
69
+ # session-start.sh backgrounds the timer; capture.js session-start will fail to
70
+ # reach MCP but must not block the spawn. The timer must remain alive after its
71
+ # first tick while transcript creation is still within the startup grace period.
72
+ # ---------------------------------------------------------------------------
73
+ echo "--- Defect 1: spawn race (transcript absent at SessionStart) ---"
74
+ log "Transcript absent before hook"
75
+ [ ! -f "$TRANSCRIPT_PATH" ] && pass || fail "transcript unexpectedly exists"
76
+
77
+ # Spawn the timer directly the way the fixed session-start.sh does. A one-second
78
+ # interval guarantees the missing-transcript branch runs before the assertion.
79
+ MEKO_CHECKPOINT_INTERVAL=1 MEKO_TIMER_MAX_AGE=999 MEKO_TIMER_IDLE_EXIT=999 \
80
+ MEKO_TIMER_STARTUP_GRACE=10 \
81
+ nohup node "$TIMER_JS" "$TRANSCRIPT_PATH" </dev/null >/dev/null 2>&1 &
82
+ TIMER_PID=$!
83
+ SPAWNED_PIDS+=("$TIMER_PID")
84
+ sleep 1.5
85
+
86
+ log "Timer survives first tick with missing transcript"
87
+ if kill -0 "$TIMER_PID" 2>/dev/null; then pass "pid $TIMER_PID"; else fail "timer did not stay up"; fi
88
+
89
+ log "Timer wrote its pid file"
90
+ [ -f "$PID_FILE" ] && pass || fail "no pid file at $PID_FILE"
91
+
92
+ kill "$TIMER_PID" 2>/dev/null || true
93
+ wait_gone "$TIMER_PID" || true
94
+ rm -f "$PID_FILE"
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Defect 3a — max-age self-exit. Transcript exists and is fresh, but max-age is
98
+ # tiny, so the first tick must self-exit and remove its pid file.
99
+ # ---------------------------------------------------------------------------
100
+ echo ""
101
+ echo "--- Defect 3a: max-age self-exit backstop ---"
102
+ printf '{"type":"user","uuid":"u1","message":{"content":"hi"}}\n' > "$TRANSCRIPT_PATH"
103
+
104
+ MEKO_CHECKPOINT_INTERVAL=1 MEKO_TIMER_MAX_AGE=1 MEKO_TIMER_IDLE_EXIT=999 \
105
+ nohup node "$TIMER_JS" "$TRANSCRIPT_PATH" </dev/null >/dev/null 2>&1 &
106
+ TIMER_PID=$!
107
+ SPAWNED_PIDS+=("$TIMER_PID")
108
+
109
+ log "Timer self-exits after max-age"
110
+ if wait_gone "$TIMER_PID" 60; then pass; else fail "still running after max-age"; fi
111
+
112
+ log "Pid file removed on self-exit"
113
+ [ ! -f "$PID_FILE" ] && pass || fail "stale pid file remains"
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Defect 3b — idle self-exit. Make the transcript look old (mtime in the past)
117
+ # so the idle backstop trips on the first tick.
118
+ # ---------------------------------------------------------------------------
119
+ echo ""
120
+ echo "--- Defect 3b: idle self-exit backstop ---"
121
+ printf '{"type":"user","uuid":"u1","message":{"content":"hi"}}\n' > "$TRANSCRIPT_PATH"
122
+ touch -t 202001010000 "$TRANSCRIPT_PATH" # far in the past
123
+
124
+ MEKO_CHECKPOINT_INTERVAL=1 MEKO_TIMER_MAX_AGE=999 MEKO_TIMER_IDLE_EXIT=1 \
125
+ nohup node "$TIMER_JS" "$TRANSCRIPT_PATH" </dev/null >/dev/null 2>&1 &
126
+ TIMER_PID=$!
127
+ SPAWNED_PIDS+=("$TIMER_PID")
128
+
129
+ log "Timer self-exits on idle transcript"
130
+ if wait_gone "$TIMER_PID" 60; then pass; else fail "still running with idle transcript"; fi
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Defect 3c — session-end.sh kills by transcript path when the pid file is gone.
134
+ # ---------------------------------------------------------------------------
135
+ echo ""
136
+ echo "--- Defect 3c: session-end path-based kill (no pid file) ---"
137
+ printf '{"type":"user","uuid":"u1","message":{"content":"hi"}}\n' > "$TRANSCRIPT_PATH"
138
+
139
+ # Long-lived timer; then delete its pid file to simulate the orphan condition.
140
+ MEKO_CHECKPOINT_INTERVAL=999 MEKO_TIMER_MAX_AGE=999 MEKO_TIMER_IDLE_EXIT=999 \
141
+ nohup node "$TIMER_JS" "$TRANSCRIPT_PATH" </dev/null >/dev/null 2>&1 &
142
+ TIMER_PID=$!
143
+ SPAWNED_PIDS+=("$TIMER_PID")
144
+ sleep 0.5
145
+ rm -f "$PID_FILE" # pid file vanished out of band -> classic orphan setup
146
+
147
+ log "Orphan timer running with no pid file"
148
+ if kill -0 "$TIMER_PID" 2>/dev/null && [ ! -f "$PID_FILE" ]; then pass "pid $TIMER_PID"; else fail "precondition not met"; fi
149
+
150
+ # session-end.sh must find it by transcript path and kill it.
151
+ printf '{"transcript_path":"%s"}' "$TRANSCRIPT_PATH" | bash "$SESSION_END" >/dev/null 2>&1 || true
152
+
153
+ log "session-end.sh killed the orphan by transcript path"
154
+ if wait_gone "$TIMER_PID" 30; then pass; else fail "orphan survived session-end"; fi
155
+
156
+ # ---------------------------------------------------------------------------
157
+ echo ""
158
+ echo "============================================================"
159
+ TOTAL=$((PASS + FAIL))
160
+ echo " Total: $TOTAL | Passed: $PASS | Failed: $FAIL"
161
+ echo "============================================================"
162
+ [ "$FAIL" -gt 0 ] && { echo " RESULT: FAILED"; exit 1; } || { echo " RESULT: ALL PASSED"; exit 0; }
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # meko-statusline.sh — Claude Code statusline contribution showing the active Meko datapack pin.
3
+ #
4
+ # Composable: if --inner-cmd is provided (or MEKO_INNER_STATUSLINE env var), runs it first and
5
+ # appends our suffix. So a user with an existing statusLine doesn't lose it.
6
+ #
7
+ # Usage in ~/.claude/settings.json (after installer wires it):
8
+ # { "statusLine": { "type": "command", "command": "$HOME/.claude/meko-statusline.sh --inner-cmd <existing-cmd>" } }
9
+ #
10
+ # Or, if you maintain the inner command separately, set MEKO_INNER_STATUSLINE=<cmd> in env.
11
+ #
12
+ # Outputs nothing (empty string) when no pin is set, so the indicator is invisible until you pin.
13
+ # The output goes on stdout — Claude Code displays it as-is, supporting ANSI color codes if you
14
+ # wrap the suffix.
15
+ #
16
+ # Reads JSON from stdin (Claude Code passes session metadata) and forwards it verbatim to the inner
17
+ # command. The inner command's stdout is printed first, then our suffix.
18
+
19
+ set -u
20
+
21
+ INNER_CMD=""
22
+ if [[ "${1:-}" == "--inner-cmd" && -n "${2:-}" ]]; then
23
+ INNER_CMD="$2"
24
+ elif [[ -n "${MEKO_INNER_STATUSLINE:-}" ]]; then
25
+ INNER_CMD="$MEKO_INNER_STATUSLINE"
26
+ fi
27
+
28
+ # Buffer stdin once so we can pipe the same JSON to both the inner command and our derivation.
29
+ STDIN_PAYLOAD=""
30
+ if [[ -p /dev/stdin || ! -t 0 ]]; then
31
+ STDIN_PAYLOAD="$(cat)"
32
+ fi
33
+
34
+ # --- Inner command first (preserves any pre-existing statusline content) ---
35
+
36
+ if [[ -n "$INNER_CMD" ]]; then
37
+ if [[ -n "$STDIN_PAYLOAD" ]]; then
38
+ INNER_OUT="$(printf '%s' "$STDIN_PAYLOAD" | eval "$INNER_CMD" 2>/dev/null || true)"
39
+ else
40
+ INNER_OUT="$(eval "$INNER_CMD" 2>/dev/null || true)"
41
+ fi
42
+ # Strip a single trailing newline so our suffix appears on the same line.
43
+ INNER_OUT="${INNER_OUT%$'\n'}"
44
+ printf '%s' "$INNER_OUT"
45
+ fi
46
+
47
+ # --- Derive agent_id from cwd ---
48
+ #
49
+ # Mirrors deriveAgentId() in skills/hooks-handlers/lib/capture.js:51-65:
50
+ # - basename of cwd, lowercased
51
+ # - non-[a-z0-9-]+ → '-'
52
+ # - trim leading/trailing '-'
53
+ # - max 64 chars
54
+ # - prefix with 'claude_code:'
55
+ #
56
+ # Then mirrors datapackPinSlug() in capture.js:341-348:
57
+ # - non-[A-Za-z0-9._-]+ → '_'
58
+ # - trim leading/trailing '_'
59
+ #
60
+ # Prefer cwd from the JSON stdin payload (Claude Code's session metadata format), falling back
61
+ # to PWD if stdin was empty / not piped.
62
+
63
+ CWD="$PWD"
64
+ if [[ -n "$STDIN_PAYLOAD" ]]; then
65
+ # Best-effort extraction without requiring jq. Look for "cwd":"…" first; fall back to "workspace":{"current_dir":"…"}.
66
+ # Isolate the exact key/value pair with grep -oE before extracting so a greedy `.*` can't grab a
67
+ # different key that merely ends in `cwd`/`current_dir` earlier on the same line.
68
+ EXTRACTED="$(printf '%s' "$STDIN_PAYLOAD" | grep -oE '"cwd"[[:space:]]*:[[:space:]]*"[^"]+"' | sed -E 's/.*"([^"]+)"$/\1/' | head -n 1)"
69
+ if [[ -z "$EXTRACTED" ]]; then
70
+ EXTRACTED="$(printf '%s' "$STDIN_PAYLOAD" | grep -oE '"current_dir"[[:space:]]*:[[:space:]]*"[^"]+"' | sed -E 's/.*"([^"]+)"$/\1/' | head -n 1)"
71
+ fi
72
+ if [[ -n "$EXTRACTED" ]]; then
73
+ CWD="$EXTRACTED"
74
+ fi
75
+ fi
76
+
77
+ # MEKO_AGENT_ID overrides the cwd-derived id, matching resolveSessionAgentId() in
78
+ # capture.js (the env override wins verbatim when non-empty).
79
+ if [[ -n "${MEKO_AGENT_ID:-}" ]]; then
80
+ AGENT_ID="$MEKO_AGENT_ID"
81
+ else
82
+ BASENAME="$(basename "$CWD")"
83
+ PROJECT_SLUG="$(printf '%s' "$BASENAME" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+|-+$//g')"
84
+ PROJECT_SLUG="${PROJECT_SLUG:0:64}"
85
+
86
+ if [[ -z "$PROJECT_SLUG" || "$PROJECT_SLUG" == "-" ]]; then
87
+ AGENT_ID="claude_code"
88
+ else
89
+ AGENT_ID="claude_code:$PROJECT_SLUG"
90
+ fi
91
+ fi
92
+
93
+ PIN_SLUG="$(printf '%s' "$AGENT_ID" | sed -E 's/[^A-Za-z0-9._-]+/_/g; s/^_+|_+$//g')"
94
+ # Match datapackPinSlug() in capture.js: an empty slug falls back to the common bucket
95
+ # (meko_agent), so both components look for the same pin file.
96
+ if [[ -z "$PIN_SLUG" ]]; then
97
+ PIN_SLUG="meko_agent"
98
+ fi
99
+ PIN_FILE="${MEKO_WATERMARK_DIR:-$HOME/.claude/meko-capture}/pin-${PIN_SLUG}.json"
100
+
101
+ # --- Read the pin file and append the suffix if a valid pin is present ---
102
+
103
+ if [[ -f "$PIN_FILE" ]]; then
104
+ # grep -oE isolates the exact "datapack_name":"…" pair before extracting, so a
105
+ # greedy `.*` can't grab a different key that merely ends in datapack_name.
106
+ PIN_NAME="$(grep -oE '"datapack_name"[[:space:]]*:[[:space:]]*"[^"]+"' "$PIN_FILE" | sed -E 's/.*"([^"]+)"$/\1/' | head -n 1)"
107
+ if [[ -n "$PIN_NAME" ]]; then
108
+ SUFFIX="Meko: $PIN_NAME"
109
+ # Add a separator only if the inner command produced output.
110
+ if [[ -n "${INNER_OUT:-}" ]]; then
111
+ printf ' \xe2\x80\xa2 %s' "$SUFFIX"
112
+ else
113
+ printf '%s' "$SUFFIX"
114
+ fi
115
+ fi
116
+ fi
117
+
118
+ # Always finish with a newline so subsequent shell renders cleanly.
119
+ printf '\n'
@@ -1,36 +1,70 @@
1
1
  #!/usr/bin/env bash
2
- # Package meko-mcp-tools-desktop as a .skill file for Claude Desktop distribution.
2
+ # Package the Claude Desktop skills as importable .skill files.
3
3
  # Usage: bash scripts/package-desktop-skill.sh
4
- # Output: meko-mcp-tools-desktop.skill (ZIP archive)
4
+ # Outputs: meko-mcp-tools-desktop.skill and meko-select-datapack-desktop.skill
5
5
 
6
6
  set -euo pipefail
7
7
 
8
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
- SKILLS_ROOT="$SCRIPT_DIR/../skills"
10
- SKILL_NAME="meko-mcp-tools-desktop"
11
- OUTPUT="$SCRIPT_DIR/../$SKILL_NAME.skill"
12
-
13
- if [ ! -d "$SKILLS_ROOT/$SKILL_NAME" ]; then
14
- echo "Error: Skill directory not found at $SKILLS_ROOT/$SKILL_NAME" >&2
15
- exit 1
16
- fi
17
-
18
- if [ ! -f "$SKILLS_ROOT/$SKILL_NAME/SKILL.md" ]; then
19
- echo "Error: SKILL.md not found in $SKILLS_ROOT/$SKILL_NAME/" >&2
20
- exit 1
21
- fi
22
-
23
- # Remove previous build if it exists
24
- rm -f "$OUTPUT"
25
-
26
- # Package skill + references (exclude OS artifacts and desktop README)
27
- cd "$SKILLS_ROOT"
28
- zip -r "$OUTPUT" \
29
- "$SKILL_NAME/SKILL.md" \
30
- "$SKILL_NAME/references/" \
31
- -x "*.DS_Store" "*__pycache__*"
32
-
33
- echo "Created $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
34
- echo ""
35
- echo "Contents:"
36
- unzip -l "$OUTPUT" | grep -E "^\s+[0-9]" | grep -v "files$"
9
+ SKILLS_ROOT="${MEKO_DESKTOP_SKILLS_ROOT:-$SCRIPT_DIR/../skills}"
10
+ SKILL_NAMES=("meko-mcp-tools-desktop" "meko-select-datapack-desktop")
11
+ XML_TAG_PATTERN='</?[[:alpha:]_:][[:alnum:]_.:-]*([[:blank:]][^<>]*)?[[:blank:]]*/?>'
12
+
13
+ for SKILL_NAME in "${SKILL_NAMES[@]}"; do
14
+ if [ ! -d "$SKILLS_ROOT/$SKILL_NAME" ]; then
15
+ echo "Error: Skill directory not found at $SKILLS_ROOT/$SKILL_NAME" >&2
16
+ exit 1
17
+ fi
18
+
19
+ if [ ! -f "$SKILLS_ROOT/$SKILL_NAME/SKILL.md" ]; then
20
+ echo "Error: SKILL.md not found in $SKILLS_ROOT/$SKILL_NAME/" >&2
21
+ exit 1
22
+ fi
23
+
24
+ if ! grep -q '^description:[[:space:]]*' "$SKILLS_ROOT/$SKILL_NAME/SKILL.md"; then
25
+ echo "Error: SKILL.md description is missing: $SKILL_NAME" >&2
26
+ exit 1
27
+ fi
28
+
29
+ DESCRIPTION="$(awk '
30
+ /^description:[[:space:]]*/ {
31
+ in_description = 1
32
+ value = $0
33
+ sub(/^description:[[:space:]]*/, "", value)
34
+ if (value !~ /^[>|][+-]?$/) printf "%s ", value
35
+ next
36
+ }
37
+ in_description && /^[^[:space:]]/ { exit }
38
+ in_description {
39
+ sub(/^[[:space:]]+/, "")
40
+ printf "%s ", $0
41
+ }
42
+ ' "$SKILLS_ROOT/$SKILL_NAME/SKILL.md")"
43
+
44
+ if [[ -z "${DESCRIPTION//[[:space:]]/}" ]]; then
45
+ echo "Error: SKILL.md description is empty: $SKILL_NAME" >&2
46
+ exit 1
47
+ fi
48
+
49
+ if printf '%s\n' "$DESCRIPTION" | grep -Eq "$XML_TAG_PATTERN"; then
50
+ echo "Error: SKILL.md description cannot contain XML-like tags: $SKILL_NAME" >&2
51
+ exit 1
52
+ fi
53
+ done
54
+
55
+ for SKILL_NAME in "${SKILL_NAMES[@]}"; do
56
+ OUTPUT="$SCRIPT_DIR/../$SKILL_NAME.skill"
57
+ rm -f "$OUTPUT"
58
+
59
+ # Package skill + references (exclude OS artifacts and desktop README)
60
+ cd "$SKILLS_ROOT"
61
+ zip -r "$OUTPUT" \
62
+ "$SKILL_NAME/SKILL.md" \
63
+ "$SKILL_NAME/references/" \
64
+ -x "*.DS_Store" "*__pycache__*"
65
+
66
+ echo "Created $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
67
+ echo ""
68
+ echo "Contents:"
69
+ unzip -l "$OUTPUT" | grep -E "^\s+[0-9]" | grep -v "files$"
70
+ done