yoke-mcp 0.1.0

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/doctor.js ADDED
@@ -0,0 +1,178 @@
1
+ // Checks every link in the chain, and says which one is broken.
2
+ //
3
+ // There are four things between a tool call and a tab, and any of them can be
4
+ // the reason nothing works: the build, the host registration, Chrome having
5
+ // spawned the host, and the extension answering. A single "not reachable" tells
6
+ // you none of that, so this reports each link separately and names the next
7
+ // action rather than leaving it to be guessed.
8
+ import { existsSync, readFileSync, statSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { browserDirs, EXTENSION_ID, HOST_NAME } from './install.js';
12
+ import { endpointPath } from './socket-path.js';
13
+ import { ask } from './socket-client.js';
14
+ const extensionRoot = () => join(dirname(fileURLToPath(import.meta.url)), '..');
15
+ /** Is the compiled host actually on disk, and executable by Chrome? */
16
+ function checkBuild() {
17
+ const host = join(dirname(fileURLToPath(import.meta.url)), 'native-host.js');
18
+ if (!existsSync(host)) {
19
+ return { ok: false, label: 'build', detail: `${host} is missing`, fix: 'npm run build' };
20
+ }
21
+ // Chrome executes this path directly, so a missing execute bit is fatal and
22
+ // silent: Chrome simply never starts the host.
23
+ const executable = (statSync(host).mode & 0o111) !== 0;
24
+ return executable
25
+ ? { ok: true, label: 'build', detail: 'the host is built and executable' }
26
+ : { ok: false, label: 'build', detail: 'the host is not executable', fix: `chmod +x ${host}` };
27
+ }
28
+ /**
29
+ * Is the host registered, and does the registration agree with this build?
30
+ *
31
+ * Two ways this goes wrong quietly: the manifest points at a path that no longer
32
+ * exists, or it allowlists a different extension id, in which case Chrome
33
+ * refuses the connection without telling anybody why.
34
+ */
35
+ function checkRegistration() {
36
+ const dirs = browserDirs();
37
+ if (dirs.length === 0) {
38
+ return {
39
+ ok: true,
40
+ label: 'registration',
41
+ detail: 'this platform registers the host in the registry, which is not checked here',
42
+ };
43
+ }
44
+ const found = [];
45
+ const problems = [];
46
+ for (const [browser, dir] of dirs) {
47
+ const file = join(dir, `${HOST_NAME}.json`);
48
+ if (!existsSync(file)) {
49
+ continue;
50
+ }
51
+ let manifest;
52
+ try {
53
+ manifest = JSON.parse(readFileSync(file, 'utf8'));
54
+ }
55
+ catch {
56
+ problems.push(`${browser}: the manifest is not valid JSON`);
57
+ continue;
58
+ }
59
+ if (!existsSync(manifest.path)) {
60
+ problems.push(`${browser}: points at ${manifest.path}, which does not exist`);
61
+ continue;
62
+ }
63
+ // The launcher names an absolute interpreter precisely so Chrome does not
64
+ // need a PATH. If that interpreter has since moved, Chrome fails with
65
+ // nothing but "Native host has exited", so it is checked here instead.
66
+ const launcher = readFileSync(manifest.path, 'utf8');
67
+ const interpreter = /exec "([^"]+)"|"([^"]+)" "/.exec(launcher);
68
+ const node = interpreter?.[1] ?? interpreter?.[2];
69
+ if (node && !existsSync(node)) {
70
+ problems.push(`${browser}: its launcher runs ${node}, which no longer exists`);
71
+ continue;
72
+ }
73
+ if (!manifest.allowed_origins.includes(`chrome-extension://${EXTENSION_ID}/`)) {
74
+ problems.push(`${browser}: allowlists ${manifest.allowed_origins.join(', ')}, not ${EXTENSION_ID}`);
75
+ continue;
76
+ }
77
+ found.push(browser);
78
+ }
79
+ if (problems.length > 0) {
80
+ return { ok: false, label: 'registration', detail: problems.join('; '), fix: 'yoke install' };
81
+ }
82
+ if (found.length === 0) {
83
+ return {
84
+ ok: false,
85
+ label: 'registration',
86
+ detail: 'no browser has the host registered',
87
+ fix: 'yoke install',
88
+ };
89
+ }
90
+ return { ok: true, label: 'registration', detail: `registered for ${found.join(', ')}` };
91
+ }
92
+ /**
93
+ * Has Chrome started the host?
94
+ *
95
+ * The socket exists only while the host runs, and Chrome only runs it while the
96
+ * extension holds the port open. So an absent socket almost always means the
97
+ * extension is not loaded or its service worker is asleep.
98
+ */
99
+ function checkSocket() {
100
+ const socket = endpointPath();
101
+ if (process.platform === 'win32') {
102
+ return { ok: true, label: 'host running', detail: 'named pipes cannot be probed by existence; the ping below is the real check' };
103
+ }
104
+ return existsSync(socket)
105
+ ? { ok: true, label: 'host running', detail: socket }
106
+ : {
107
+ ok: false,
108
+ label: 'host running',
109
+ detail: `no socket at ${socket}`,
110
+ fix: `load ${join(extensionRoot(), 'extension')} at chrome://extensions with Developer mode on`,
111
+ };
112
+ }
113
+ /** Does the extension itself answer? */
114
+ async function checkPing() {
115
+ try {
116
+ const { extension } = await ask('ping', {}, { timeoutMs: 3_000 });
117
+ return { ok: true, label: 'extension', detail: `answered, version ${extension}` };
118
+ }
119
+ catch (failure) {
120
+ return {
121
+ ok: false,
122
+ label: 'extension',
123
+ detail: failure instanceof Error ? failure.message : String(failure),
124
+ fix: 'open chrome://extensions and check the service worker is running',
125
+ };
126
+ }
127
+ }
128
+ /**
129
+ * The acceptance test for the whole project: every tab, not a group's worth.
130
+ *
131
+ * Reported as a count and as how many sit outside any group, because the second
132
+ * number is the one a tab-group-scoped bridge cannot see at all.
133
+ */
134
+ async function checkTabs() {
135
+ try {
136
+ const { tabs } = await ask('listTabs', {}, { timeoutMs: 5_000 });
137
+ const ungrouped = tabs.filter((tab) => tab.groupId === -1).length;
138
+ return {
139
+ ok: tabs.length > 0,
140
+ label: 'tabs visible',
141
+ detail: `${tabs.length} tab(s), ${ungrouped} of them in no tab group`,
142
+ };
143
+ }
144
+ catch (failure) {
145
+ return {
146
+ ok: false,
147
+ label: 'tabs visible',
148
+ detail: failure instanceof Error ? failure.message : String(failure),
149
+ };
150
+ }
151
+ }
152
+ /** Runs the checks in order, stopping the remote ones once a local one fails. */
153
+ export async function doctor() {
154
+ const checks = [checkBuild(), checkRegistration(), checkSocket()];
155
+ // No point asking the extension anything when the socket is not even there:
156
+ // the answer would be the same timeout dressed up as two failures.
157
+ if (checks.every((check) => check.ok)) {
158
+ checks.push(await checkPing());
159
+ if (checks[checks.length - 1]?.ok) {
160
+ checks.push(await checkTabs());
161
+ }
162
+ }
163
+ return checks;
164
+ }
165
+ export function render(checks) {
166
+ const lines = checks.map((check) => {
167
+ const mark = check.ok ? 'ok ' : 'FAIL';
168
+ const fix = check.ok || !check.fix ? '' : `\n fix: ${check.fix}`;
169
+ return `${mark} ${check.label.padEnd(14)} ${check.detail}${fix}`;
170
+ });
171
+ const broken = checks.find((check) => !check.ok);
172
+ lines.push('');
173
+ lines.push(broken
174
+ ? `Not working yet. The first thing to fix is "${broken.label}".`
175
+ : 'Working. Every link in the chain answered.');
176
+ return `${lines.join('\n')}\n`;
177
+ }
178
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ export declare const HOST_NAME = "io.github.hamzahamidi.yoke";
2
+ /**
3
+ * Derived from the public key pinned in the extension's manifest. Chrome computes
4
+ * it the same way, so a mismatch means the extension was built with a different
5
+ * key and native messaging will refuse to connect.
6
+ */
7
+ export declare const EXTENSION_ID = "oceljemfocgfidhhdlbojkbkmlbfclna";
8
+ export interface HostManifest {
9
+ name: string;
10
+ description: string;
11
+ path: string;
12
+ type: 'stdio';
13
+ allowed_origins: string[];
14
+ }
15
+ export interface InstallResult {
16
+ written: Array<[browser: string, file: string]>;
17
+ skipped: Array<[browser: string, reason: string]>;
18
+ extensionId: string;
19
+ hostName: string;
20
+ platform: NodeJS.Platform;
21
+ }
22
+ /**
23
+ * Where each browser looks for host manifests.
24
+ *
25
+ * Per-user directories only. A system-wide install needs root and would register
26
+ * the host for every account on the machine, which is not something a command
27
+ * line should arrange on someone's behalf.
28
+ */
29
+ export declare function browserDirs(platform?: NodeJS.Platform, home?: string): Array<[browser: string, dir: string]>;
30
+ export declare const manifestFor: (hostPath: string) => HostManifest;
31
+ /**
32
+ * Writes the manifest into every browser directory whose parent already exists.
33
+ *
34
+ * Only where the browser is actually installed: creating a Brave directory on a
35
+ * machine with no Brave leaves litter for something that is not there.
36
+ */
37
+ export declare function install({ hostPath, platform, home, }: {
38
+ hostPath: string;
39
+ platform?: NodeJS.Platform;
40
+ home?: string;
41
+ }): InstallResult;
42
+ export declare function uninstall({ platform, home, }?: {
43
+ platform?: NodeJS.Platform;
44
+ home?: string;
45
+ }): {
46
+ removed: Array<[string, string]>;
47
+ };
@@ -0,0 +1,165 @@
1
+ // Registering the native messaging host with the browsers on this machine.
2
+ //
3
+ // Chrome will only launch a host declared in a manifest at a fixed path, and will
4
+ // only connect an extension whose id appears in that manifest's allowed_origins.
5
+ // Both halves are why this exists: the id is pinned by the key in the
6
+ // extension's manifest, and this manifest has to be written into each browser's
7
+ // own directory.
8
+ import { chmodSync, existsSync, mkdirSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { dirname, join } from 'node:path';
11
+ // Reverse DNS on the GitHub namespace rather than a domain, because that is the
12
+ // ownership actually demonstrable here. Chrome reads this string by exact match
13
+ // from a file on the user's disk, so changing it breaks every existing install.
14
+ export const HOST_NAME = 'io.github.hamzahamidi.yoke';
15
+ /**
16
+ * Derived from the public key pinned in the extension's manifest. Chrome computes
17
+ * it the same way, so a mismatch means the extension was built with a different
18
+ * key and native messaging will refuse to connect.
19
+ */
20
+ export const EXTENSION_ID = 'oceljemfocgfidhhdlbojkbkmlbfclna';
21
+ /**
22
+ * Where each browser looks for host manifests.
23
+ *
24
+ * Per-user directories only. A system-wide install needs root and would register
25
+ * the host for every account on the machine, which is not something a command
26
+ * line should arrange on someone's behalf.
27
+ */
28
+ export function browserDirs(platform = process.platform, home = homedir()) {
29
+ if (platform === 'darwin') {
30
+ const support = join(home, 'Library', 'Application Support');
31
+ return [
32
+ ['Chrome', join(support, 'Google', 'Chrome', 'NativeMessagingHosts')],
33
+ ['Chrome Beta', join(support, 'Google', 'Chrome Beta', 'NativeMessagingHosts')],
34
+ ['Chromium', join(support, 'Chromium', 'NativeMessagingHosts')],
35
+ ['Edge', join(support, 'Microsoft Edge', 'NativeMessagingHosts')],
36
+ ['Brave', join(support, 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts')],
37
+ ];
38
+ }
39
+ if (platform === 'win32') {
40
+ // Windows declares this in the registry rather than on disk, so the caller
41
+ // is told what to do instead of being silently skipped.
42
+ return [];
43
+ }
44
+ const config = process.env['XDG_CONFIG_HOME'] ?? join(home, '.config');
45
+ return [
46
+ ['Chrome', join(config, 'google-chrome', 'NativeMessagingHosts')],
47
+ ['Chromium', join(config, 'chromium', 'NativeMessagingHosts')],
48
+ ['Edge', join(config, 'microsoft-edge', 'NativeMessagingHosts')],
49
+ ['Brave', join(config, 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts')],
50
+ ];
51
+ }
52
+ /**
53
+ * Writes a launcher that runs the host with an absolute node path.
54
+ *
55
+ * Chrome executes the manifest's `path` directly, and a Chrome started from the
56
+ * Dock or Finder inherits a minimal PATH: on macOS typically
57
+ * /usr/bin:/bin:/usr/sbin:/sbin. A `#!/usr/bin/env node` shebang therefore fails
58
+ * to resolve for anyone whose node came from Homebrew, nvm, asdf or Volta, which
59
+ * is nearly everyone, and the failure surfaces only as "Native host has exited".
60
+ * Baking in the interpreter that is running this install removes the guess.
61
+ */
62
+ /**
63
+ * A node path that survives a node upgrade, where one can be proven identical.
64
+ *
65
+ * process.execPath is exact but versioned: under Homebrew it reads
66
+ * /opt/homebrew/Cellar/node/26.5.1/bin/node, a path `brew upgrade node` deletes.
67
+ * The launcher would then point at nothing, and Chrome reports that only as
68
+ * "Native host has exited". A stable symlink to the same file does not move, so
69
+ * it is preferred, but only when realpath proves it resolves to the same binary:
70
+ * guessing a path that happens to exist could pin a different node version.
71
+ */
72
+ function stableNodePath() {
73
+ const exact = process.execPath;
74
+ let resolved;
75
+ try {
76
+ resolved = realpathSync(exact);
77
+ }
78
+ catch {
79
+ return exact;
80
+ }
81
+ const candidates = [
82
+ '/opt/homebrew/bin/node',
83
+ '/usr/local/bin/node',
84
+ join(homedir(), '.volta', 'bin', 'node'),
85
+ '/usr/bin/node',
86
+ ];
87
+ for (const candidate of candidates) {
88
+ try {
89
+ if (realpathSync(candidate) === resolved) {
90
+ return candidate;
91
+ }
92
+ }
93
+ catch { /* not installed, try the next */ }
94
+ }
95
+ return exact;
96
+ }
97
+ function writeLauncher(hostPath, nodePath = stableNodePath()) {
98
+ if (process.platform === 'win32') {
99
+ // Chrome runs .bat through the shell, and %~dp0 keeps it relocatable.
100
+ const batch = join(dirname(hostPath), 'yoke-host.bat');
101
+ writeFileSync(batch, `@echo off\r\n"${nodePath}" "${hostPath}" %*\r\n`);
102
+ return batch;
103
+ }
104
+ const launcher = join(dirname(hostPath), 'yoke-host.sh');
105
+ writeFileSync(launcher, `#!/bin/sh\nexec "${nodePath}" "${hostPath}" "$@"\n`, { mode: 0o755 });
106
+ chmodSync(launcher, 0o755);
107
+ return launcher;
108
+ }
109
+ export const manifestFor = (hostPath) => ({
110
+ name: HOST_NAME,
111
+ description: 'yoke native messaging host',
112
+ path: hostPath,
113
+ type: 'stdio',
114
+ allowed_origins: [`chrome-extension://${EXTENSION_ID}/`],
115
+ });
116
+ /**
117
+ * Writes the manifest into every browser directory whose parent already exists.
118
+ *
119
+ * Only where the browser is actually installed: creating a Brave directory on a
120
+ * machine with no Brave leaves litter for something that is not there.
121
+ */
122
+ export function install({ hostPath, platform = process.platform, home = homedir(), }) {
123
+ if (!existsSync(hostPath)) {
124
+ throw new Error(`the host script is not at ${hostPath}`);
125
+ }
126
+ try {
127
+ chmodSync(hostPath, 0o755);
128
+ }
129
+ catch { /* read-only install; reported below */ }
130
+ // The manifest points at a launcher rather than at the host, so Chrome never
131
+ // has to find node on a PATH it does not have.
132
+ const launcher = writeLauncher(hostPath);
133
+ const body = `${JSON.stringify(manifestFor(launcher), null, 2)}\n`;
134
+ const written = [];
135
+ const skipped = [];
136
+ for (const [browser, dir] of browserDirs(platform, home)) {
137
+ if (!existsSync(dirname(dir))) {
138
+ skipped.push([browser, 'not installed']);
139
+ continue;
140
+ }
141
+ try {
142
+ mkdirSync(dir, { recursive: true });
143
+ const file = join(dir, `${HOST_NAME}.json`);
144
+ writeFileSync(file, body);
145
+ written.push([browser, file]);
146
+ }
147
+ catch (failure) {
148
+ skipped.push([browser, failure instanceof Error ? failure.message : String(failure)]);
149
+ }
150
+ }
151
+ return { written, skipped, extensionId: EXTENSION_ID, hostName: HOST_NAME, platform };
152
+ }
153
+ export function uninstall({ platform = process.platform, home = homedir(), } = {}) {
154
+ const removed = [];
155
+ for (const [browser, dir] of browserDirs(platform, home)) {
156
+ const file = join(dir, `${HOST_NAME}.json`);
157
+ try {
158
+ unlinkSync(file);
159
+ removed.push([browser, file]);
160
+ }
161
+ catch { /* not there */ }
162
+ }
163
+ return { removed };
164
+ }
165
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}