@@ -13,13 +13,22 @@ const GetRecordByIdentifierSchema = z
1313 uuid : z . string ( ) . optional ( ) . describe ( "UUID of the record" ) ,
1414 id : z . number ( ) . optional ( ) . describe ( "ID of the record (requires databaseName)" ) ,
1515 databaseName : z . string ( ) . optional ( ) . describe ( "Database name (required with id)" ) ,
16+ referenceURL : z
17+ . string ( )
18+ . optional ( )
19+ . describe (
20+ "A x-devonthink-item:// URL. Works for all record types including imported emails which use non-UUID identifiers." ,
21+ ) ,
1622 } )
1723 . strict ( )
1824 . refine (
1925 ( data ) =>
20- data . uuid !== undefined || ( data . id !== undefined && data . databaseName !== undefined ) ,
26+ data . referenceURL !== undefined ||
27+ data . uuid !== undefined ||
28+ ( data . id !== undefined && data . databaseName !== undefined ) ,
2129 {
22- message : "Either UUID alone, or ID with databaseName must be provided" ,
30+ message :
31+ "Either referenceURL alone, UUID alone, or ID with databaseName must be provided" ,
2332 } ,
2433 ) ;
2534
@@ -37,6 +46,7 @@ interface RecordResult {
3746 recordType : string ;
3847 kind : string ;
3948 database : string ;
49+ referenceURL : string ;
4050 creationDate ?: string ;
4151 modificationDate ?: string ;
4252 tags ?: string [ ] ;
@@ -47,7 +57,7 @@ interface RecordResult {
4757}
4858
4959const getRecordByIdentifier = async ( input : GetRecordByIdentifierInput ) : Promise < RecordResult > => {
50- const { uuid, id, databaseName } = input ;
60+ const { uuid, id, databaseName, referenceURL } = input ;
5161
5262 // Validate string inputs
5363 if ( uuid && ! isJXASafeString ( uuid ) ) {
@@ -56,90 +66,155 @@ const getRecordByIdentifier = async (input: GetRecordByIdentifierInput): Promise
5666 if ( databaseName && ! isJXASafeString ( databaseName ) ) {
5767 return { success : false , error : "Database name contains invalid characters" } ;
5868 }
69+ if ( referenceURL && ! isJXASafeString ( referenceURL ) ) {
70+ return { success : false , error : "Reference URL contains invalid characters" } ;
71+ }
72+ if ( id !== undefined && typeof id !== "number" ) {
73+ return { success : false , error : "ID must be a number" } ;
74+ }
5975
6076 const script = `
6177 (() => {
6278 const theApp = Application("DEVONthink");
6379 theApp.includeStandardAdditions = true;
64-
80+
6581 // Inject helper functions
6682 ${ getRecordLookupHelpers ( ) }
6783 ${ getDatabaseHelper }
68-
84+
6985 try {
7086 let targetRecord;
7187 let targetDatabase;
7288 let lookupResult;
73-
74- if (${ uuid ? `"${ escapeStringForJXA ( uuid ) } "` : "null" } ) {
89+
90+ if (${ referenceURL ? `"${ escapeStringForJXA ( referenceURL ) } "` : "null" } ) {
91+ // Reference URL lookup (x-devonthink-item:// URLs)
92+ const refURL = ${ referenceURL ? `"${ escapeStringForJXA ( referenceURL ) } "` : "null" } ;
93+ const prefix = "x-devonthink-item://";
94+
95+ // Extract the identifier part after the prefix
96+ const identifier = refURL.startsWith(prefix) ? refURL.substring(prefix.length) : refURL;
97+
98+ // Check if it looks like a UUID (hex digits and hyphens)
99+ const uuidPattern = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
100+
101+ if (uuidPattern.test(identifier)) {
102+ // Fast path: standard UUID format
103+ targetRecord = theApp.getRecordWithUuid(identifier.toUpperCase());
104+ }
105+
106+ if (!targetRecord) {
107+ // Non-UUID format (e.g. imported emails with message-ID-based UUIDs).
108+ // DEVONthink stores the URL-decoded identifier as the record's UUID,
109+ // so decode and try a direct UUID lookup first.
110+ try {
111+ const decoded = decodeURIComponent(identifier);
112+ if (decoded !== identifier) {
113+ targetRecord = theApp.getRecordWithUuid(decoded);
114+ }
115+ } catch (e) {
116+ // Invalid percent-encoding — skip
117+ }
118+ }
119+
120+ if (!targetRecord) {
121+ // Fall back to lookupRecordsWithURL across all open databases.
122+ const databases = theApp.databases();
123+ for (let i = 0; i < databases.length; i++) {
124+ const db = databases[i];
125+ const results = theApp.lookupRecordsWithURL(refURL, { "in": db });
126+ if (results && results.length > 0) {
127+ // lookupRecordsWithURL matches the url property;
128+ // verify by checking referenceURL
129+ for (let j = 0; j < results.length; j++) {
130+ if (results[j].referenceURL() === refURL) {
131+ targetRecord = results[j];
132+ break;
133+ }
134+ }
135+ if (targetRecord) break;
136+ }
137+ }
138+ }
139+
140+ if (!targetRecord) {
141+ return JSON.stringify({
142+ success: false,
143+ error: "Record not found for reference URL: " + refURL
144+ });
145+ }
146+
147+ targetDatabase = targetRecord.database();
148+
149+ } else if (${ uuid ? `"${ escapeStringForJXA ( uuid ) } "` : "null" } ) {
75150 // UUID lookup - globally unique
76- const lookupOptions = {
77- uuid: ${ uuid ? `"${ escapeStringForJXA ( uuid ) } "` : "null" }
78- };
79-
151+ const lookupOptions = {};
152+ lookupOptions["uuid"] = ${ uuid ? `"${ escapeStringForJXA ( uuid ) } "` : "null" } ;
153+
80154 lookupResult = getRecord(theApp, lookupOptions);
81-
155+
82156 if (!lookupResult.record) {
83157 return JSON.stringify({
84158 success: false,
85159 error: "Record with UUID " + (${ uuid ? `"${ escapeStringForJXA ( uuid ) } "` : "null" } || "unknown") + " not found"
86160 });
87161 }
88-
162+
89163 targetRecord = lookupResult.record;
90164 // Get the database of the record
91165 targetDatabase = targetRecord.database();
92-
93- } else if (${ id !== undefined ? id : " null" } && ${ databaseName ? `"${ escapeStringForJXA ( databaseName ) } "` : "null" } ) {
166+
167+ } else if (${ formatValueForJXA ( id ) } !== null && ${ databaseName ? `"${ escapeStringForJXA ( databaseName ) } "` : "null" } ) {
94168 // ID + Database lookup
95169 targetDatabase = getDatabase(theApp, ${ databaseName ? `"${ escapeStringForJXA ( databaseName ) } "` : "null" } );
96-
97- const lookupOptions = {
98- id: ${ id } ,
99- database: targetDatabase
100- };
101-
170+
171+ const lookupOptions = {};
172+ lookupOptions["id"] = ${ formatValueForJXA ( id ) } ;
173+ lookupOptions["database"] = targetDatabase;
174+
102175 lookupResult = getRecord(theApp, lookupOptions);
103-
176+
104177 if (!lookupResult.record) {
105178 return JSON.stringify({
106179 success: false,
107- error: "Record with ID " + ${ id } + " not found in database '" + (${ databaseName ? `"${ escapeStringForJXA ( databaseName ) } "` : "null" } || "unknown") + "'"
180+ error: "Record with ID " + ${ formatValueForJXA ( id ) } + " not found in database '" + (${ databaseName ? `"${ escapeStringForJXA ( databaseName ) } "` : "null" } || "unknown") + "'"
108181 });
109182 }
110-
183+
111184 targetRecord = lookupResult.record;
112185 }
113-
186+
114187 // Extract record properties
115- const record = {
116- id: targetRecord.id(),
117- uuid: targetRecord.uuid(),
118- name: targetRecord.name(),
119- path: targetRecord.path(),
120- location: targetRecord.location(),
121- recordType: targetRecord.recordType(),
122- kind: targetRecord.kind(),
123- database: targetDatabase.name(),
124- creationDate: targetRecord.creationDate() ? targetRecord.creationDate().toString() : null,
125- modificationDate: targetRecord.modificationDate () ? targetRecord.modificationDate ().toString() : null,
126- tags: targetRecord.tags(),
127- size: targetRecord.size()
128- } ;
129-
188+ const record = {};
189+ record["id"] = targetRecord.id();
190+ record[" uuid"] = targetRecord.uuid();
191+ record[" name"] = targetRecord.name();
192+ record[" path"] = targetRecord.path();
193+ record[" location"] = targetRecord.location();
194+ record[" recordType"] = targetRecord.recordType();
195+ record[" kind"] = targetRecord.kind();
196+ record[" database"] = targetDatabase.name();
197+ record["referenceURL"] = targetRecord.referenceURL();
198+ record["creationDate"] = targetRecord.creationDate () ? targetRecord.creationDate ().toString() : null;
199+ record["modificationDate"] = targetRecord.modificationDate() ? targetRecord.modificationDate().toString() : null;
200+ record["tags"] = targetRecord.tags();
201+ record["size"] = targetRecord.size() ;
202+
130203 // Add optional properties if available
131- if (targetRecord.url && targetRecord.url()) {
132- record.url = targetRecord.url();
133- }
134- if (targetRecord.comment && targetRecord.comment()) {
135- record.comment = targetRecord.comment();
136- }
137-
204+ try {
205+ const recordUrl = targetRecord.url();
206+ if (recordUrl) record["url"] = recordUrl;
207+ } catch (e) {}
208+ try {
209+ const recordComment = targetRecord.comment();
210+ if (recordComment) record["comment"] = recordComment;
211+ } catch (e) {}
212+
138213 return JSON.stringify({
139214 success: true,
140215 record: record
141216 });
142-
217+
143218 } catch (error) {
144219 return JSON.stringify({
145220 success: false,
@@ -155,7 +230,7 @@ const getRecordByIdentifier = async (input: GetRecordByIdentifierInput): Promise
155230export const getRecordByIdentifierTool : Tool = {
156231 name : "get_record_by_identifier" ,
157232 description :
158- 'Get a DEVONthink record using its UUID or ID. \n\nExample (UUID):\n{\n "uuid": "1234-5678-90AB-CDEF"\n}\n\nExample (ID):\n{\n "id": 12345,\n "databaseName": "MyDatabase"\n}' ,
233+ 'Get a DEVONthink record using its UUID, ID, or x-devonthink-item:// reference URL.\n\nExample (Reference URL):\n{\n "referenceURL": "x-devonthink-item://1234-5678-90AB-CDEF"\n}\n\nExample (Reference URL - email):\n{\n "referenceURL": "x-devonthink-item://message:%3Cfoo@bar.com%3E"\n} \n\nExample (UUID):\n{\n "uuid": "1234-5678-90AB-CDEF"\n}\n\nExample (ID):\n{\n "id": 12345,\n "databaseName": "MyDatabase"\n}' ,
159234 inputSchema : zodToJsonSchema ( GetRecordByIdentifierSchema ) as ToolInput ,
160235 run : getRecordByIdentifier ,
161236} ;
0 commit comments