I think you may have hit the central weakness of the current design.

and as a temporary replacement for the missing set map, I sometimes also run a script to create a menu of all existing links in the Hook database.

This one is in JavaScript for Automation, and can be run from something like Keyboard Maestro, or from Script Editor with the language tab at top left set to `JavaScript`

##
JS Script - menu of all links (by name)

```
(() => {
'use strict';
ObjC.import('sqlite3');
// HOOK: SLIGHTLY FULLER EXAMPLE OF TABLE-READING
// FROM JAVASCRIPT FOR AUTOMATION
// (Menu shows labels, follows links)
// Rob Trew 2019
const main = () => {
const multipleSelections = true;
return sj(either(
msg => msg,
xs => {
const
links = nubBy(
a => b => snd(a) === snd(b),
xs
),
addrs = map(fst, links),
labels = map(snd, links),
sa = standardSEAdditions();
return bindLR(
showMenuLR(true, 'Hook links', labels),
choices => map(
x => {
const
strAddr = addrs[elemIndex(x, labels).Just],
strURL = strAddr.startsWith('/') ? (
'file://' + strAddr + '/' + x
) : strAddr;
return (
sa.activate(),
sa.openLocation(strURL),
strURL
);
},
choices
)
);
},
linksFromHooKDBPathLR(
'~/Library/Application Support/' +
'com.cogsciapps.hook/hook.sqlite'
)
));
};
// HOOK.APP - SIMPLEST LISTING OF LINKS VIA SQLITE
// linkAndLabelFromMeta :: String -> [String]
const linkAndLabelFromMeta = s => {
const xs = s.split('$$$');
return 1 < xs.length ? (
[xs[0], base64decode(xs[1])]
) : [s, ''];
};
// linksFromHooKDBPathLR :: FilePath -> Either String [String]
const linksFromHooKDBPathLR = strDBPath => {
const
SQLITE_OK = parseInt($.SQLITE_OK, 10),
SQLITE_ROW = parseInt($.SQLITE_ROW, 10),
ppDb = Ref(),
strSQL =
'SELECT srcMetaString, destMetaString, ' +
'COALESCE(path, "") as folder, ' +
'COALESCE(name, "") as fileName ' +
'FROM link l LEFT JOIN fileinfo f ' +
'ON l.dest=f.fileid ' +
'ORDER by src',
colText = curry($.sqlite3_column_text);
return bindLR(
bindLR(
SQLITE_OK !== $.sqlite3_open(filePath(strDBPath), ppDb) ? (
Left($.sqlite3_errmsg(fst(ppDb)))
) : Right(fst(ppDb)),
db => {
const ppStmt = Ref();
return SQLITE_OK !== $.sqlite3_prepare_v2(
db, strSQL, -1, ppStmt, Ref()
) ? (
Left($.sqlite3_errmsg(db))
) : Right(Tuple3(
db,
fst(ppStmt),
enumFromTo(
0,
$.sqlite3_column_count(ppStmt[0]) - 1
)
));
}
),
// (Link, labe) from all available rows in the table:
tpl => Right(
sortBy(mappendComparing([snd]),
concatMap(
x => {
const [from, to] = map(
linkAndLabelFromMeta,
x.slice(0, 1)
).concat([x.slice(2)]);
return (0 < (
fst(to).length + snd(to).length)) ? (
[from, to]
) : [from];
},
unfoldr(
stmt => SQLITE_ROW !== $.sqlite3_step(stmt) ? (
$.sqlite3_finalize(stmt),
$.sqlite3_close(fst(tpl)),
Nothing()
) : Just(
Tuple(
map(colText(stmt), tpl[2]),
stmt
)
),
snd(tpl)
)
)
)
)
);
};
// JXA ------------------------------------------------
// base64decode :: String -> String
const base64decode = s =>
ObjC.unwrap(
$.NSString.alloc.initWithDataEncoding(
$.NSData.alloc.initWithBase64EncodedStringOptions(
s, 0
),
$.NSUTF8StringEncoding
)
);
// showMenuLR :: Bool -> String -> [String] -> Either String [String]
const showMenuLR = (blnMult, title, xs) =>
0 < xs.length ? (() => {
const sa = standardSEAdditions();
sa.activate();
const v = sa.chooseFromList(xs, {
withTitle: title,
withPrompt: 'Select' + (
blnMult ? ' one or more of ' +
xs.length.toString() : ':'
),
defaultItems: xs[0],
okButtonName: 'OK',
cancelButtonName: 'Cancel',
multipleSelectionsAllowed: blnMult,
emptySelectionAllowed: false
});
return Array.isArray(v) ? (
Right(v)
) : Left('User cancelled ' + title + ' menu.');
})() : Left(title + ': No items to choose from.');
// standardSEAdditions :: () -> Application
const standardSEAdditions = () =>
Object.assign(Application('System Events'), {
includeStandardAdditions: true
});
// GENERIC FUNCTIONS ----------------------------------
// https://github.com/RobTrew/prelude-jxa
// Just :: a -> Maybe a
const Just = x => ({
type: 'Maybe',
Nothing: false,
Just: x
});
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Nothing :: Maybe a
const Nothing = () => ({
type: 'Maybe',
Nothing: true,
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = (a, b) => ({
type: 'Tuple',
'0': a,
'1': b,
length: 2
});
// Tuple3 (,,) :: a -> b -> c -> (a, b, c)
const Tuple3 = (a, b, c) => ({
type: 'Tuple3',
'0': a,
'1': b,
'2': c,
length: 3
});
// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
const bindLR = (m, mf) =>
undefined !== m.Left ? (
m
) : mf(m.Right);
// compare :: a -> a -> Ordering
const compare = (a, b) =>
a < b ? -1 : (a > b ? 1 : 0);
// comparing :: (a -> b) -> (a -> a -> Ordering)
const comparing = f =>
(x, y) => {
const
a = f(x),
b = f(y);
return a < b ? -1 : (a > b ? 1 : 0);
};
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) =>
xs.flatMap(f);
// curry :: ((a, b) -> c) -> a -> b -> c
const curry = f => a => b => f(a, b);
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = (fl, fr, e) =>
'Either' === e.type ? (
undefined !== e.Left ? (
fl(e.Left)
) : fr(e.Right)
) : undefined;
// elemIndex :: Eq a => a -> [a] -> Maybe Int
const elemIndex = (x, xs) => {
const i = xs.indexOf(x);
return -1 === i ? (
Nothing()
) : Just(i);
};
// enumFromTo :: Int -> Int -> [Int]
const enumFromTo = (m, n) =>
Array.from({
length: 1 + n - m
}, (_, i) => m + i);
// filePath :: String -> FilePath
const filePath = s =>
ObjC.unwrap(ObjC.wrap(s)
.stringByStandardizingPath);
// fst :: (a, b) -> a
const fst = tpl => tpl[0];
// identity :: a -> a
const identity = x => x;
// intercalate :: String -> [String] -> String
const intercalate = s => xs =>
xs.join(s);
// mappendComparing :: [(a -> b)] -> (a -> a -> Ordering)
const mappendComparing = fs =>
(x, y) => fs.reduce(
(ordr, f) => (ordr || compare(f(x), f(y))),
0
);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) =>
(Array.isArray(xs) ? (
xs
) : xs.split('')).map(f);
// Default value (v) if m.Nothing, or f(m.Just)
// maybe :: b -> (a -> b) -> Maybe a -> b
const maybe = v => f => m =>
m.Nothing ? v : f(m.Just);
// nubBy :: (a -> a -> Bool) -> [a] -> [a]
const nubBy = (fEq, xs) => {
const go = xs => 0 < xs.length ? (() => {
const x = xs[0];
return [x].concat(
go(xs.slice(1)
.filter(y => !fEq(x)(y))
)
)
})() : [];
return go(xs);
};
// showJSON :: a -> String
const sj = x => JSON.stringify(x, null, 2);
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// snd :: (a, b) -> b
const snd = tpl => tpl[1];
// sortBy :: (a -> a -> Ordering) -> [a] -> [a]
const sortBy = (f, xs) =>
xs.slice()
.sort(f);
// unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
const unfoldr = (f, v) => {
let
xr = [v, v],
xs = [];
while (true) {
const mb = f(xr[1]);
if (mb.Nothing) {
return xs
} else {
xr = mb.Just;
xs.push(xr[0])
}
}
};
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = p => f => x => {
let v = x;
while (!p(v)) v = f(v);
return v;
};
// MAIN ---
return main();
})();
```