Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"plugins": [
{
"name": "apple-notes",
"version": "2.6.10",
"version": "2.6.11",
"source": {
"source": "local",
"path": "./codex"
Expand Down
2 changes: 1 addition & 1 deletion .antigravity-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"plugins": [
{
"name": "apple-notes",
"version": "2.6.10",
"version": "2.6.11",
"description": "Manage Apple Notes through natural language - create, search, read, update, delete, organize folders, inspect metadata, run diagnostics, and work with attachments (macOS only).",
"source": {
"source": "local",
Expand Down
2 changes: 1 addition & 1 deletion .antigravity-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apple-notes",
"version": "2.6.10",
"version": "2.6.11",
"description": "Manage Apple Notes through natural language - create, search, read, update, delete, organize folders, inspect metadata, run diagnostics, and work with attachments (macOS only).",
"author": {
"name": "Rob Sweet",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{
"name": "apple-notes",
"description": "Manage Apple Notes through natural language - create, search, read, update, delete, and organize notes and folders",
"version": "2.6.10",
"version": "2.6.11",
"author": {
"name": "Rob Sweet",
"email": "rob@superiortech.io"
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apple-notes",
"version": "2.6.10",
"version": "2.6.11",
"description": "Manage Apple Notes through natural language - create, search, read, update, delete, and organize notes and folders (macOS only)",
"author": {
"name": "Rob Sweet",
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- **`.hermes-plugin/` packaging docs** (`README.md`, `config.yaml`). Hermes Agent has no plugin/marketplace drop-in, so a directory of manifest-looking files was easy to misread as an installable package. The setup it documented is not lost — the `hermes mcp add` command, the `~/.hermes/config.yaml` `mcp_servers:` snippet, and the restart note now live inline in the README's "Other Hosts" section. Matches apple-mail-mcp#116, keeping multi-host packaging parity across the four Apple MCP servers. No effect on the published package: `.hermes-plugin/` was never in `package.json` `files[]`.

### Fixed
- **`search-notes` now says it searched titles only when a title search returns nothing.** `searchContent` defaults to `false`, and the two modes are exclusive: `searchNotes` builds the AppleScript `whose` clause as either `name contains` or `body contains`, never both. So the natural first call, `search-notes({query: "<term>"})`, returned a bare `{"notes":[],"count":0}` for a term appearing in dozens of note *bodies*, with nothing in the response saying bodies were never read. That is a silent false negative, the worst failure mode for a search tool: it does not error, it confidently reports absence, and a caller reasonably concludes the note does not exist. The empty result now carries a hint that only titles were searched and that `searchContent: true` searches bodies instead. Scoped to the empty result, so a successful search is byte-identical and no extra AppleScript work is done; the default stays `false` and no search semantics change. The tool description already mentioned `searchContent`, but a description is not in front of the caller at the moment it is interpreting an empty result, and that is where the wrong conclusion gets drawn. `CLAUDE.md`'s `search-notes` guidance is corrected in the same change: it read "search note body, not just titles", which describes an additive search and so stated the opposite of the exclusive behaviour, in the file agents read before they call the tool. `README.md`'s parameter table was already accurate.
- **`version-guard` no longer demands a version bump for byte-neutral `src/` changes.** The shipped-bytes detector treated every non-test file under `src/` as shipped, but TypeScript there reaches users only after esbuild inlines it into `build/index.js` — so a comment-, formatting- or type-only edit that leaves the committed bundle byte-identical was hard-blocked, leaving only two bad options: publish a release of literally nothing, or do not write the comment. `src/**/*.ts` is now a first-cause detector that implies a bump only when `build/**` changed too. The exemption is sound rather than merely convenient: ci.yml's "Verify committed build/ matches source" step rebuilds and requires `git diff --quiet build/`, and it runs in the `test` job whose `test (22)`/`test (24)` contexts are required by branch protection — so at merge time an unchanged `build/` provably matches `src/`. Everything else under `src/` (the verbatim-shipped `*_reader.py` sidecars), `requirements.txt` and `build/**` stay unconditional detectors, and the rule is written fail-safe: only `.ts` counts as bundle-only, so any new file type under `src/` still requires a bump.
- **Dependabot auto-bump silently stopped staging its own changes.** `dependabot-rebuild.yml`'s bump step writes the patch version, syncs the plugin manifests and prepends a CHANGELOG entry, then staged them with `git add package.json CHANGELOG.md build .claude-plugin .agents codex .hermes-plugin .antigravity-plugin`. Once `.hermes-plugin/` was removed that pathspec matched nothing, and `git add` is all-or-nothing — it exited 128 and staged **none** of the others, with `2>/dev/null || true` hiding the failure. The following step re-adds only `build/`, so a Dependabot PR would have committed a rebuilt bundle with no version bump and no changelog entry, failing `require-version-bump` and blocking the automation that is meant to run without a human. Dropped the stale path, and dropped the error suppression so a future missing path fails loudly instead of silently skipping the bump.
- **`pnpm version` no longer breaks with the `.hermes-plugin/` removal.** The `version` lifecycle script listed `.hermes-plugin` in its `git add`; `git add` exits 128 on a pathspec that matches nothing, which would have broken the documented release step (`pnpm version <patch|minor|major> --no-git-tag-version`) for every subsequent release. The stale path is dropped from the `git add` list.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ This works in: `create-note` (folder param), `search-notes`, `list-notes`, `move
`list-folders` returns full hierarchical paths, so duplicate folder names (e.g., multiple "Archive" folders) are disambiguated.

### search-notes
- Set `searchContent: true` to search note body, not just titles
- Set `searchContent: true` to search note bodies **instead of** titles, not in addition to them. The two modes are exclusive, so no single call matches titles or bodies. A title-only search that finds nothing says so in the response; treat that as "no title matched", not "no such note exists", and retry with `searchContent: true`.
- Searches are case-insensitive
- Results include note IDs for reliable subsequent operations
- Use `modifiedSince` (ISO 8601 date) to filter to recently modified notes — useful for large collections
Expand Down
11 changes: 10 additions & 1 deletion build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41871,6 +41871,14 @@ function describeSearchLimit(effectiveLimit, wasDefault, resultCount) {
return { info, truncationNote };
}

// src/utils/searchScope.ts
function describeSearchScope(searchContent, resultCount) {
if (searchContent || resultCount > 0) {
return "";
}
return "\n\n\u2139\uFE0F Only note titles were searched, so a term that appears in note bodies would not match. Retry with `searchContent: true` to search bodies instead.";
}

// src/tools/doctor.ts
import { spawnSync } from "child_process";
function runDoctor(manager) {
Expand Down Expand Up @@ -42191,8 +42199,9 @@ server.registerTool(

${syncWarnings.join(" ")}` : "";
if (notes.length === 0) {
const scopeHint = describeSearchScope(searchContent, notes.length);
return successResponse(
`No notes found matching "${query}" in ${searchType}${folderInfo}${dateInfo}${syncNote}`,
`No notes found matching "${query}" in ${searchType}${folderInfo}${dateInfo}${scopeHint}${syncNote}`,
{ notes: [], count: 0 }
);
}
Expand Down
2 changes: 1 addition & 1 deletion codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apple-notes",
"version": "2.6.10",
"version": "2.6.11",
"description": "Manage Apple Notes through natural language - create, search, read, update, delete, organize folders, inspect metadata, run diagnostics, and work with attachments (macOS only).",
"author": {
"name": "Rob Sweet",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apple-notes-mcp",
"version": "2.6.10",
"version": "2.6.11",
"packageManager": "pnpm@11.9.0",
"description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
"type": "module",
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { parseHashtags } from "@/utils/hashtags.js";
import { stripLargeInlineImages, strippedImagesWarning } from "@/utils/inlineImages.js";
import { resolveUpdateResponseTitle } from "@/utils/updateResponseTitle.js";
import { resolveSearchLimit, describeSearchLimit } from "@/utils/searchLimit.js";
import { describeSearchScope } from "@/utils/searchScope.js";
import { runDoctor, formatDoctorReport } from "@/tools/doctor.js";
import { FULL_DISK_ACCESS_GUIDE_URL } from "@/utils/docsUrls.js";
import { loadFileConfig } from "@/services/fileConfig.js";
Expand Down Expand Up @@ -297,8 +298,12 @@ server.registerTool(
const syncNote = syncWarnings.length > 0 ? `\n\n${syncWarnings.join(" ")}` : "";

if (notes.length === 0) {
// Disclose a title-only search on the empty result: bodies were never read, so a
// bare `{"notes":[],"count":0}` reads as "no such note exists" for a term that may
// appear in dozens of note bodies.
const scopeHint = describeSearchScope(searchContent, notes.length);
return successResponse(
`No notes found matching "${query}" in ${searchType}${folderInfo}${dateInfo}${syncNote}`,
`No notes found matching "${query}" in ${searchType}${folderInfo}${dateInfo}${scopeHint}${syncNote}`,
{ notes: [], count: 0 }
);
}
Expand Down
20 changes: 20 additions & 0 deletions src/utils/searchScope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, it, expect } from "vitest";
import { describeSearchScope } from "@/utils/searchScope.js";

describe("describeSearchScope", () => {
it("discloses the title-only scope when a title search returns nothing", () => {
const hint = describeSearchScope(false, 0);
expect(hint).toContain("Only note titles were searched");
expect(hint).toContain("`searchContent: true`");
});

it("stays silent when a title search found matches", () => {
expect(describeSearchScope(false, 1)).toBe("");
expect(describeSearchScope(false, 50)).toBe("");
});

it("stays silent when the caller already searched bodies", () => {
expect(describeSearchScope(true, 0)).toBe("");
expect(describeSearchScope(true, 3)).toBe("");
});
});
29 changes: 29 additions & 0 deletions src/utils/searchScope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Build the advisory fragment that discloses a title-only search on an empty result.
*
* `search-notes` matches note titles unless the caller passes `searchContent: true`,
* and the two modes are exclusive: the AppleScript `whose` clause is built as either
* `name contains` or `body contains`, never both. A title-only search that matches
* nothing therefore returns `{"notes":[],"count":0}` for a term that may appear in
* dozens of note bodies, and nothing in that response says bodies were never read.
* The obvious reading is "no such note exists", which is a silent false negative: the
* tool does not error, it confidently reports absence.
*
* The hint fires only on the empty result, so an ordinary successful search is
* unchanged and no extra AppleScript work is done. Search behaviour is untouched;
* this is disclosure only, mirroring how the applied result cap is disclosed by
* `describeSearchLimit` in ./searchLimit.ts.
*
* @param searchContent - true when the caller asked for a body search
* @param resultCount - number of notes returned after dedup
* @returns a trailing hint, empty unless a title-only search returned nothing
*/
export function describeSearchScope(searchContent: boolean, resultCount: number): string {
if (searchContent || resultCount > 0) {
return "";
}
return (
"\n\nℹ️ Only note titles were searched, so a term that appears in note bodies " +
"would not match. Retry with `searchContent: true` to search bodies instead."
);
}