@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.
- package/README.md +59 -25
- package/assets/hooks-handlers/before-submit-prompt.sh +2 -2
- package/assets/hooks-handlers/codex-session-end.sh +78 -0
- package/assets/hooks-handlers/lib/capture.js +366 -152
- package/assets/hooks-handlers/lib/checkpoint-timer.js +67 -13
- package/assets/hooks-handlers/session-end.sh +10 -0
- package/assets/hooks-handlers/session-start.sh +14 -3
- package/assets/hooks-handlers/test_checkpoint_timer.sh +162 -0
- package/assets/scripts/meko-statusline.sh +119 -0
- package/assets/scripts/package-desktop-skill.sh +64 -30
- package/assets/skills/meko-mcp-tools/SKILL.md +84 -86
- package/assets/skills/meko-mcp-tools/references/_sections.md +2 -4
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +29 -25
- package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +5 -7
- package/assets/skills/meko-mcp-tools/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +126 -184
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +31 -37
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +10 -26
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +26 -30
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +60 -66
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -93
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +5 -99
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +9 -8
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +138 -138
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +46 -16
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +2 -2
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +28 -24
- package/assets/skills/meko-mcp-tools-desktop/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +120 -174
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +16 -22
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +28 -34
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +60 -66
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +28 -93
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +4 -98
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +208 -0
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +41 -0
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
- package/bin/create-meko-setup.mjs +74 -6
- package/lib/clients/claude-code.mjs +137 -6
- package/lib/clients/claude-desktop.mjs +51 -12
- package/lib/clients/codex.mjs +329 -13
- package/lib/clients/common.mjs +43 -1
- package/lib/clients/cursor.mjs +82 -21
- package/lib/config.mjs +26 -0
- package/lib/detect.mjs +7 -2
- package/lib/installer.mjs +24 -0
- package/lib/mcp-transport.mjs +5 -0
- package/lib/prompt.mjs +1 -1
- package/lib/url-selection.mjs +55 -0
- package/lib/validate.mjs +0 -1
- package/package.json +1 -1
- package/scripts/bundle-assets.mjs +8 -0
- package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +0 -75
- package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
- 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 {
|
|
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
|
-
//
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2
|
+
# Package the Claude Desktop skills as importable .skill files.
|
|
3
3
|
# Usage: bash scripts/package-desktop-skill.sh
|
|
4
|
-
#
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"$
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|