mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
fix: prevent internal prompts and duplicates in memory database
- Add server-side detection of SDK processor's internal system prompt
in handleSessionInit, since CLAUDE_MNEMONIC_INTERNAL env var is not
propagated by Claude Code to hook subprocesses
- Add cross-session duplicate detection (FindRecentPromptByTextGlobal)
to catch same prompt text arriving from different session IDs
- Add hooks, mcpServers, and commands references to plugin.json per
Claude Code plugin spec
- Remove MCP server injection from register-plugin.sh (now in plugin.json)
- Use ${CLAUDE_PLUGIN_ROOT} for statusline path instead of hardcoded path
- Add python3 fallback for plugin registration when jq is unavailable
- Replace hardcoded 1.0.0 version in findWorkerBinary with glob lookup
- Add cache copy verification in register-plugin.sh
- Add update-version Makefile target to keep metadata in sync
This commit is contained in:
@@ -14,7 +14,7 @@ GOARCH ?= $(shell go env GOARCH)
|
||||
export CGO_ENABLED=1
|
||||
BUILD_TAGS := -tags "fts5"
|
||||
|
||||
.PHONY: all build clean test install lint hooks worker mcp stop-worker start-worker restart-worker dashboard website dev-website setup-libs
|
||||
.PHONY: all build clean test install lint hooks worker mcp stop-worker start-worker restart-worker dashboard website dev-website setup-libs update-version
|
||||
|
||||
all: build
|
||||
|
||||
@@ -22,8 +22,20 @@ all: build
|
||||
setup-libs:
|
||||
@./scripts/download-onnx-libs.sh all
|
||||
|
||||
# Update root plugin metadata with current version
|
||||
update-version:
|
||||
@mkdir -p .claude-plugin
|
||||
@sed 's/{{ .Version }}/$(VERSION)/g; s/{{.Version}}/$(VERSION)/g' $(PLUGIN_DIR)/.claude-plugin/plugin.json.tpl > .claude-plugin/plugin.json
|
||||
@echo "Updated .claude-plugin/plugin.json to version $(VERSION)"
|
||||
@# marketplace.json contains release-specific data (URLs, SHA256 hashes) that requires manual update per release.
|
||||
@# Only the top-level version field is updated here.
|
||||
@if [ -f marketplace.json ]; then \
|
||||
sed 's/"version": "[^"]*"/"version": "$(VERSION)"/' marketplace.json > marketplace.json.tmp && mv marketplace.json.tmp marketplace.json; \
|
||||
echo "Updated marketplace.json version fields to $(VERSION)"; \
|
||||
fi
|
||||
|
||||
# Build all binaries
|
||||
build: setup-libs dashboard worker hooks mcp
|
||||
build: setup-libs update-version dashboard worker hooks mcp
|
||||
|
||||
# Build Vue dashboard
|
||||
dashboard:
|
||||
@@ -235,6 +247,7 @@ help:
|
||||
@echo " make fmt - Format code"
|
||||
@echo " make clean - Clean build artifacts"
|
||||
@echo " make dev - Run worker in development mode"
|
||||
@echo " make update-version - Update version in plugin metadata files"
|
||||
@echo " make deps - Download dependencies"
|
||||
@echo " make website - Build website for production"
|
||||
@echo " make dev-website - Run website dev server"
|
||||
|
||||
@@ -264,6 +264,28 @@ func (s *PromptStore) FindRecentPromptByText(ctx context.Context, claudeSessionI
|
||||
return prompt.ID, prompt.PromptNumber, true
|
||||
}
|
||||
|
||||
// FindRecentPromptByTextGlobal finds a recent prompt by exact text match within a time window
|
||||
// across ALL sessions (not scoped to a single claude session ID).
|
||||
// This catches duplicates when the same prompt arrives from different session IDs,
|
||||
// e.g. when the SDK processor's callClaudeCLI subprocess fires with a different session ID.
|
||||
// Returns (promptID, promptNumber, found).
|
||||
func (s *PromptStore) FindRecentPromptByTextGlobal(ctx context.Context, promptText string, withinSeconds int) (int64, int, bool) {
|
||||
cutoffEpoch := time.Now().Add(-time.Duration(withinSeconds) * time.Second).UnixMilli()
|
||||
|
||||
var prompt UserPrompt
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("prompt_text = ? AND created_at_epoch >= ?",
|
||||
promptText, cutoffEpoch).
|
||||
Order("created_at_epoch DESC").
|
||||
First(&prompt).Error
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
return prompt.ID, prompt.PromptNumber, true
|
||||
}
|
||||
|
||||
// GetRecentUserPromptsByProject retrieves recent user prompts for a specific project.
|
||||
func (s *PromptStore) GetRecentUserPromptsByProject(ctx context.Context, project string, limit int) ([]*models.UserPromptWithSession, error) {
|
||||
var results []struct {
|
||||
|
||||
@@ -333,6 +333,39 @@ func TestPromptStore_FindRecentPromptByText(t *testing.T) {
|
||||
assert.False(t, notFound, "Should not find prompt outside time window")
|
||||
}
|
||||
|
||||
func TestPromptStore_FindRecentPromptByTextGlobal(t *testing.T) {
|
||||
promptStore, _, cleanup := testPromptStore(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Save a prompt under session "claude-1"
|
||||
id, err := promptStore.SaveUserPromptWithMatches(ctx, "claude-1", 1, "What is the architecture?", 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Global search should find it without specifying session ID
|
||||
foundID, foundNumber, found := promptStore.FindRecentPromptByTextGlobal(ctx, "What is the architecture?", 60)
|
||||
assert.True(t, found, "Should find the prompt globally")
|
||||
assert.Equal(t, id, foundID)
|
||||
assert.Equal(t, 1, foundNumber)
|
||||
|
||||
// Global search should find it even when a different session ID would have been used
|
||||
// (This is the core cross-session dedup scenario)
|
||||
foundID2, foundNumber2, found2 := promptStore.FindRecentPromptByTextGlobal(ctx, "What is the architecture?", 60)
|
||||
assert.True(t, found2, "Should find the prompt from any session")
|
||||
assert.Equal(t, id, foundID2)
|
||||
assert.Equal(t, 1, foundNumber2)
|
||||
|
||||
// Different text should not match
|
||||
_, _, notFound := promptStore.FindRecentPromptByTextGlobal(ctx, "Different text", 60)
|
||||
assert.False(t, notFound, "Should not find a different prompt")
|
||||
|
||||
// Should not find prompt outside time window
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _, notFound = promptStore.FindRecentPromptByTextGlobal(ctx, "What is the architecture?", 0)
|
||||
assert.False(t, notFound, "Should not find prompt outside time window")
|
||||
}
|
||||
|
||||
func TestPromptStore_GetRecentUserPromptsByProject(t *testing.T) {
|
||||
promptStore, store, cleanup := testPromptStore(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -57,6 +57,7 @@ type PromptReader interface {
|
||||
GetAllPrompts(ctx context.Context) ([]*models.UserPromptWithSession, error)
|
||||
GetRecentUserPromptsByProject(ctx context.Context, project string, limit int) ([]*models.UserPromptWithSession, error)
|
||||
FindRecentPromptByText(ctx context.Context, claudeSessionID, promptText string, withinSeconds int) (int64, int, bool)
|
||||
FindRecentPromptByTextGlobal(ctx context.Context, promptText string, withinSeconds int) (int64, int, bool)
|
||||
}
|
||||
|
||||
// PromptWriter defines write operations for prompts.
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -44,7 +45,30 @@ func (s *Service) handleSessionInit(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Reject requests from internal CLI calls (callClaudeCLI runs from /tmp with no session ID).
|
||||
// These are the memory extraction agent's own prompts, not real user prompts.
|
||||
if req.ClaudeSessionID == "" {
|
||||
log.Debug().Str("project", req.Project).Msg("Rejecting session init with empty claude session ID (internal call)")
|
||||
writeJSON(w, SessionInitResponse{
|
||||
Skipped: true,
|
||||
Reason: "internal",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Reject prompts that contain the internal system prompt (from SDK processor).
|
||||
// The SDK processor sets CLAUDE_MNEMONIC_INTERNAL=1 on the subprocess env,
|
||||
// but Claude Code does NOT propagate custom env vars to hook subprocesses,
|
||||
// so that guard fails and the system prompt leaks into the database as a
|
||||
// regular user prompt. This server-side check catches those cases reliably.
|
||||
if strings.Contains(req.Prompt, "You are a memory extraction agent for Claude Code sessions") {
|
||||
log.Debug().Str("project", req.Project).Msg("Rejecting session init with internal system prompt")
|
||||
writeJSON(w, SessionInitResponse{
|
||||
Skipped: true,
|
||||
Reason: "internal",
|
||||
})
|
||||
return
|
||||
}
|
||||
// Privacy check
|
||||
if privacy.IsEntirelyPrivate(req.Prompt) {
|
||||
// Create session but skip processing
|
||||
@@ -84,6 +108,24 @@ func (s *Service) handleSessionInit(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// CROSS-SESSION DUPLICATE DETECTION: Same prompt text from ANY session within the window.
|
||||
// This catches duplicates when the SDK processor's callClaudeCLI subprocess fires
|
||||
// UserPromptSubmit with a different Claude session ID than the original hook.
|
||||
if _, existingNum, found := s.promptStore.FindRecentPromptByTextGlobal(r.Context(), cleanedPrompt, DuplicatePromptWindowSeconds); found {
|
||||
sessionID, _ := s.sessionStore.CreateSDKSession(r.Context(), req.ClaudeSessionID, req.Project, cleanedPrompt)
|
||||
|
||||
log.Debug().
|
||||
Int64("sessionId", sessionID).
|
||||
Int("promptNumber", existingNum).
|
||||
Msg("Cross-session duplicate prompt detected")
|
||||
|
||||
writeJSON(w, SessionInitResponse{
|
||||
SessionDBID: sessionID,
|
||||
PromptNumber: existingNum,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create session (idempotent)
|
||||
sessionID, err := s.sessionStore.CreateSDKSession(r.Context(), req.ClaudeSessionID, req.Project, cleanedPrompt)
|
||||
if err != nil {
|
||||
@@ -210,6 +252,12 @@ func (s *Service) handleObservation(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Reject requests from internal CLI calls (no session ID = internal callClaudeCLI)
|
||||
if req.ClaudeSessionID == "" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Find session
|
||||
sess, err := s.sessionStore.FindAnySDKSession(r.Context(), req.ClaudeSessionID)
|
||||
|
||||
@@ -696,7 +696,12 @@ func (p *Processor) callClaudeCLI(ctx context.Context, prompt string) (string, e
|
||||
// (hooks are triggered based on working directory)
|
||||
cmd.Dir = "/tmp"
|
||||
|
||||
// Disable any plugin hooks by setting an env var that our hooks can check
|
||||
// Disable any plugin hooks by setting an env var that our hooks can check.
|
||||
// NOTE: This env var is NOT reliably propagated to hook subprocesses because
|
||||
// Claude Code constructs its own environment for hooks. The server-side check
|
||||
// in handleSessionInit (handlers_sessions.go) provides the reliable guard by
|
||||
// detecting the system prompt signature in the prompt text. This env var is
|
||||
// kept as a best-effort defense-in-depth measure for cases where it does work.
|
||||
cmd.Env = append(os.Environ(), "CLAUDE_MNEMONIC_INTERNAL=1")
|
||||
|
||||
// Capture output with size limits to prevent unbounded memory usage
|
||||
|
||||
+13
-2
@@ -410,8 +410,6 @@ func findWorkerBinary() string {
|
||||
locations := []string{
|
||||
"./worker",
|
||||
"./bin/worker",
|
||||
filepath.Join(home, ".claude/plugins/cache/claude-mnemonic/claude-mnemonic/1.0.0/worker"),
|
||||
filepath.Join(home, ".claude/plugins/marketplaces/claude-mnemonic/worker"),
|
||||
}
|
||||
|
||||
for _, loc := range locations {
|
||||
@@ -420,6 +418,19 @@ func findWorkerBinary() string {
|
||||
}
|
||||
}
|
||||
|
||||
// Try cache directory with any version (glob returns lexically sorted matches)
|
||||
matches, _ := filepath.Glob(filepath.Join(home, ".claude/plugins/cache/claude-mnemonic/claude-mnemonic/*/worker"))
|
||||
if len(matches) > 0 {
|
||||
// Use the last match (latest version due to lexical sorting)
|
||||
return matches[len(matches)-1]
|
||||
}
|
||||
|
||||
// Try marketplaces directory
|
||||
marketplacePath := filepath.Join(home, ".claude/plugins/marketplaces/claude-mnemonic/worker")
|
||||
if _, err := os.Stat(marketplacePath); err == nil {
|
||||
return marketplacePath
|
||||
}
|
||||
|
||||
// Try PATH
|
||||
if path, err := exec.LookPath("claude-mnemonic-worker"); err == nil {
|
||||
return path
|
||||
|
||||
@@ -5,5 +5,14 @@
|
||||
"author": {
|
||||
"name": "lukaszraczylo",
|
||||
"email": "lukaszraczylo@users.noreply.github.com"
|
||||
}
|
||||
},
|
||||
"hooks": "./hooks/hooks.json",
|
||||
"mcpServers": {
|
||||
"claude-mnemonic": {
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/mcp-server",
|
||||
"args": ["--project", "${CLAUDE_PROJECT}"],
|
||||
"env": {}
|
||||
}
|
||||
},
|
||||
"commands": ["./commands/restart.md"]
|
||||
}
|
||||
|
||||
+110
-51
@@ -69,7 +69,101 @@ if [ ! -f "$MARKETPLACES_FILE" ]; then
|
||||
echo '{}' > "$MARKETPLACES_FILE"
|
||||
fi
|
||||
|
||||
# Check if jq is available
|
||||
# Validate marketplace path exists and contains expected files
|
||||
if [ ! -d "$MARKETPLACE_PATH" ]; then
|
||||
echo "Warning: Marketplace directory not found at $MARKETPLACE_PATH"
|
||||
echo "Plugin files may not be copied to cache correctly."
|
||||
fi
|
||||
|
||||
# Ensure cache directory exists and copy plugin files
|
||||
mkdir -p "$CACHE_PATH/.claude-plugin"
|
||||
mkdir -p "$CACHE_PATH/hooks"
|
||||
mkdir -p "$CACHE_PATH/commands"
|
||||
|
||||
# Copy files from marketplace to cache
|
||||
if ! cp -r "$MARKETPLACE_PATH/"* "$CACHE_PATH/" 2>/dev/null; then
|
||||
echo "ERROR: Failed to copy plugin files to cache directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify critical files exist in cache
|
||||
for f in worker mcp-server hooks/hooks.json .claude-plugin/plugin.json; do
|
||||
if [ ! -f "$CACHE_PATH/$f" ]; then
|
||||
echo "WARNING: Expected file $f not found in cache after copy"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- JSON registration ---
|
||||
# Uses jq if available, falls back to python3 for systems without jq.
|
||||
|
||||
STATUSLINE_CMD="\${CLAUDE_PLUGIN_ROOT}/hooks/statusline"
|
||||
|
||||
# Helper: register plugin using python3 (jq-free fallback)
|
||||
register_with_python() {
|
||||
python3 - "$PLUGINS_FILE" "$SETTINGS_FILE" "$MARKETPLACES_FILE" \
|
||||
"$PLUGIN_KEY" "$CACHE_PATH" "$VERSION" "$TIMESTAMP" \
|
||||
"$STATUSLINE_CMD" "$MARKETPLACE_NAME" "$MARKETPLACE_PATH" <<'PYEOF'
|
||||
import json, sys, os
|
||||
|
||||
plugins_file, settings_file, marketplaces_file = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
plugin_key, cache_path, version, timestamp = sys.argv[4], sys.argv[5], sys.argv[6], sys.argv[7]
|
||||
statusline_cmd, marketplace_name, marketplace_path = sys.argv[8], sys.argv[9], sys.argv[10]
|
||||
|
||||
def load_json(path):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
return {}
|
||||
|
||||
def save_json(path, data):
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
os.replace(tmp, path)
|
||||
|
||||
# 1. installed_plugins.json
|
||||
plugins = load_json(plugins_file)
|
||||
plugins.setdefault("version", 2)
|
||||
plugins.setdefault("plugins", {})
|
||||
plugins["plugins"][plugin_key] = [{
|
||||
"scope": "user",
|
||||
"installPath": cache_path,
|
||||
"version": version,
|
||||
"installedAt": timestamp,
|
||||
"lastUpdated": timestamp,
|
||||
"isLocal": True
|
||||
}]
|
||||
save_json(plugins_file, plugins)
|
||||
print("Plugin registered in installed_plugins.json")
|
||||
|
||||
# 2. settings.json
|
||||
settings = load_json(settings_file)
|
||||
settings.setdefault("enabledPlugins", {})
|
||||
settings["enabledPlugins"][plugin_key] = True
|
||||
settings["statusLine"] = {
|
||||
"type": "command",
|
||||
"command": statusline_cmd,
|
||||
"padding": 0
|
||||
}
|
||||
save_json(settings_file, settings)
|
||||
print("Plugin enabled in settings.json")
|
||||
print("Statusline configured in settings.json")
|
||||
|
||||
# 3. known_marketplaces.json
|
||||
marketplaces = load_json(marketplaces_file)
|
||||
marketplaces[marketplace_name] = {
|
||||
"source": {"source": "directory", "path": marketplace_path},
|
||||
"installLocation": marketplace_path,
|
||||
"lastUpdated": timestamp
|
||||
}
|
||||
save_json(marketplaces_file, marketplaces)
|
||||
print("Marketplace registered in known_marketplaces.json")
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# Detect JSON tool and register
|
||||
if command -v jq &> /dev/null; then
|
||||
# Validate jq version (1.6+ required for //= operator)
|
||||
JQ_VERSION=$(jq --version 2>/dev/null | sed 's/jq-//')
|
||||
@@ -77,26 +171,18 @@ if command -v jq &> /dev/null; then
|
||||
JQ_MINOR=$(echo "$JQ_VERSION" | cut -d. -f2)
|
||||
if [ -n "$JQ_MAJOR" ] && [ -n "$JQ_MINOR" ]; then
|
||||
if [ "$JQ_MAJOR" -lt 1 ] || { [ "$JQ_MAJOR" -eq 1 ] && [ "$JQ_MINOR" -lt 6 ]; }; then
|
||||
echo "ERROR: jq 1.6+ is required (found jq-$JQ_VERSION)"
|
||||
echo "Please upgrade jq: brew install jq (macOS) or apt-get install jq (Linux)"
|
||||
exit 1
|
||||
echo "jq $JQ_VERSION found but too old (need 1.6+), trying python3..."
|
||||
if command -v python3 &> /dev/null; then
|
||||
register_with_python
|
||||
else
|
||||
echo "ERROR: jq 1.6+ or python3 is required for plugin registration"
|
||||
exit 1
|
||||
fi
|
||||
echo "Plugin registered successfully using python3"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate marketplace path exists and contains expected files
|
||||
if [ ! -d "$MARKETPLACE_PATH" ]; then
|
||||
echo "Warning: Marketplace directory not found at $MARKETPLACE_PATH"
|
||||
echo "Plugin files may not be copied to cache correctly."
|
||||
fi
|
||||
|
||||
# Ensure cache directory exists and copy plugin files
|
||||
mkdir -p "$CACHE_PATH/.claude-plugin"
|
||||
mkdir -p "$CACHE_PATH/hooks"
|
||||
mkdir -p "$CACHE_PATH/commands"
|
||||
|
||||
# Copy files from marketplace to cache
|
||||
cp -r "$MARKETPLACE_PATH/"* "$CACHE_PATH/" 2>/dev/null || true
|
||||
|
||||
# Use jq for proper JSON manipulation
|
||||
PLUGIN_ENTRY=$(cat <<EOF
|
||||
[{
|
||||
@@ -117,8 +203,6 @@ EOF
|
||||
echo "Plugin registered in installed_plugins.json"
|
||||
|
||||
# Enable the plugin in settings.json and configure statusline
|
||||
# First ensure enabledPlugins object exists, then add our plugin
|
||||
STATUSLINE_CMD="$MARKETPLACE_PATH/hooks/statusline"
|
||||
STATUSLINE_ENTRY=$(cat <<EOF
|
||||
{
|
||||
"type": "command",
|
||||
@@ -151,38 +235,13 @@ EOF
|
||||
'.[$key] = $entry' "$MARKETPLACES_FILE"
|
||||
|
||||
echo "Marketplace registered in known_marketplaces.json"
|
||||
|
||||
# Register MCP server in settings.json
|
||||
MCP_BINARY="$MARKETPLACE_PATH/mcp-server"
|
||||
if [ -f "$MCP_BINARY" ]; then
|
||||
echo "Registering MCP server in settings.json..."
|
||||
|
||||
# MCP server entry - note the escaped ${CLAUDE_PROJECT}
|
||||
MCP_ENTRY=$(cat <<'EOF'
|
||||
{
|
||||
"command": "MCP_BINARY_PLACEHOLDER",
|
||||
"args": ["--project", "${CLAUDE_PROJECT}"],
|
||||
"env": {}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
# Replace placeholder with actual path
|
||||
MCP_ENTRY=$(echo "$MCP_ENTRY" | sed "s|MCP_BINARY_PLACEHOLDER|$MCP_BINARY|g")
|
||||
|
||||
# Add or update mcpServers field
|
||||
if safe_jq_write --arg key "claude-mnemonic" --argjson entry "$MCP_ENTRY" \
|
||||
'.mcpServers //= {} | .mcpServers[$key] = $entry' "$SETTINGS_FILE"; then
|
||||
echo "MCP server registered successfully"
|
||||
else
|
||||
echo "Warning: Failed to register MCP server (jq error)"
|
||||
fi
|
||||
else
|
||||
echo "MCP server binary not found at $MCP_BINARY, skipping MCP registration"
|
||||
fi
|
||||
|
||||
echo "Plugin registered successfully using jq"
|
||||
|
||||
elif command -v python3 &> /dev/null; then
|
||||
register_with_python
|
||||
echo "Plugin registered successfully using python3"
|
||||
else
|
||||
echo "ERROR: jq is required for plugin registration"
|
||||
echo "Please install jq: brew install jq (macOS) or apt-get install jq (Linux)"
|
||||
echo "ERROR: jq or python3 is required for plugin registration"
|
||||
echo "Please install one: brew install jq (macOS) or apt-get install jq (Linux)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "v0.11.43-2-g11fd196-dirty",
|
||||
"version": "v0.11.47-3-gfcab3eb-dirty",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "v0.11.43-2-g11fd196-dirty",
|
||||
"version": "v0.11.47-3-gfcab3eb-dirty",
|
||||
"dependencies": {
|
||||
"vis-data": "^7.1.9",
|
||||
"vis-network": "^9.1.9",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "v0.11.43-2-g11fd196-dirty",
|
||||
"version": "v0.11.47-3-gfcab3eb-dirty",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user