Repository navigation
feat: save new contacts through the Mac bridge - #91
Conversation
ffbdaac to
3accc5f
Compare
3accc5f to
70da7fc
Compare
|
Same note as on #90: we publish a weekly update crediting contributors and tag by X handle where we can verify one, and I could not find one published for you. Reply with the handle if you want to be tagged, or say you would rather not and we will credit you as Kb2uka. |
… cannot WHAT: CHANGE.log and WEEKLY-NOTES.md now require asking Fred the SAME DAY a PR merges when the handle lookup verifies nothing, instead of leaving a gap for the post. WEEKLY-NOTES gains a 'Handles Fred still needs to answer' section, which is where an open question lives until it resolves. WHY: Fred, standing rule, 2026-09-18. A thank-you without the @ never reaches the person and a guessed @ tags a stranger; he often knows them or can ask. ANSWERED TODAY: @jethrojones confirmed for Jethro Jones (X name matches, posts about building apps and about schools, which fits his known work). Fred is asking Ian Swope himself, 12 merged PRs and the largest untagged gap; he stays untagged until he answers, no guessing. ASKED THE CONTRIBUTORS DIRECTLY on their own PRs, offering 'I would rather not be tagged' as an equally good answer: cw228 (#100), Kb2uka (#90, #91) and cjoh (#86, whose report produced the UTC fix). LR-T: blip, contributors, handles, weekly-update LR-D: blip, community LR-K: ask-fred-early-for-handles, offer-rather-not-be-tagged Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ainpzHJ5F7nsV4hB6HpZy
|
Reviewed this properly, and the short version is that I want it. It is the safest contact-write surface of anything open on this repo. Context first, since it changes where this PR sits: I closed #90 today, as a product decision rather than a quality one, and explained there why. That makes this the surviving half of the split I asked you for, so it is worth finishing rather than parking. What I checked and what holds up:
Three things before it can land, in order of how much they matter: 1. 2. 3.
Two things I am not asking you to change, but want named in the PR rather than left implicit:
The real cost of this feature is the one your Take it out of draft when the rebase is done and I will merge it. |
|
@StudioXRadio — tag away. Rebasing onto current main now and working the three findings: On #90 — appreciated, and no hard feelings. I'd rather have the decision right than early. Happy to come back with Undo Send on the terms you set now that #36 is closed. |
Split the action from nixfred#79 for independent permission and confined-key review. Preserve bounded stdin, native verification, and refusal behavior. Reported by ianswope.
contact-save.ts spawned join(HOME,'bin','contact-save') directly, so a user
with bin_dir= set in bridge.conf got a broken save while every other caller
followed the key.
The guard test that exists for this ("no spawner hard-codes ~/bin") did not
catch it: it matched only double-quoted "bin", "imsg" and the tools imsg and
contacts. Widened to every quote style and all five shim tools, it also found
the same regression already on main in contact-details.ts (exact-card details)
and contact-vcard.ts (vCard export), both fixed here.
contact-save joins the ShimTool union. A new test asserts the save spawns
where bin_dir= puts it, for the default and for a relocated bin dir.
…ames The duplicate check pulled _parse_phone and _same_number out of the vendored imsg. A sync-bridge.sh pull that renamed either one turned every regional duplicate check into "unavailable" with nothing to show for it. imsg now names parse_phone and same_number as the supported entry point for this, so the same rename breaks imsg's own import loudly instead. The matcher itself is unchanged and still shared: copying it would let the regional rules drift from the bridge's, which is the bug that made this check necessary.
70da7fc to
f379e8e
Compare
|
Rebased and out of draft. All three findings are addressed, CI is green, and it is mergeable clean. One thing to see before you merge, since it is scope you did not ask for. Fixing finding 1 meant widening the existing On finding 3 I kept one implementation rather than copying the helpers: |
…d that missed them
WHAT: contact-details.ts and contact-vcard.ts spawned $HOME/bin/contacts
directly instead of shimPath('contacts'), so #100's bin_dir= moved the shims
and left these two features pointing at a path that no longer exists. Both now
resolve through shimPath like every other spawner.
THE GUARD: bin-dir.test.ts's "no spawner hard-codes ~/bin" matched only
double-quoted forms and the tools imsg|contacts, so a single-quoted
join(...,'bin','contacts') sailed past it. Widened to every quote style and all
five shim tools; it fails on the old code and passes on the new.
FOUND BY @Kb2uka while rebasing #91, in two files that PR does not otherwise
touch (386d9d1 and ab3e2ee). Landing it on main on its own, so the fix is not
waiting on a larger feature review.
bun test 592 pass, 0 fail.
LR-T: blip, bin-dir, shim-path, contacts, guard-test
LR-D: blip, collector
LR-K: guard-regex-too-narrow, bin-dir-every-spawner
LR-R: 313326e
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ainpzHJ5F7nsV4hB6HpZy
Four merges plus the bin_dir fix, each with who did it and what was actually verified. Handle lookups run for the two new names: dreinecke and Kb2uka both publish nothing (checked 2026-09-18), recorded as such rather than skipped. Also records what is waiting on people: #103 rebase, #91 write decision, #83 nudged, #25 draft. LR-T: blip, weekly-notes, changelog, contributors LR-D: blip, community, docs LR-K: weekly-notes-written-as-work-lands Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ainpzHJ5F7nsV4hB6HpZy
|
Merged as 347ef54. Thank you @Kb2uka. This one crossed a line we had written down as an invariant, that contact review is read-only and nothing is ever written to Contacts, so I read the write path before putting it to Fred rather than reviewing the description. What decided it:
Fred's call, made on that basis. bun test 617 pass, 11 Mac bridge suites OK including your test_contact_save.py. Not exercised live: creating a real contact, which needs the prompt answered at the Mac. Your three conflicts were your own doing in the best way: the bin_dir regression you found in contact-details.ts and contact-vcard.ts shipped on main first as 4134bbf so it would not wait on this review. I kept the union of the two guard regexes, yours for backticks and mine for the word boundary, and kept shimPath('contacts') whose default home is exactly what your explicit argument passes. Separately: you have no X handle I can verify and we credit contributors by handle in a weekly public update. Reply with one if you want to be tagged, or say you would rather not and we will credit you as Kb2uka. |
Contacts write is now a thing Blip can do, so it says so in the user-facing changelog including what it cannot do (no edit, no delete) and that it fails closed without the grant. LR-T: blip, changelog, weekly-notes, contact-save LR-D: blip, docs LR-K: contacts-write-is-create-only Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ainpzHJ5F7nsV4hB6HpZy
|
Thanks for merging it, and for reading the write path itself rather than the description — that is the review I would want on anything that crosses an invariant, and the five points you landed on are exactly the ones I would have wanted checked. Good call shipping the On the credit: yes, I would like to be tagged. The handle is @StudioXRadio (https://x.com/StudioXRadio). I posted it earlier in this thread on the 18th, so it may just have been buried above the merge. Happy to come back with Undo Send on the terms you set now that #36 is closed. |
WHAT: four stale facts in CLAUDE.md, from Ian Swope.
* The deploy `cp` omitted otp-desktop.py. otp-autofill.ts:22 spawns it from
import.meta.dir, i.e. the INSTALLED plugin dir, so a deploy made from this
recipe leaves security-code autofill with a missing or stale helper and
nothing says so. AGENTS.md:43 and CONTRIBUTING.md:42 already listed it.
* The Linux shim list said four names; blip-setup has installed a fifth,
contact-save, since 39bcd29 (#91).
* blip-dispatch gates six tools, not five (TOOLS, bridge/mac/blip-dispatch:21).
* bun test is 620+ tests in ~1.5 s, not "90+ tests, ~40 ms".
VERIFIED each claim against the code before merging (TOOLS set, the spawn line,
blip-setup's shim loop, the other two recipes).
WHY IT MATTERED HERE, not just in theory: this is the recipe I used on
2026-09-20 to take vic from 2.5.0 to 2.6.1. vic's otp-desktop.py was still the
2.5.0 copy, so #110's Zen/Firefox fallback never reached it. And neither gus nor
vic had ~/bin/contact-save, so Save-as-contact (#91) failed on both. Both fixed
on both hosts alongside this merge.
LR-T: blip, docs, deploy-recipe, otp-autofill, shims, contact-save
LR-D: blip
LR-K: deploy-recipe-must-list-otp-desktop-py, shims-are-copies-refresh-them
LR-R: #111, #110, #91
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XaUhj37aZSBwchzhMSQmnK
…s, with graphic WHAT: docs/x-post-2026-09-26.txt (plain text, 2.6k chars, long post) credits every author of the 35 PRs merged since Post 1 (11 Sep) with their verified X handle, three by GitHub name (joshhattan, cw228, dreinecke: none published). docs/img/x-post-2026-09-26.png (1600x900, rendered from the .html beside it with headless Chromium): chamfered cards, iMessage blue, JetBrainsMono. X-POSTS.md: Post 2 and 3 superseded by 4 (never posted). Kb2uka is @StudioXRadio, which he said on #91 on 18 Sep and nobody recorded. LR-T: blip, x-post, contributors, graphic, release, 2.6.2, StudioXRadio LR-D: blip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeRMdPWNC2myz9U8hEeeCi
Summary
Companion PR: #90. Both target
mainindependently.Splits the contact saving half of #79 for independent review. Adds only
contact-saveto the confined key. Review an unknown sender, enter a name, confirm the fields, and save a new Mac contact. The helper checks exact and regional duplicates and reads the saved card back before reporting success.Includes the formatted-number and native Contacts predicate fallback fixes reported by @ianswope. Contact saving requires Contacts write access and does not require Accessibility or Swift. Setup and recovery after a lost response are documented in
docs/SAVE-CONTACT.md.Review fixes (2026-09-18)
Rebased onto current
main(35 commits). The three findings from @nixfred's review:1.
contact-save.tsbypassedshimPath(). Fixed: it now resolves throughshimPath('contact-save', home)like every other caller, andcontact-savejoins the
ShimToolunion.The guard test that exists for exactly this — "no spawner hard-codes ~/bin" —
did not catch it. Its regex matched only double-quoted
"bin", "imsg"and thetools
imsg/contacts; the regression was written'bin','contact-save'.Widened to every quote style and all five shim tools, it then found the same
regression already on
main, in two files this PR does not otherwise touch:contact-details.ts:24(exact-card details, from 386d9d1)contact-vcard.ts:91(vCard export, from ab3e2ee)Both are fixed here, because leaving them would leave the widened guard red.
Happy to split them into their own PR if you would rather review them apart
from the contact save.
2.
scripts/blip-setupreverted main's install loop. Resolved in therebase itself rather than patched afterwards: main's
mkdir -p "$bin_dir"/target="$bin_dir/$t"loop, thebin_dir/prev_bin_dir/BLIP_BIN_DIRhandling, the header comment and the generated
bridge.confbanner are allmain's;
contact-saveis added to the tool list. Same forREADME.mdanddocs/PRIVACY.md, which keep main'sbin_dir=wording.ci.ymltakes main'spy_compileline withbridge/mac/contact-saveappended — as you noted, if#103 lands first this is the line to take from main again.
3.
phone_matcher()reached into imsg's private names.imsgnow namesparse_phoneandsame_numberas the supported entry point for this, andcontact-saveconsumes those. Async-bridge.shpull that renames eitherprivate function now breaks imsg's own import loudly, instead of silently
turning every regional duplicate check into "unavailable".
I kept one implementation rather than copying the helpers: a copy would let the
regional rules drift from the bridge's own, which is the failure that made this
check necessary in the first place. Say the word if you would rather have the
copy and take the duplication.
Named, as asked, rather than left implicit
contact-save.js:85writes through the deprecatedABAddressBookwhilereading through
CNContactStore. It fails closed ifsharedAddressBookreturns
null, so a macOS that removes the framework degrades to"unavailable" rather than corrupting anything. That is the right failure
direction, and it is still a bet on an API Apple has been retiring for a
decade. If it goes, the create path needs a
CNSaveRequestreplacement.contact-save.js:53: ifadapter.read()throws aftersaveAndReturnErroralready committed, the caller sees{ok:false, code:'unknown'}while the card exists. The code comment says"everything after this point may already have committed" and
docs/SAVE-CONTACT.mdcovers recovery. Worst case is a stray card the userdeletes by hand; there is no automatic retry, by design.
Fail-then-pass
Finding 1,
bun test bin-dir.test.ts contact-save.test.ts:Finding 3,
python3 bridge/mac/test_contact_save.py:Gates after the rebase
bun testexpect()calls, 19 filesbridge/mac/test_contact_save.pybridge/mac/test_phone_match.pybridge/mac/contacts_test.pybridge/mac/test_read_verification.pybridge/mac/test_blip_check.pypy_compilelinebash -n,shellcheck -S warningStill no live Contacts mutation claimed: there is no Mac in this loop and the
create path is unchanged by these fixes.
Proof
Fresh captures of the real QML component on a remote Linux host, with synthetic data and a stub helper. These verify rendering and are not evidence of native Mac mutation.
The native helper carries forward the implementation from #79, with a correction that distinguishes unavailable Contacts storage from denied permission in both inspection and creation. The regression failed before and passed after the correction. Regressions failed before and passed after preventing a busy save from being closed, refreshing candidates on Back, clearing stale errors on Edit, and distinguishing a missing helper or offline bridge from an uncertain save. The form was rendered again after the review fixes. Synthetic regressions cover formatted identities, denied access, exact and regional duplicates, failed native predicates, failed fallback, and saved-card verification. Earlier live card creation and cleanup evidence is recorded in #79; this split does not claim a new live Contacts mutation.
Gates
Below describes the pre-rebase revision (70da7fc). For the current head see
"Gates after the rebase" above: the TypeScript count moved 551 -> 609 with main's
35 commits and the new regression.
551 TypeScript tests and 96 Python tests passed on a remote Linux host. On macOS, 31 Python tests and JavaScript compilation passed. Python compilation, Bash syntax, ShellCheck, and diff whitespace checks passed. Both remote hosts tested the same verified Git bundle. Hosted CI requires maintainer approval.
Review rounds
Stopped at the five round limit. Accepted defects were fixed and covered by regressions. The final two comments were checked against the actual permission guidance and the passing native fixture tests; no open implementation findings remain after arbitration. The first attempt from Seat A in round 2 returned no verdict and was rerun successfully.
Needs a human
Maintainer review of the contact creation and confirmation flow, plus approval for the fork workflow. Closing the containing window can lose an in-flight save result; check Contacts before trying again, as documented in
docs/SAVE-CONTACT.md. The form itself prevents closing or going back while a save is busy. No merge or deployment is included.factory: rounds=5 evidence=A,B,D