@tekmidian/devonthink-mcp 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/columnLayout.d.ts +20 -24
- package/dist/tools/columnLayout.js +284 -191
- package/dist/tools/columnLayout.js.map +1 -1
- package/dist/tools/listSmartGroups.d.ts +28 -4
- package/dist/tools/listSmartGroups.js +150 -56
- package/dist/tools/listSmartGroups.js.map +1 -1
- package/dist/tools/listSmartRules.d.ts +9 -6
- package/dist/tools/listSmartRules.js +64 -62
- package/dist/tools/listSmartRules.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,29 +2,24 @@
|
|
|
2
2
|
* columnLayout.ts — Read and copy DEVONthink column layouts for smart groups/rules.
|
|
3
3
|
*
|
|
4
4
|
* Column layouts for list views are stored in
|
|
5
|
-
* ~/Library/Preferences/com.devon-technologies.think.plist under
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* ~/Library/Preferences/com.devon-technologies.think.plist under three related keys
|
|
6
|
+
* per smart group or rule (keyed by name or UUID):
|
|
7
|
+
*
|
|
8
|
+
* ListColumnsHorizontal-{name}
|
|
9
|
+
* → Array of visible column identifiers in display order
|
|
10
|
+
*
|
|
11
|
+
* TableView Columns ListColumnsHorizontal-{name}
|
|
12
|
+
* → Array of ALL column identifiers (visible + hidden)
|
|
13
|
+
*
|
|
14
|
+
* TableView Column Widths ListColumnsHorizontal-{name}
|
|
15
|
+
* → Dict of { columnIdentifier: widthFloat }
|
|
16
|
+
*
|
|
17
|
+
* The plist may use a smart group's UUID instead of its display name.
|
|
8
18
|
*
|
|
9
19
|
* Tools exported:
|
|
10
20
|
* get_column_layout — Read columns for a named smart group or rule
|
|
11
21
|
* copy_column_layout — Copy column layout from one smart group/rule to another
|
|
12
22
|
*/
|
|
13
|
-
interface ColumnEntry {
|
|
14
|
-
identifier: string | null;
|
|
15
|
-
title: string | null;
|
|
16
|
-
width: number | null;
|
|
17
|
-
visible: boolean | null;
|
|
18
|
-
position: number | null;
|
|
19
|
-
}
|
|
20
|
-
interface GetColumnLayoutResult {
|
|
21
|
-
success: boolean;
|
|
22
|
-
name?: string;
|
|
23
|
-
plistKey?: string;
|
|
24
|
-
columns?: ColumnEntry[];
|
|
25
|
-
rawValue?: string;
|
|
26
|
-
error?: string;
|
|
27
|
-
}
|
|
28
23
|
export declare const getColumnLayoutTool: {
|
|
29
24
|
name: string;
|
|
30
25
|
description: string;
|
|
@@ -35,21 +30,22 @@ export declare const getColumnLayoutTool: {
|
|
|
35
30
|
type: string;
|
|
36
31
|
description: string;
|
|
37
32
|
};
|
|
38
|
-
|
|
33
|
+
uuid: {
|
|
39
34
|
type: string;
|
|
40
35
|
description: string;
|
|
41
36
|
};
|
|
42
37
|
};
|
|
43
38
|
required: string[];
|
|
44
39
|
};
|
|
45
|
-
run: (args: Record<string, unknown>) => Promise<
|
|
40
|
+
run: (args: Record<string, unknown>) => Promise<unknown>;
|
|
46
41
|
};
|
|
47
42
|
interface CopyColumnLayoutResult {
|
|
48
43
|
success: boolean;
|
|
49
44
|
sourceName?: string;
|
|
50
45
|
targetName?: string;
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
resolvedSourceKey?: string;
|
|
47
|
+
resolvedTargetKey?: string;
|
|
48
|
+
keysCopied?: string[];
|
|
53
49
|
message?: string;
|
|
54
50
|
error?: string;
|
|
55
51
|
}
|
|
@@ -67,11 +63,11 @@ export declare const copyColumnLayoutTool: {
|
|
|
67
63
|
type: string;
|
|
68
64
|
description: string;
|
|
69
65
|
};
|
|
70
|
-
|
|
66
|
+
sourceUuid: {
|
|
71
67
|
type: string;
|
|
72
68
|
description: string;
|
|
73
69
|
};
|
|
74
|
-
|
|
70
|
+
targetUuid: {
|
|
75
71
|
type: string;
|
|
76
72
|
description: string;
|
|
77
73
|
};
|
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
* columnLayout.ts — Read and copy DEVONthink column layouts for smart groups/rules.
|
|
3
3
|
*
|
|
4
4
|
* Column layouts for list views are stored in
|
|
5
|
-
* ~/Library/Preferences/com.devon-technologies.think.plist under
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* ~/Library/Preferences/com.devon-technologies.think.plist under three related keys
|
|
6
|
+
* per smart group or rule (keyed by name or UUID):
|
|
7
|
+
*
|
|
8
|
+
* ListColumnsHorizontal-{name}
|
|
9
|
+
* → Array of visible column identifiers in display order
|
|
10
|
+
*
|
|
11
|
+
* TableView Columns ListColumnsHorizontal-{name}
|
|
12
|
+
* → Array of ALL column identifiers (visible + hidden)
|
|
13
|
+
*
|
|
14
|
+
* TableView Column Widths ListColumnsHorizontal-{name}
|
|
15
|
+
* → Dict of { columnIdentifier: widthFloat }
|
|
16
|
+
*
|
|
17
|
+
* The plist may use a smart group's UUID instead of its display name.
|
|
8
18
|
*
|
|
9
19
|
* Tools exported:
|
|
10
20
|
* get_column_layout — Read columns for a named smart group or rule
|
|
@@ -24,7 +34,7 @@ const PLISTBUDDY = "/usr/libexec/PlistBuddy";
|
|
|
24
34
|
*/
|
|
25
35
|
function plistRead(command) {
|
|
26
36
|
try {
|
|
27
|
-
const result = execSync(`${PLISTBUDDY} -c
|
|
37
|
+
const result = execSync(`${PLISTBUDDY} -c ${shellQuote(command)} ${shellQuote(PLIST_PATH)}`, {
|
|
28
38
|
encoding: "utf-8",
|
|
29
39
|
timeout: 10000,
|
|
30
40
|
});
|
|
@@ -35,84 +45,185 @@ function plistRead(command) {
|
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
/**
|
|
38
|
-
*
|
|
39
|
-
|
|
48
|
+
* Shell-quote a string by wrapping in single quotes and escaping embedded single quotes.
|
|
49
|
+
*/
|
|
50
|
+
function shellQuote(s) {
|
|
51
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the three plist keys for a given base name (name or UUID).
|
|
55
|
+
*/
|
|
56
|
+
function keysForName(name) {
|
|
57
|
+
return {
|
|
58
|
+
columnsKey: `ListColumnsHorizontal-${name}`,
|
|
59
|
+
tableViewColumnsKey: `TableView Columns ListColumnsHorizontal-${name}`,
|
|
60
|
+
tableViewWidthsKey: `TableView Column Widths ListColumnsHorizontal-${name}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse a PlistBuddy "Array { ... }" output into a string array.
|
|
65
|
+
*/
|
|
66
|
+
function parseArray(raw) {
|
|
67
|
+
const lines = raw
|
|
68
|
+
.split("\n")
|
|
69
|
+
.map((l) => l.trim())
|
|
70
|
+
.filter((l) => l !== "" && l !== "Array {" && l !== "}");
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse a PlistBuddy "Dict { key = value ... }" output into a Record<string, number>.
|
|
75
|
+
*/
|
|
76
|
+
function parseWidthDict(raw) {
|
|
77
|
+
const result = {};
|
|
78
|
+
const lines = raw
|
|
79
|
+
.split("\n")
|
|
80
|
+
.map((l) => l.trim())
|
|
81
|
+
.filter((l) => l !== "" && l !== "Dict {" && l !== "}");
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
const match = line.match(/^(\S+)\s*=\s*(.+)$/);
|
|
84
|
+
if (match) {
|
|
85
|
+
result[match[1]] = parseFloat(match[2]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get all distinct base names that exist in the plist for ListColumnsHorizontal entries.
|
|
92
|
+
* Returns up to `limit` names for error messages.
|
|
40
93
|
*/
|
|
41
|
-
function
|
|
94
|
+
function getExistingNames(limit = 20) {
|
|
95
|
+
// Use Python to read the plist keys — more reliable for binary plists
|
|
96
|
+
const script = `
|
|
97
|
+
import plistlib, re, sys
|
|
98
|
+
with open('${PLIST_PATH}', 'rb') as f:
|
|
99
|
+
data = plistlib.load(f)
|
|
100
|
+
prefix = 'ListColumnsHorizontal-'
|
|
101
|
+
names = set()
|
|
102
|
+
for k in data:
|
|
103
|
+
if k.startswith(prefix):
|
|
104
|
+
names.add(k[len(prefix):])
|
|
105
|
+
elif k.startswith('TableView Columns ' + prefix):
|
|
106
|
+
names.add(k[len('TableView Columns ' + prefix):])
|
|
107
|
+
elif k.startswith('TableView Column Widths ' + prefix):
|
|
108
|
+
names.add(k[len('TableView Column Widths ' + prefix):])
|
|
109
|
+
# Exclude Outline variants
|
|
110
|
+
names = [n for n in names if not n.startswith('Outline')]
|
|
111
|
+
names.sort()
|
|
112
|
+
print('\\n'.join(names[:${limit}]))
|
|
113
|
+
`.trim();
|
|
42
114
|
try {
|
|
43
|
-
execSync(
|
|
115
|
+
const result = execSync(`python3 -c ${shellQuote(script)}`, {
|
|
44
116
|
encoding: "utf-8",
|
|
45
117
|
timeout: 10000,
|
|
46
118
|
});
|
|
47
|
-
return
|
|
119
|
+
return result.trim().split("\n").filter(Boolean);
|
|
48
120
|
}
|
|
49
121
|
catch {
|
|
50
|
-
return
|
|
122
|
+
return [];
|
|
51
123
|
}
|
|
52
124
|
}
|
|
53
125
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* must be escaped.
|
|
126
|
+
* Fuzzy-search existing names in the plist for a partial match.
|
|
127
|
+
* Returns all names that include the search term (case-insensitive).
|
|
57
128
|
*/
|
|
58
|
-
function
|
|
59
|
-
|
|
129
|
+
function findMatchingNames(searchTerm) {
|
|
130
|
+
const allNames = getExistingNames(200);
|
|
131
|
+
const lower = searchTerm.toLowerCase();
|
|
132
|
+
return allNames.filter((n) => n.toLowerCase().includes(lower));
|
|
60
133
|
}
|
|
61
134
|
/**
|
|
62
|
-
*
|
|
135
|
+
* Look up column layout data for a single base name (exact match).
|
|
136
|
+
* Returns null if none of the three key variants exist.
|
|
63
137
|
*/
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
138
|
+
function readLayoutForName(baseName) {
|
|
139
|
+
const { columnsKey, tableViewColumnsKey, tableViewWidthsKey } = keysForName(baseName);
|
|
140
|
+
const rawColumns = plistRead(`Print ':${columnsKey}'`);
|
|
141
|
+
const rawTableViewColumns = plistRead(`Print ':${tableViewColumnsKey}'`);
|
|
142
|
+
const rawWidths = plistRead(`Print ':${tableViewWidthsKey}'`);
|
|
143
|
+
// If none of the three keys exist, this name has no layout stored
|
|
144
|
+
if (!rawColumns && !rawTableViewColumns && !rawWidths) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const keysFound = [];
|
|
148
|
+
if (rawColumns)
|
|
149
|
+
keysFound.push(columnsKey);
|
|
150
|
+
if (rawTableViewColumns)
|
|
151
|
+
keysFound.push(tableViewColumnsKey);
|
|
152
|
+
if (rawWidths)
|
|
153
|
+
keysFound.push(tableViewWidthsKey);
|
|
154
|
+
return {
|
|
155
|
+
found: true,
|
|
156
|
+
name: baseName,
|
|
157
|
+
resolvedKey: baseName,
|
|
158
|
+
columns: rawColumns ? parseArray(rawColumns) : null,
|
|
159
|
+
tableViewColumns: rawTableViewColumns ? parseArray(rawTableViewColumns) : null,
|
|
160
|
+
widths: rawWidths ? parseWidthDict(rawWidths) : null,
|
|
161
|
+
keysFound,
|
|
162
|
+
};
|
|
71
163
|
}
|
|
72
164
|
const getColumnLayout = async (args) => {
|
|
73
|
-
const { name,
|
|
165
|
+
const { name, uuid } = args;
|
|
74
166
|
if (!name || typeof name !== "string") {
|
|
75
167
|
return { success: false, error: "name parameter is required" };
|
|
76
168
|
}
|
|
77
|
-
// Try
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for (const key of keysToTry) {
|
|
90
|
-
const raw = plistRead(`Print '${key}'`);
|
|
91
|
-
if (raw) {
|
|
169
|
+
// Try exact name first
|
|
170
|
+
const exactResult = readLayoutForName(name);
|
|
171
|
+
if (exactResult) {
|
|
172
|
+
return {
|
|
173
|
+
success: true,
|
|
174
|
+
...exactResult,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// If a UUID was supplied, try that too
|
|
178
|
+
if (uuid && typeof uuid === "string") {
|
|
179
|
+
const uuidResult = readLayoutForName(uuid);
|
|
180
|
+
if (uuidResult) {
|
|
92
181
|
return {
|
|
93
182
|
success: true,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
rawValue: raw,
|
|
97
|
-
columns: [],
|
|
183
|
+
...uuidResult,
|
|
184
|
+
name, // Override with the human-readable name
|
|
98
185
|
};
|
|
99
186
|
}
|
|
100
187
|
}
|
|
101
|
-
//
|
|
102
|
-
const
|
|
188
|
+
// Try fuzzy match against existing names
|
|
189
|
+
const fuzzyMatches = findMatchingNames(name);
|
|
190
|
+
if (fuzzyMatches.length === 1) {
|
|
191
|
+
// Single match — use it
|
|
192
|
+
const fuzzyResult = readLayoutForName(fuzzyMatches[0]);
|
|
193
|
+
if (fuzzyResult) {
|
|
194
|
+
return {
|
|
195
|
+
success: true,
|
|
196
|
+
...fuzzyResult,
|
|
197
|
+
nameSearched: name,
|
|
198
|
+
note: `Exact name not found; matched "${fuzzyMatches[0]}" via partial search`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Not found — return helpful error with examples
|
|
203
|
+
const examples = getExistingNames(15);
|
|
204
|
+
if (fuzzyMatches.length > 1) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
name,
|
|
208
|
+
error: `No exact match for "${name}". Multiple partial matches found — be more specific.`,
|
|
209
|
+
partialMatches: fuzzyMatches,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
103
212
|
return {
|
|
104
213
|
success: false,
|
|
105
214
|
name,
|
|
106
|
-
error: `
|
|
107
|
-
"
|
|
108
|
-
|
|
215
|
+
error: `No column layout found for "${name}". ` +
|
|
216
|
+
"This smart group may not have a custom layout yet (it will use defaults). " +
|
|
217
|
+
"Use copy_column_layout to copy an existing layout to it.",
|
|
218
|
+
exampleNames: examples,
|
|
109
219
|
};
|
|
110
220
|
};
|
|
111
221
|
export const getColumnLayoutTool = {
|
|
112
222
|
name: "get_column_layout",
|
|
113
223
|
description: "Read the column layout for a DEVONthink smart group or smart rule from preferences. " +
|
|
114
|
-
"Returns the
|
|
115
|
-
|
|
224
|
+
"Returns the ordered visible columns, all table view columns, and column widths. " +
|
|
225
|
+
"Looks up by name (or UUID). Supports partial name matching. " +
|
|
226
|
+
'Input: { "name": "Archivieren - Jobs" } or { "name": "Jobs", "uuid": "4A469368-..." }',
|
|
116
227
|
inputSchema: {
|
|
117
228
|
type: "object",
|
|
118
229
|
properties: {
|
|
@@ -120,202 +231,184 @@ export const getColumnLayoutTool = {
|
|
|
120
231
|
type: "string",
|
|
121
232
|
description: "Name of the smart group or smart rule whose column layout to read",
|
|
122
233
|
},
|
|
123
|
-
|
|
234
|
+
uuid: {
|
|
124
235
|
type: "string",
|
|
125
|
-
description: "Optional
|
|
126
|
-
"
|
|
236
|
+
description: "Optional UUID of the smart group (fallback if name not found). " +
|
|
237
|
+
"DEVONthink sometimes stores layouts under the UUID rather than the display name.",
|
|
127
238
|
},
|
|
128
239
|
},
|
|
129
240
|
required: ["name"],
|
|
130
241
|
},
|
|
131
242
|
run: getColumnLayout,
|
|
132
243
|
};
|
|
244
|
+
/**
|
|
245
|
+
* Use Python plistlib to copy all three column layout keys from source to target.
|
|
246
|
+
* This handles nested structures (Array/Dict) correctly in a single atomic read/write.
|
|
247
|
+
*/
|
|
248
|
+
function copyLayoutViaPython(sourceBaseName, targetBaseName) {
|
|
249
|
+
const suffixes = [
|
|
250
|
+
"ListColumnsHorizontal",
|
|
251
|
+
"TableView Columns ListColumnsHorizontal",
|
|
252
|
+
"TableView Column Widths ListColumnsHorizontal",
|
|
253
|
+
];
|
|
254
|
+
const script = `
|
|
255
|
+
import plistlib, sys, os
|
|
256
|
+
|
|
257
|
+
plist_path = '${PLIST_PATH}'
|
|
258
|
+
source = '${sourceBaseName.replace(/'/g, "\\'")}'
|
|
259
|
+
target = '${targetBaseName.replace(/'/g, "\\'")}'
|
|
260
|
+
|
|
261
|
+
suffixes = [
|
|
262
|
+
'ListColumnsHorizontal',
|
|
263
|
+
'TableView Columns ListColumnsHorizontal',
|
|
264
|
+
'TableView Column Widths ListColumnsHorizontal',
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
with open(plist_path, 'rb') as f:
|
|
268
|
+
data = plistlib.load(f)
|
|
269
|
+
|
|
270
|
+
copied = []
|
|
271
|
+
for suffix in suffixes:
|
|
272
|
+
src_key = suffix + '-' + source
|
|
273
|
+
tgt_key = suffix + '-' + target
|
|
274
|
+
if src_key in data:
|
|
275
|
+
import copy
|
|
276
|
+
data[tgt_key] = copy.deepcopy(data[src_key])
|
|
277
|
+
copied.append(tgt_key)
|
|
278
|
+
|
|
279
|
+
if not copied:
|
|
280
|
+
print('ERROR: no source keys found for: ' + source, file=sys.stderr)
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
with open(plist_path, 'wb') as f:
|
|
284
|
+
plistlib.dump(data, f, fmt=plistlib.FMT_BINARY)
|
|
285
|
+
|
|
286
|
+
print('\\n'.join(copied))
|
|
287
|
+
`.trim();
|
|
288
|
+
try {
|
|
289
|
+
const result = execSync(`python3 -c ${shellQuote(script)}`, {
|
|
290
|
+
encoding: "utf-8",
|
|
291
|
+
timeout: 15000,
|
|
292
|
+
});
|
|
293
|
+
const keysCopied = result.trim().split("\n").filter(Boolean);
|
|
294
|
+
return { ok: true, keysCopied };
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
298
|
+
return { ok: false, keysCopied: [], error: errMsg };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
133
301
|
const copyColumnLayout = async (args) => {
|
|
134
|
-
const { sourceName, targetName,
|
|
302
|
+
const { sourceName, targetName, sourceUuid, targetUuid } = args;
|
|
135
303
|
if (!sourceName || typeof sourceName !== "string") {
|
|
136
304
|
return { success: false, error: "sourceName parameter is required" };
|
|
137
305
|
}
|
|
138
306
|
if (!targetName || typeof targetName !== "string") {
|
|
139
307
|
return { success: false, error: "targetName parameter is required" };
|
|
140
308
|
}
|
|
141
|
-
// Resolve source
|
|
142
|
-
const escapedSource = escapePBKey(sourceName);
|
|
143
|
-
const sourceKeysToTry = sourcePlistKey
|
|
144
|
-
? [sourcePlistKey]
|
|
145
|
-
: [
|
|
146
|
-
`Columns:${escapedSource}`,
|
|
147
|
-
`NSTableView Columns ${escapedSource}`,
|
|
148
|
-
`ViewState:${escapedSource}:Columns`,
|
|
149
|
-
`SmartGroupColumns:${escapedSource}`,
|
|
150
|
-
`ColumnState:${escapedSource}`,
|
|
151
|
-
];
|
|
309
|
+
// Resolve source: try exact name, then UUID, then fuzzy
|
|
152
310
|
let resolvedSourceKey = null;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const raw = plistRead(`Print '${key}'`);
|
|
156
|
-
if (raw) {
|
|
157
|
-
resolvedSourceKey = key;
|
|
158
|
-
sourceValue = raw;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
311
|
+
if (readLayoutForName(sourceName)) {
|
|
312
|
+
resolvedSourceKey = sourceName;
|
|
161
313
|
}
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
success: false,
|
|
165
|
-
sourceName,
|
|
166
|
-
targetName,
|
|
167
|
-
error: `Source column layout for "${sourceName}" not found. Try specifying sourcePlistKey.`,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
// Resolve target key (same pattern, replacing source name with target name)
|
|
171
|
-
const escapedTarget = escapePBKey(targetName);
|
|
172
|
-
let resolvedTargetKey;
|
|
173
|
-
if (targetPlistKey) {
|
|
174
|
-
resolvedTargetKey = targetPlistKey;
|
|
314
|
+
else if (sourceUuid && readLayoutForName(sourceUuid)) {
|
|
315
|
+
resolvedSourceKey = sourceUuid;
|
|
175
316
|
}
|
|
176
317
|
else {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (existingTarget) {
|
|
183
|
-
const deleted = plistWrite(`Delete '${resolvedTargetKey}'`);
|
|
184
|
-
if (!deleted) {
|
|
318
|
+
const fuzzy = findMatchingNames(sourceName);
|
|
319
|
+
if (fuzzy.length === 1 && readLayoutForName(fuzzy[0])) {
|
|
320
|
+
resolvedSourceKey = fuzzy[0];
|
|
321
|
+
}
|
|
322
|
+
else if (fuzzy.length > 1) {
|
|
185
323
|
return {
|
|
186
324
|
success: false,
|
|
187
325
|
sourceName,
|
|
188
326
|
targetName,
|
|
189
|
-
|
|
190
|
-
targetPlistKey: resolvedTargetKey,
|
|
191
|
-
error: `Could not delete existing target key: ${resolvedTargetKey}`,
|
|
327
|
+
error: `Ambiguous source name "${sourceName}". Multiple matches: ${fuzzy.slice(0, 8).join(", ")}`,
|
|
192
328
|
};
|
|
193
329
|
}
|
|
194
330
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// We use a temp file approach with plutil + PlistBuddy.
|
|
198
|
-
const os = await import("node:os");
|
|
199
|
-
const fs = await import("node:fs");
|
|
200
|
-
const tmpFile = join(os.tmpdir(), `dt-column-copy-${Date.now()}.plist`);
|
|
201
|
-
try {
|
|
202
|
-
// Export current plist to XML
|
|
203
|
-
execSync(`plutil -convert xml1 -o "${tmpFile}" "${PLIST_PATH}"`, {
|
|
204
|
-
encoding: "utf-8",
|
|
205
|
-
timeout: 10000,
|
|
206
|
-
});
|
|
207
|
-
// Read the source value from the xml copy (same key)
|
|
208
|
-
const sourceFromTmp = plistRead(`Print '${resolvedSourceKey}'`);
|
|
209
|
-
if (!sourceFromTmp) {
|
|
210
|
-
fs.unlinkSync(tmpFile);
|
|
211
|
-
return {
|
|
212
|
-
success: false,
|
|
213
|
-
error: "Could not re-read source value after XML conversion",
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
// The safest way to copy is to use PlistBuddy on the tmp file to
|
|
217
|
-
// get the XML fragment, then use Add command on the original.
|
|
218
|
-
// However, PlistBuddy cannot directly export a subtree as XML.
|
|
219
|
-
// Instead we detect the type and use a simpler approach:
|
|
220
|
-
// read individual leaf values and reconstruct with Add commands.
|
|
221
|
-
//
|
|
222
|
-
// For now, return what we found and indicate manual steps needed for
|
|
223
|
-
// complex nested structures. If the source is a simple string:
|
|
224
|
-
const trimmed = sourceValue.trim();
|
|
225
|
-
const isSimpleValue = !trimmed.startsWith("Dict") && !trimmed.startsWith("Array");
|
|
226
|
-
if (isSimpleValue) {
|
|
227
|
-
// Determine plist type and add
|
|
228
|
-
const numVal = Number(trimmed);
|
|
229
|
-
if (!isNaN(numVal) && trimmed !== "") {
|
|
230
|
-
plistWrite(`Add '${resolvedTargetKey}' integer ${trimmed}`);
|
|
231
|
-
}
|
|
232
|
-
else if (trimmed === "true" || trimmed === "false") {
|
|
233
|
-
plistWrite(`Add '${resolvedTargetKey}' bool ${trimmed}`);
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
const escaped = trimmed.replace(/'/g, "'\\''");
|
|
237
|
-
plistWrite(`Add '${resolvedTargetKey}' string '${escaped}'`);
|
|
238
|
-
}
|
|
239
|
-
// Verify
|
|
240
|
-
const written = plistRead(`Print '${resolvedTargetKey}'`);
|
|
241
|
-
fs.unlinkSync(tmpFile);
|
|
242
|
-
if (written) {
|
|
243
|
-
return {
|
|
244
|
-
success: true,
|
|
245
|
-
sourceName,
|
|
246
|
-
targetName,
|
|
247
|
-
sourcePlistKey: resolvedSourceKey,
|
|
248
|
-
targetPlistKey: resolvedTargetKey,
|
|
249
|
-
message: `Column layout copied from "${sourceName}" to "${targetName}". Value: ${written}`,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
return {
|
|
254
|
-
success: false,
|
|
255
|
-
error: "Write appeared to succeed but key is not readable after write",
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
// Complex nested structure — return info so user can inspect and act
|
|
260
|
-
fs.unlinkSync(tmpFile);
|
|
331
|
+
if (!resolvedSourceKey) {
|
|
332
|
+
const examples = getExistingNames(10);
|
|
261
333
|
return {
|
|
262
334
|
success: false,
|
|
263
335
|
sourceName,
|
|
264
336
|
targetName,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
"Automated copy of nested plist structures requires iterating each child key. " +
|
|
269
|
-
`Source value:\n${sourceValue}\n\n` +
|
|
270
|
-
"Use get_column_layout to inspect the structure, then use shell PlistBuddy " +
|
|
271
|
-
"commands to copy individual entries manually.",
|
|
337
|
+
error: `Source column layout for "${sourceName}" not found. ` +
|
|
338
|
+
"This smart group may not have a custom layout saved yet. " +
|
|
339
|
+
`Known layouts include: ${examples.slice(0, 8).join(", ")}`,
|
|
272
340
|
};
|
|
273
341
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
catch {
|
|
281
|
-
// ignore cleanup error
|
|
282
|
-
}
|
|
342
|
+
// Resolve target base name: prefer UUID if supplied and different from name,
|
|
343
|
+
// otherwise use the display name (DEVONthink will save new layouts by name).
|
|
344
|
+
const resolvedTargetKey = targetUuid ?? targetName;
|
|
345
|
+
// Perform the copy via Python
|
|
346
|
+
const copyResult = copyLayoutViaPython(resolvedSourceKey, resolvedTargetKey);
|
|
347
|
+
if (!copyResult.ok) {
|
|
283
348
|
return {
|
|
284
349
|
success: false,
|
|
285
|
-
|
|
350
|
+
sourceName,
|
|
351
|
+
targetName,
|
|
352
|
+
resolvedSourceKey,
|
|
353
|
+
resolvedTargetKey,
|
|
354
|
+
error: `Copy failed: ${copyResult.error}`,
|
|
286
355
|
};
|
|
287
356
|
}
|
|
357
|
+
// Verify by reading back the target
|
|
358
|
+
const verification = readLayoutForName(resolvedTargetKey);
|
|
359
|
+
if (!verification) {
|
|
360
|
+
return {
|
|
361
|
+
success: false,
|
|
362
|
+
sourceName,
|
|
363
|
+
targetName,
|
|
364
|
+
resolvedSourceKey,
|
|
365
|
+
resolvedTargetKey,
|
|
366
|
+
error: "Copy appeared to succeed but target keys not readable after write",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
success: true,
|
|
371
|
+
sourceName,
|
|
372
|
+
targetName,
|
|
373
|
+
resolvedSourceKey,
|
|
374
|
+
resolvedTargetKey,
|
|
375
|
+
keysCopied: copyResult.keysCopied,
|
|
376
|
+
message: `Copied column layout from "${sourceName}" to "${targetName}". ` +
|
|
377
|
+
`Keys written: ${copyResult.keysCopied.join(", ")}. ` +
|
|
378
|
+
`Columns: [${verification.columns?.join(", ") ?? "n/a"}]. ` +
|
|
379
|
+
"Restart DEVONthink or close/reopen the smart group window for the change to take effect.",
|
|
380
|
+
};
|
|
288
381
|
};
|
|
289
382
|
export const copyColumnLayoutTool = {
|
|
290
383
|
name: "copy_column_layout",
|
|
291
|
-
description: "Copy the column layout
|
|
292
|
-
"
|
|
293
|
-
"
|
|
294
|
-
"
|
|
295
|
-
'Input: { "sourceName": "Jobs - To Review", "targetName": "Jobs - Archive" }',
|
|
384
|
+
description: "Copy the column layout (column order, visible columns, and column widths) from one " +
|
|
385
|
+
"DEVONthink smart group or smart rule to another. " +
|
|
386
|
+
"All three layout keys are copied atomically. Supports partial name matching. " +
|
|
387
|
+
'Input: { "sourceName": "Archivieren - Jobs", "targetName": "Jobs - To Review" }',
|
|
296
388
|
inputSchema: {
|
|
297
389
|
type: "object",
|
|
298
390
|
properties: {
|
|
299
391
|
sourceName: {
|
|
300
392
|
type: "string",
|
|
301
|
-
description: "Name of the source smart group or smart rule",
|
|
393
|
+
description: "Name of the source smart group or smart rule (must have a saved layout)",
|
|
302
394
|
},
|
|
303
395
|
targetName: {
|
|
304
396
|
type: "string",
|
|
305
397
|
description: "Name of the target smart group or smart rule to copy the layout to",
|
|
306
398
|
},
|
|
307
|
-
|
|
399
|
+
sourceUuid: {
|
|
308
400
|
type: "string",
|
|
309
|
-
description: "Optional
|
|
401
|
+
description: "Optional UUID of the source smart group (fallback if name lookup fails). " +
|
|
402
|
+
"DEVONthink sometimes stores layouts under the UUID.",
|
|
310
403
|
},
|
|
311
|
-
|
|
404
|
+
targetUuid: {
|
|
312
405
|
type: "string",
|
|
313
|
-
description: "Optional
|
|
406
|
+
description: "Optional UUID of the target smart group. If supplied, the layout is written " +
|
|
407
|
+
"under the UUID key (which DEVONthink prefers for smart groups).",
|
|
314
408
|
},
|
|
315
409
|
},
|
|
316
410
|
required: ["sourceName", "targetName"],
|
|
317
411
|
},
|
|
318
412
|
run: copyColumnLayout,
|
|
319
413
|
};
|
|
320
|
-
void readCount; // suppress unused warning — kept for future use
|
|
321
414
|
//# sourceMappingURL=columnLayout.js.map
|