Skip to content

Fix: rename reserved AppleScript variable in getGroupMembers - #4

Open
oichtental wants to merge 2 commits into
RyanLisse:mainfrom
oichtental:main
Open

oichtental wants to merge 2 commits into
RyanLisse:mainfrom
oichtental:main

Conversation

@oichtental

@oichtental oichtental commented Feb 8, 2026

Copy link
Copy Markdown

Summary

  • Fix groups members crash: line is a reserved word in AppleScript, causing error -10003 ("Zugriff nicht erlaubt" / access not allowed). Renamed to recordLine to match the convention used in all other AppleScript blocks in ContactsService.swift.
  • Fix MCP server premature exit: Added waitUntilCompleted() so the MCP server stays alive after start().

Test plan

  • contactbook groups members "Privat" — returns 70 members (was crashing before)
  • contactbook groups members "Sophia" --json — returns valid JSON
  • contactbook contacts list, contacts search, contacts get, lookup — all still work

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved contacts listing consistency, including missing values, addresses, text formatting, and contact limits.
    • Prevented command hangs during output processing and added clear timeout errors.
    • Preserved error reporting when commands fail.
    • Updated server shutdown flow to wait for completion.
  • Chores

    • Updated ignore rules to exclude session image cache files.

…etGroupMembers

`line` is a reserved word in AppleScript, causing groups members to crash
with "Zugriff nicht erlaubt" (-10003). Also add waitUntilCompleted() to
MCP server so it doesn't exit prematurely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Updates AppleScript execution to drain process output safely, adds timeout errors, and builds contact records from bulk properties. The server now waits for completion. The Swift SDK requirement and Git ignore rules are updated.

Changes

Contactbook runtime and repository updates

Layer / File(s) Summary
AppleScript execution and contact record construction
Sources/Core/Services/ContactsService.swift
Concurrent pipe draining prevents process deadlocks. Timeout handling now throws an explicit error and preserves stderr output. Contact listing uses bulk properties, sanitization, missing-value handling, address aggregation, local limits, and consistent group-member output naming.
Server completion handling
Sources/MCP/ContactbookMCPServer.swift
The server waits for completion after transport startup.
Repository configuration
Package.swift, .gitignore
The swift-sdk minimum version changes to 0.12.1. The image-cache/ directory is ignored.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately identifies the AppleScript variable rename in getGroupMembers, which is a documented and relevant change in the pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tlmquintino

tlmquintino commented May 25, 2026

Copy link
Copy Markdown

I hit this same issue with the reserved 'line' and I confirm this fix works.
BTW, excellent tool -- very useful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
Sources/Core/Services/ContactsService.swift (2)

62-65: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Replace the sleep-poll loop with an event-driven wait.

runAppleScript is a synchronous method on the ContactsService actor. The loop blocks the calling cooperative-pool thread for up to timeout seconds. lookupByPhone passes 180 s. Concurrent MCP tool calls therefore hold a Swift concurrency thread while no work runs.

Use process.terminationHandler with a withCheckedThrowingContinuation, or run the blocking wait on a dedicated thread outside the actor. That keeps the timeout behavior and frees the executor thread.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Core/Services/ContactsService.swift` around lines 62 - 65, Replace
the sleep-poll loop in runAppleScript with an event-driven process wait using
process.terminationHandler and a withCheckedThrowingContinuation, while
preserving the existing timeout behavior and termination-result handling. Ensure
the continuation resumes exactly once on process termination or timeout, without
blocking the ContactsService actor’s cooperative executor thread.

138-156: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Fetch only the requested number of contacts.

The AppleScript block reads 13 properties for every person, then the Swift limit trims the output. Fetch people 1 thru min(limit ?? 50, count of people) after guarding against zero people. This avoids transferring the full database when the user requests only one record.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Core/Services/ContactsService.swift` around lines 138 - 156, Update
the AppleScript around the Contacts fetch to determine the requested limit and
total people count, return safely when there are zero people, and fetch only
people 1 thru min(limit ?? 50, count of people). Remove the later full-result
trimming via n while preserving the existing property extraction and output
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Core/Services/ContactsService.swift`:
- Around line 160-170: Update the AppleScript address-building logic in
parseContacts to use a blank-on-missing helper for street, city, state, ZIP, and
country values, rather than calling txt directly. Define the helper next to txt
and apply it to every address component so missing fields contribute empty
strings and never the literal “missing value”.
- Around line 67-79: Update the process cleanup around terminate() and
ioGroup.wait() to enforce bounded reader cleanup: allow the existing SIGTERM
grace period, then send SIGKILL if the process remains running, and bound the
normal-exit ioGroup.wait() rather than waiting indefinitely. Ensure both timeout
and normal exit paths cannot leave reader threads or pipe descriptors blocked.

---

Nitpick comments:
In `@Sources/Core/Services/ContactsService.swift`:
- Around line 62-65: Replace the sleep-poll loop in runAppleScript with an
event-driven process wait using process.terminationHandler and a
withCheckedThrowingContinuation, while preserving the existing timeout behavior
and termination-result handling. Ensure the continuation resumes exactly once on
process termination or timeout, without blocking the ContactsService actor’s
cooperative executor thread.
- Around line 138-156: Update the AppleScript around the Contacts fetch to
determine the requested limit and total people count, return safely when there
are zero people, and fetch only people 1 thru min(limit ?? 50, count of people).
Remove the later full-result trimming via n while preserving the existing
property extraction and output behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 447a399d-c9cb-4ea0-960d-4c0db8591034

📥 Commits

Reviewing files that changed from the base of the PR and between e2f9c81 and aa34fbe.

📒 Files selected for processing (2)
  • Package.swift
  • Sources/Core/Services/ContactsService.swift

Comment on lines +67 to +79
if process.isRunning {
process.terminate()
return ""
_ = ioGroup.wait(timeout: .now() + 5)
// Surface the timeout instead of returning "" — an empty string parses as
// "no contacts", which is indistinguishable from a genuinely empty result
// and silently hides the real failure from callers (and from MCP clients).
throw ContactsError.operationFailed(
"AppleScript timed out after \(Int(timeout))s (Contacts.app may be busy or syncing)"
)
}

let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
// Both reads end when osascript closes its descriptors on exit.
ioGroup.wait()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the reader cleanup and escalate to SIGKILL.

Two problems exist in the exit paths.

Line 68 calls terminate(), which sends SIGTERM. If osascript does not exit, the two reader closures stay blocked in readDataToEndOfFile() forever. Line 69 bounds only the caller's wait, not the reader threads. Each timed-out call then leaks two threads on the concurrent queue and both pipe file descriptors.

Line 79 calls ioGroup.wait() with no bound. The reads finish only when every writer descriptor closes. If osascript leaves a child process that inherited the write end, this call never returns, and the timeout that this change adds is bypassed.

Escalate to SIGKILL after the grace period, and bound the normal wait.

🛡️ Proposed fix
         if process.isRunning {
             process.terminate()
-            _ = ioGroup.wait(timeout: .now() + 5)
+            if ioGroup.wait(timeout: .now() + 5) == .timedOut {
+                kill(process.processIdentifier, SIGKILL)
+                _ = ioGroup.wait(timeout: .now() + 2)
+            }
             // Surface the timeout instead of returning "" — an empty string parses as
             // "no contacts", which is indistinguishable from a genuinely empty result
             // and silently hides the real failure from callers (and from MCP clients).
             throw ContactsError.operationFailed(
                 "AppleScript timed out after \(Int(timeout))s (Contacts.app may be busy or syncing)"
             )
         }
 
         // Both reads end when osascript closes its descriptors on exit.
-        ioGroup.wait()
+        if ioGroup.wait(timeout: .now() + 5) == .timedOut {
+            throw ContactsError.operationFailed(
+                "AppleScript output could not be read after osascript exited"
+            )
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Core/Services/ContactsService.swift` around lines 67 - 79, Update the
process cleanup around terminate() and ioGroup.wait() to enforce bounded reader
cleanup: allow the existing SIGTERM grace period, then send SIGKILL if the
process remains running, and bound the normal-exit ioGroup.wait() rather than
waiting indefinitely. Ensure both timeout and normal exit paths cannot leave
reader threads or pipe descriptors blocked.

Comment on lines +160 to +170
set addrOut to ""
set streets to item i of stAll
if streets is not missing value then
repeat with j from 1 to (count of streets)
set part to my txt(item j of streets)
if part is "missing value" then set part to ""
set part to part & ", " & my txt(item j of (item i of ctAll)) & ", " & my txt(item j of (item i of staAll)) & " " & my txt(item j of (item i of zipAll)) & ", " & my txt(item j of (item i of cnAll))
if addrOut is not "" then set addrOut to addrOut & ";;;"
set addrOut to addrOut & part
end repeat
end if

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Address parts insert the literal text missing value.

Line 164 maps a missing street to an empty string. Lines 166 does not do the same for city, state, zip, and country. txt returns the literal string "missing value" for those fields.

A contact with a street but no state then produces Hauptstr. 1, Berlin, missing value 10115, DE. parseContacts compares only the complete field against "missing value" at line 575, so the text reaches the CLI output and the MCP JSON.

Add a blank-on-missing helper and use it for every address part.

🐛 Proposed fix
         on cleanTxt(v)

Add the helper next to txt:

+        on blankTxt(v)
+            set s to my txt(v)
+            if s is "missing value" then return ""
+            return s
+        end blankTxt
+
         on cleanTxt(v)

Then use it for all address parts:

                 repeat with j from 1 to (count of streets)
-                    set part to my txt(item j of streets)
-                    if part is "missing value" then set part to ""
-                    set part to part & ", " & my txt(item j of (item i of ctAll)) & ", " & my txt(item j of (item i of staAll)) & " " & my txt(item j of (item i of zipAll)) & ", " & my txt(item j of (item i of cnAll))
+                    set part to my blankTxt(item j of streets)
+                    set part to part & ", " & my blankTxt(item j of (item i of ctAll)) & ", " & my blankTxt(item j of (item i of staAll)) & " " & my blankTxt(item j of (item i of zipAll)) & ", " & my blankTxt(item j of (item i of cnAll))
                     if addrOut is not "" then set addrOut to addrOut & ";;;"
                     set addrOut to addrOut & part
                 end repeat
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Core/Services/ContactsService.swift` around lines 160 - 170, Update
the AppleScript address-building logic in parseContacts to use a
blank-on-missing helper for street, city, state, ZIP, and country values, rather
than calling txt directly. Define the helper next to txt and apply it to every
address component so missing fields contribute empty strings and never the
literal “missing value”.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants