Skip to content

Commit afeb18c

Browse files
jsflaxclaude
andcommitted
Add Sparkle auto-update, DMG distribution, and 3D visualizer features
Sparkle integration: SPUStandardUpdaterController with CheckForUpdatesView menu item, appcast.xml feed, Ed25519 signed updates. CLIInstaller syncs bundled MCP server, hooks, and agents to ~/.claude/bin/ on app launch when the app version is newer. Release pipeline overhauled: xcodebuild archive, Developer ID signing, DMG creation via create-dmg, notarization, Sparkle appcast generation, and appcast.xml committed back to main. 3D visualizer: hub expansion with Fibonacci sphere orbits, edge flow particles, search spotlight, and related Graph3DView/GraphView updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 119c114 commit afeb18c

13 files changed

Lines changed: 903 additions & 29 deletions

File tree

.github/workflows/release.yml

Lines changed: 203 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ jobs:
1717
- name: Run tests
1818
run: swift test
1919

20-
- name: Build release binary
20+
- name: Build CLI release binaries
2121
run: swift build -c release
2222

23-
- name: Package artifacts
23+
- name: Package CLI tarball
2424
run: |
2525
mkdir -p staging/agents
2626
cp .build/release/ClaudeMemory staging/memory
@@ -31,12 +31,166 @@ jobs:
3131
cp agents/*.md staging/agents/
3232
cd staging && tar czf ../claude-memory-macos-arm64.tar.gz *
3333
34-
- name: Create release
34+
- name: Import signing certificate
35+
if: env.DEVELOPER_ID_CERT_BASE64 != ''
36+
env:
37+
DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }}
38+
DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
39+
run: |
40+
KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db
41+
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
42+
43+
# Create temporary keychain
44+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
45+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
46+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
47+
48+
# Import certificate
49+
echo "$DEVELOPER_ID_CERT_BASE64" | base64 --decode > $RUNNER_TEMP/cert.p12
50+
security import $RUNNER_TEMP/cert.p12 -P "$DEVELOPER_ID_CERT_PASSWORD" \
51+
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
52+
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
53+
54+
# Add to search list
55+
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
56+
57+
- name: Archive app with xcodebuild
58+
if: env.DEVELOPER_ID_CERT_BASE64 != ''
59+
env:
60+
DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }}
61+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
62+
run: |
63+
VERSION=${GITHUB_REF_NAME#v}
64+
xcodebuild archive \
65+
-project MemoryVisualizer.xcodeproj \
66+
-scheme MemoryVisualizer \
67+
-archivePath build/MemoryVisualizer.xcarchive \
68+
-configuration Release \
69+
CODE_SIGN_IDENTITY="Developer ID Application" \
70+
CODE_SIGN_STYLE=Manual \
71+
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
72+
MARKETING_VERSION="$VERSION" \
73+
OTHER_CODE_SIGN_FLAGS="--keychain $RUNNER_TEMP/signing.keychain-db"
74+
75+
- name: Export archive to .app
76+
if: env.DEVELOPER_ID_CERT_BASE64 != ''
77+
env:
78+
DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }}
79+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
80+
run: |
81+
# Inject team ID into ExportOptions.plist
82+
sed -i '' "s|</dict>| <key>teamID</key><string>$APPLE_TEAM_ID</string></dict>|" ExportOptions.plist
83+
xcodebuild -exportArchive \
84+
-archivePath build/MemoryVisualizer.xcarchive \
85+
-exportPath build/export \
86+
-exportOptionsPlist ExportOptions.plist
87+
88+
- name: Bundle CLI binaries into .app
89+
if: env.DEVELOPER_ID_CERT_BASE64 != ''
90+
env:
91+
DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }}
92+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
93+
run: |
94+
APP="build/export/MemoryVisualizer.app"
95+
CLI_DIR="$APP/Contents/Resources/cli"
96+
AGENTS_DIR="$CLI_DIR/agents"
97+
mkdir -p "$CLI_DIR" "$AGENTS_DIR"
98+
99+
cp .build/release/ClaudeMemory "$CLI_DIR/memory"
100+
cp .build/release/ClaudeMemoryHooks "$CLI_DIR/memory-hooks"
101+
cp -R .build/release/ClaudeMemory_ClaudeMemoryLib.bundle "$CLI_DIR/"
102+
cp -R .build/release/swift-transformers_Hub.bundle "$CLI_DIR/"
103+
cp -R .build/release/SwiftLM_SwiftLM.bundle "$CLI_DIR/"
104+
cp agents/*.md "$AGENTS_DIR/"
105+
106+
# Re-codesign since contents changed
107+
codesign --deep --force --sign "Developer ID Application" \
108+
--keychain "$RUNNER_TEMP/signing.keychain-db" \
109+
--options runtime \
110+
"$APP"
111+
112+
- name: Create DMG
113+
if: env.DEVELOPER_ID_CERT_BASE64 != ''
114+
env:
115+
DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }}
116+
run: |
117+
brew install create-dmg
118+
VERSION=${GITHUB_REF_NAME#v}
119+
create-dmg \
120+
--volname "MemoryVisualizer $VERSION" \
121+
--window-pos 200 120 \
122+
--window-size 600 400 \
123+
--icon-size 100 \
124+
--icon "MemoryVisualizer.app" 150 190 \
125+
--app-drop-link 450 190 \
126+
--no-internet-enable \
127+
"MemoryVisualizer-${VERSION}.dmg" \
128+
"build/export/MemoryVisualizer.app"
129+
130+
- name: Notarize DMG
131+
if: env.DEVELOPER_ID_CERT_BASE64 != ''
132+
env:
133+
DEVELOPER_ID_CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_BASE64 }}
134+
APPLE_ID: ${{ secrets.APPLE_ID }}
135+
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
136+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
137+
run: |
138+
VERSION=${GITHUB_REF_NAME#v}
139+
DMG="MemoryVisualizer-${VERSION}.dmg"
140+
141+
xcrun notarytool submit "$DMG" \
142+
--apple-id "$APPLE_ID" \
143+
--password "$APPLE_APP_PASSWORD" \
144+
--team-id "$APPLE_TEAM_ID" \
145+
--wait
146+
147+
xcrun stapler staple "$DMG"
148+
149+
- name: Generate Sparkle appcast
150+
if: env.SPARKLE_PRIVATE_KEY != ''
151+
env:
152+
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
153+
run: |
154+
VERSION=${GITHUB_REF_NAME#v}
155+
DMG="MemoryVisualizer-${VERSION}.dmg"
156+
157+
# Download Sparkle tools
158+
SPARKLE_VERSION="2.7.5"
159+
curl -sL "https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz" | tar xJ -C /tmp/sparkle-tools bin/
160+
161+
# Move DMG to a staging dir for generate_appcast
162+
mkdir -p appcast_staging
163+
cp "$DMG" appcast_staging/
164+
165+
# Generate appcast from the DMG
166+
echo "$SPARKLE_PRIVATE_KEY" | /tmp/sparkle-tools/bin/generate_appcast \
167+
--ed-key-file - \
168+
--download-url-prefix "https://github.com/jsflax/ClaudeMemory/releases/download/${GITHUB_REF_NAME}/" \
169+
appcast_staging
170+
171+
# Use generated appcast
172+
cp appcast_staging/appcast.xml appcast.xml
173+
174+
- name: Create GitHub Release
35175
uses: softprops/action-gh-release@v2
36176
with:
37-
files: claude-memory-macos-arm64.tar.gz
177+
files: |
178+
claude-memory-macos-arm64.tar.gz
179+
MemoryVisualizer-*.dmg
38180
generate_release_notes: true
39181

182+
- name: Commit appcast.xml to main
183+
if: env.SPARKLE_PRIVATE_KEY != ''
184+
env:
185+
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
186+
run: |
187+
git config user.name "github-actions[bot]"
188+
git config user.email "github-actions[bot]@users.noreply.github.com"
189+
git fetch origin main
190+
git checkout main
191+
git add appcast.xml
192+
git diff --cached --quiet || (git commit -m "Update appcast.xml for ${GITHUB_REF_NAME}" && git push origin main)
193+
40194
- name: Update Homebrew formula
41195
if: env.TAP_TOKEN != ''
42196
env:
@@ -56,3 +210,48 @@ jobs:
56210
git config user.email "github-actions[bot]@users.noreply.github.com"
57211
git add Formula/claude-memory.rb
58212
git diff --cached --quiet || (git commit -m "Update claude-memory to ${VERSION}" && git push)
213+
214+
- name: Notify Slack
215+
if: always() && secrets.SLACK_WEBHOOK_URL != ''
216+
env:
217+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
218+
run: |
219+
TAG="${GITHUB_REF_NAME}"
220+
REPO="${{ github.repository }}"
221+
URL="https://github.com/${REPO}/releases/tag/${TAG}"
222+
STATUS="${{ job.status }}"
223+
224+
if [ "$STATUS" = "success" ]; then
225+
EMOJI="✅"
226+
TEXT="Release *${TAG}* published successfully"
227+
else
228+
EMOJI="❌"
229+
TEXT="Release *${TAG}* failed"
230+
fi
231+
232+
curl -s -X POST "$SLACK_WEBHOOK_URL" \
233+
-H 'Content-Type: application/json' \
234+
-d "{
235+
\"blocks\": [
236+
{
237+
\"type\": \"header\",
238+
\"text\": {\"type\": \"plain_text\", \"text\": \"${EMOJI} ClaudeMemory ${TAG}\"}
239+
},
240+
{
241+
\"type\": \"section\",
242+
\"text\": {\"type\": \"mrkdwn\", \"text\": \"${TEXT}\"},
243+
\"accessory\": {
244+
\"type\": \"button\",
245+
\"text\": {\"type\": \"plain_text\", \"text\": \"View Release\"},
246+
\"url\": \"${URL}\"
247+
}
248+
}
249+
]
250+
}"
251+
252+
- name: Cleanup keychain
253+
if: always()
254+
run: |
255+
if [ -f "$RUNNER_TEMP/signing.keychain-db" ]; then
256+
security delete-keychain "$RUNNER_TEMP/signing.keychain-db" 2>/dev/null || true
257+
fi

ExportOptions.plist

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>method</key>
6+
<string>developer-id</string>
7+
<key>signingStyle</key>
8+
<string>manual</string>
9+
<key>signingCertificate</key>
10+
<string>Developer ID Application</string>
11+
</dict>
12+
</plist>

MemoryVisualizer.xcodeproj/project.pbxproj

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
A1B2C3D400000000000001 /* ClaudeMemoryLib in Frameworks */ = {isa = PBXBuildFile; productRef = D4E5F60000000000000001 /* ClaudeMemoryLib */; };
1212
A1B2C3D400000000000002 /* Lattice in Frameworks */ = {isa = PBXBuildFile; productRef = D4E5F60000000000000002 /* Lattice */; };
1313
A1B2C3D400000000000003 /* RealityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2C3D4E500000000000001 /* RealityKit.framework */; };
14+
A1B2C3D400000000000004 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = D4E5F60000000000000003 /* Sparkle */; };
1415
/* End PBXBuildFile section */
1516

1617
/* Begin PBXContainerItemProxy section */
@@ -51,6 +52,7 @@
5152
A1B2C3D400000000000001 /* ClaudeMemoryLib in Frameworks */,
5253
A1B2C3D400000000000002 /* Lattice in Frameworks */,
5354
A1B2C3D400000000000003 /* RealityKit.framework in Frameworks */,
55+
A1B2C3D400000000000004 /* Sparkle in Frameworks */,
5456
);
5557
runOnlyForDeploymentPostprocessing = 0;
5658
};
@@ -114,6 +116,7 @@
114116
packageProductDependencies = (
115117
D4E5F60000000000000001 /* ClaudeMemoryLib */,
116118
D4E5F60000000000000002 /* Lattice */,
119+
D4E5F60000000000000003 /* Sparkle */,
117120
);
118121
productName = MemoryVisualizer;
119122
productReference = B2C3D4E500000000000002 /* MemoryVisualizer.app */;
@@ -265,6 +268,9 @@
265268
GENERATE_INFOPLIST_FILE = YES;
266269
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
267270
INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "To access the Claude memory DB.";
271+
INFOPLIST_KEY_SUEnableAutomaticChecks = YES;
272+
INFOPLIST_KEY_SUFeedURL = "https://raw.githubusercontent.com/jsflax/ClaudeMemory/main/appcast.xml";
273+
INFOPLIST_KEY_SUPublicEDKey = "1dJlcpyVu57oSerP7cfvyRT0PHx7IuvCDbRSptbflfE=";
268274
LD_RUNPATH_SEARCH_PATHS = (
269275
"$(inherited)",
270276
"@executable_path/../Frameworks",
@@ -288,6 +294,9 @@
288294
GENERATE_INFOPLIST_FILE = YES;
289295
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
290296
INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "To access the Claude memory DB.";
297+
INFOPLIST_KEY_SUEnableAutomaticChecks = YES;
298+
INFOPLIST_KEY_SUFeedURL = "https://raw.githubusercontent.com/jsflax/ClaudeMemory/main/appcast.xml";
299+
INFOPLIST_KEY_SUPublicEDKey = "1dJlcpyVu57oSerP7cfvyRT0PHx7IuvCDbRSptbflfE=";
291300
LD_RUNPATH_SEARCH_PATHS = (
292301
"$(inherited)",
293302
"@executable_path/../Frameworks",
@@ -379,6 +388,10 @@
379388
isa = XCSwiftPackageProductDependency;
380389
productName = Lattice;
381390
};
391+
D4E5F60000000000000003 /* Sparkle */ = {
392+
isa = XCSwiftPackageProductDependency;
393+
productName = Sparkle;
394+
};
382395
/* End XCSwiftPackageProductDependency section */
383396
};
384397
rootObject = C3D4E5F600000000000010 /* Project object */;

Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ let package = Package(
1313
.package(url: "https://github.com/jsflax/lattice.git", from: "0.3.2"),
1414
.package(url: "https://github.com/jsflax/SwiftLM.git", branch: "main"),
1515
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
16+
.package(url: "https://github.com/sparkle-project/Sparkle.git", from: "2.0.0"),
1617
],
1718
targets: [
1819
.target(
@@ -49,6 +50,7 @@ let package = Package(
4950
dependencies: [
5051
"ClaudeMemoryLib",
5152
.product(name: "Lattice", package: "Lattice"),
53+
.product(name: "Sparkle", package: "Sparkle"),
5254
],
5355
swiftSettings: [
5456
.interoperabilityMode(.Cxx),

0 commit comments

Comments
 (0)