granttap-mcp 0.8.14 → 0.8.16

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/README.md CHANGED
@@ -223,7 +223,8 @@ grok plugin marketplace add sergii-ziborov/granttap-mcp
223
223
  grok plugin install granttap --trust
224
224
  ```
225
225
 
226
- Install **GrantTap** from the Cursor plugin marketplace, then open
226
+ Import `sergii-ziborov/granttap-mcp` as a private Git plugin in Cursor. A public
227
+ listing requires an open-source plugin license and Cursor review. Reload Cursor, then open
227
228
  **Cursor Customize → MCPs → GrantTap → Authenticate**. Complete authorization
228
229
  and any one-time QR pairing on `https://granttap.com/connect`.
229
230
 
@@ -13,7 +13,7 @@ import { createOneTimePairing, DEFAULT_RELAY, PAIRING_CODE_TTL_MINUTES } from ".
13
13
  import { declareEngine } from "../engine/engine-declaration";
14
14
  import { isMachineConfigured } from "../../../mcp/src/pairing-status";
15
15
  import { installCursorHttpConfig } from "../../../mcp/src/cursor-config";
16
- import { installHttpMcpService } from "../../../mcp/src/http-service";
16
+ import { installHttpMcpService, waitForHttpMcpHealth } from "../../../mcp/src/http-service";
17
17
 
18
18
  function state(result: InstallResult): string {
19
19
  return result.status === "manual" ? "Needs attention" : "Ready";
@@ -67,6 +67,10 @@ async function main(): Promise<void> {
67
67
  const mcpClientInstalled = before.agents.some((item) => item.installed);
68
68
  if (before.cursor.installed || mcpClientInstalled) {
69
69
  httpService = installHttpMcpService();
70
+ if (process.platform === "win32" && httpService.status !== "manual"
71
+ && !await waitForHttpMcpHealth(undefined, 15_000)) {
72
+ httpService = { status: "manual", detail: "Windows background service did not become healthy." };
73
+ }
70
74
  if (before.cursor.installed && httpService.status !== "manual") {
71
75
  cursorConfig = installCursorHttpConfig();
72
76
  }
@@ -28,6 +28,7 @@ import { spawnSync } from "node:child_process";
28
28
  import type { CodingAgent } from "../../../packages/protocol/schema";
29
29
  import { refusesLiveLaunchd } from "./launchd-safety";
30
30
  import { configDir, loadRuntimeConfig, verifiableEngine } from "./config";
31
+ import { inspectWindowsTask, installWindowsTask } from "./windows-service";
31
32
 
32
33
  const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
33
34
 
@@ -287,6 +288,7 @@ export type MonitorIntegrationStatus = {
287
288
 
288
289
  /** Read-only status check. It never installs, reloads, or repairs the helper. */
289
290
  export function inspectMonitorHelper(): MonitorIntegrationStatus {
291
+ if (process.platform === "win32") return inspectWindowsTask("monitor");
290
292
  if (process.platform !== "darwin") return { configured: false, running: false };
291
293
  const agentsDir = process.env.GRANTTAP_LAUNCH_AGENTS_DIR
292
294
  ?? join(homedir(), "Library", "LaunchAgents");
@@ -340,7 +342,8 @@ function xml(value: string): string {
340
342
  }
341
343
 
342
344
  export function isCursorHelperNode(nodePath: string): boolean {
343
- return nodePath.includes("Cursor.app") || nodePath.includes("/helpers/node");
345
+ return /Cursor\.app|[\\/]Cursor[\\/].*[\\/]helpers[\\/]node/i.test(nodePath)
346
+ || nodePath.includes("/helpers/node");
344
347
  }
345
348
 
346
349
  /** True when a plist uses the caller's explicit development monitor pin. */
@@ -407,6 +410,12 @@ function engineEnvironment(): string[] {
407
410
  }
408
411
 
409
412
  export function installMonitorHelper(): InstallResult {
413
+ if (process.platform === "win32") {
414
+ const node = resolveMonitorNodeBin();
415
+ return node && !isCursorHelperNode(node)
416
+ ? installWindowsTask("monitor", node, join(repoRoot, "bin", "granttap-mcp.mjs"))
417
+ : { status: "manual", detail: "A stable Node.js 20+ installation is required for Windows background sync." };
418
+ }
410
419
  if (process.platform !== "darwin") {
411
420
  return { status: "manual", detail: "background task sync currently requires macOS" };
412
421
  }
@@ -0,0 +1,135 @@
1
+ /** Per-user Windows Task Scheduler jobs for the loopback MCP and phone monitor. */
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { configDir } from "./config";
6
+ import type { InstallResult } from "./install";
7
+
8
+ export type WindowsJob = "http" | "monitor";
9
+ const names: Record<WindowsJob, string> = {
10
+ http: "GrantTap MCP HTTP",
11
+ monitor: "GrantTap Phone Monitor",
12
+ };
13
+
14
+ export function windowsTaskName(job: WindowsJob): string { return names[job]; }
15
+
16
+ function xml(value: string): string {
17
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;")
18
+ .replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
19
+ }
20
+
21
+ export function windowsTaskDefinition(job: WindowsJob, node: string, launcher: string, owner = "TestUser", version = "0.0.0"): string {
22
+ const date = new Date(Date.now() - 60_000);
23
+ const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
24
+ const start = local.toISOString().slice(0, 19);
25
+ const args = `"${launcher}" internal ${job === "http" ? "serve" : "monitor"}`;
26
+ return [
27
+ '<?xml version="1.0" encoding="UTF-16"?>',
28
+ '<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
29
+ `<RegistrationInfo><Description>GrantTap ${xml(version)} ${job}</Description></RegistrationInfo>`,
30
+ "<Triggers><TimeTrigger>", `<StartBoundary>${start}</StartBoundary>`,
31
+ "<Enabled>true</Enabled><Repetition><Interval>PT1M</Interval></Repetition>",
32
+ "</TimeTrigger></Triggers>",
33
+ `<Principals><Principal id="Author"><UserId>${xml(owner)}</UserId>`,
34
+ "<LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>",
35
+ "<Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>",
36
+ "<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>",
37
+ "<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>",
38
+ "<StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
39
+ "<RestartOnFailure><Interval>PT1M</Interval><Count>3</Count></RestartOnFailure>",
40
+ "</Settings>",
41
+ `<Actions Context="Author"><Exec><Command>${xml(node)}</Command>`,
42
+ `<Arguments>${xml(args)}</Arguments><WorkingDirectory>${xml(dirname(launcher))}</WorkingDirectory>`,
43
+ "</Exec></Actions></Task>", "",
44
+ ].join("\n");
45
+ }
46
+
47
+ function taskXml(job: WindowsJob): string | null {
48
+ const result = spawnSync("schtasks.exe", ["/Query", "/TN", names[job], "/XML"], { windowsHide: true });
49
+ if (result.status !== 0) return null;
50
+ const output = result.stdout;
51
+ return output[0] === 0xff && output[1] === 0xfe || output.includes(0)
52
+ ? output.toString("utf16le").replace(/^\uFEFF/, "")
53
+ : output.toString("utf8");
54
+ }
55
+
56
+ function owned(job: WindowsJob, data: string): boolean {
57
+ return /<Command>[^<]*node\.exe<\/Command>/i.test(data)
58
+ && /granttap-mcp\.mjs(?:&quot;|\")? internal (?:serve|monitor)/i.test(data)
59
+ && data.includes(`internal ${job === "http" ? "serve" : "monitor"}`);
60
+ }
61
+
62
+ function identity(): string | null {
63
+ const result = spawnSync("whoami.exe", [], { encoding: "utf8", windowsHide: true });
64
+ return result.status === 0 ? result.stdout.trim() : null;
65
+ }
66
+
67
+ function taskFile(job: WindowsJob): string { return join(configDir(), `windows-${job}-task.xml`); }
68
+
69
+ export function inspectWindowsTask(job: WindowsJob): { configured: boolean; running: boolean } {
70
+ const data = taskXml(job);
71
+ if (!data || !owned(job, data)) return { configured: false, running: false };
72
+ const state = spawnSync("powershell.exe", [
73
+ "-NoProfile", "-NonInteractive", "-Command",
74
+ `(Get-ScheduledTask -TaskName '${names[job]}').State.ToString()`,
75
+ ], { encoding: "utf8", windowsHide: true });
76
+ return { configured: true, running: state.status === 0 && state.stdout.trim() === "Running" };
77
+ }
78
+
79
+ export function installWindowsTask(job: WindowsJob, node: string, launcher: string, forceReload = false): InstallResult {
80
+ if (!existsSync(node) || !existsSync(launcher)) {
81
+ return { status: "manual", detail: "Stable Node.js and granttap-mcp installation are required on Windows." };
82
+ }
83
+ const previous = taskXml(job);
84
+ if (previous && !owned(job, previous)) {
85
+ return { status: "manual", detail: `The existing ${names[job]} task is not owned by GrantTap.` };
86
+ }
87
+ const owner = identity();
88
+ if (!owner) return { status: "manual", detail: "Could not identify the current Windows user." };
89
+ let version: string;
90
+ try {
91
+ version = (JSON.parse(readFileSync(join(dirname(launcher), "..", "package.json"), "utf8")) as { version: string }).version;
92
+ if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error("invalid version");
93
+ } catch {
94
+ return { status: "manual", detail: "Installed GrantTap package version could not be read." };
95
+ }
96
+ const path = taskFile(job);
97
+ const definition = windowsTaskDefinition(job, node, launcher, owner, version);
98
+ const previousCommand = previous?.match(/<Command>([^<]*)<\/Command>/)?.[1];
99
+ const previousArgs = previous?.match(/<Arguments>([^<]*)<\/Arguments>/)?.[1];
100
+ const normalizedArgs = previousArgs?.replaceAll("&quot;", '"').replaceAll("&amp;", "&");
101
+ const already = previousCommand === xml(node)
102
+ && normalizedArgs === `"${launcher}" internal ${job === "http" ? "serve" : "monitor"}`
103
+ && previous?.includes("<Interval>PT1M</Interval>") === true
104
+ && previous.includes(`<Description>GrantTap ${version} ${job}</Description>`);
105
+ if (already && !forceReload && inspectWindowsTask(job).running) return { status: "already", detail: names[job] };
106
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
107
+ // Task Scheduler reads an XML file; keep it private to this user's config directory.
108
+ writeFileSync(path, `\uFEFF${definition}`, { encoding: "utf16le", mode: 0o600 });
109
+ const created = spawnSync("schtasks.exe", ["/Create", "/TN", names[job], "/XML", path, "/F"], {
110
+ encoding: "utf8", windowsHide: true,
111
+ });
112
+ unlinkSync(path);
113
+ if (created.status !== 0) return { status: "manual", detail: `${names[job]} could not be registered for this Windows user.` };
114
+ if (previous && (forceReload || !already)) {
115
+ spawnSync("schtasks.exe", ["/End", "/TN", names[job]], { windowsHide: true });
116
+ }
117
+ const started = spawnSync("schtasks.exe", ["/Run", "/TN", names[job]], { encoding: "utf8", windowsHide: true });
118
+ return started.status === 0
119
+ ? { status: already ? "already" : "installed", detail: names[job] }
120
+ : { status: "manual", detail: `${names[job]} was registered but did not start.` };
121
+ }
122
+
123
+ export function snapshotWindowsTask(job: WindowsJob): string | null { return taskXml(job); }
124
+
125
+ export function restoreWindowsTask(job: WindowsJob, previous: string | null): boolean {
126
+ const current = taskXml(job);
127
+ if (!current || !owned(job, current) || (previous && !owned(job, previous))) return false;
128
+ spawnSync("schtasks.exe", ["/End", "/TN", names[job]], { windowsHide: true });
129
+ if (!previous) return spawnSync("schtasks.exe", ["/Delete", "/TN", names[job], "/F"], { windowsHide: true }).status === 0;
130
+ const path = taskFile(job);
131
+ writeFileSync(path, `\uFEFF${previous}`, { encoding: "utf16le", mode: 0o600 });
132
+ const restored = spawnSync("schtasks.exe", ["/Create", "/TN", names[job], "/XML", path, "/F"], { windowsHide: true });
133
+ unlinkSync(path);
134
+ return restored.status === 0 && spawnSync("schtasks.exe", ["/Run", "/TN", names[job]], { windowsHide: true }).status === 0;
135
+ }
@@ -13,7 +13,6 @@ import {
13
13
  waitForHttpMcpHealth,
14
14
  } from "../http-service";
15
15
  import type { InstallResult } from "../../../bridge/src/install";
16
- import { existsSync } from "node:fs";
17
16
 
18
17
  async function authorize(): Promise<void> {
19
18
  const preflight = validateCursorHttpConfig();
@@ -33,7 +32,7 @@ async function authorize(): Promise<void> {
33
32
  if (alreadyHealthy) {
34
33
  if (before.configured && before.running) {
35
34
  service = { status: "already", detail: httpMcpLaunchAgentPath() };
36
- } else if (existsSync(httpMcpLaunchAgentPath())) {
35
+ } else if (serviceBefore.exists) {
37
36
  repairAttempted = true;
38
37
  service = installHttpMcpService({ forceReload: true });
39
38
  } else {
@@ -70,9 +69,9 @@ async function authorize(): Promise<void> {
70
69
  [
71
70
  `[granttap-mcp] Cursor · Configured — ${cursor.detail}`,
72
71
  `[granttap-mcp] Persistent local OAuth is healthy at ${mcpUrl}`,
73
- `[granttap-mcp] LaunchAgent: ${service.status} (${service.detail})`,
72
+ `[granttap-mcp] Background service: ${service.status} (${service.detail})`,
74
73
  "[granttap-mcp] Open Cursor Customize → MCPs → GrantTap → Authenticate. The browser continues at granttap.com/connect.",
75
- "[granttap-mcp] If this Mac is not paired, granttap.com/connect shows a QR and manual token.",
74
+ "[granttap-mcp] If this computer is not paired, granttap.com/connect shows a QR and manual token.",
76
75
  "",
77
76
  ].join("\n"),
78
77
  );
@@ -7,6 +7,7 @@ export const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "
7
7
  export const HTTP_SERVICE_LABEL = "com.granttap.mcp-http";
8
8
 
9
9
  export function httpMcpLaunchAgentPath(): string {
10
+ if (process.platform === "win32") return "GrantTap MCP HTTP";
10
11
  const agentsDir = process.env.GRANTTAP_LAUNCH_AGENTS_DIR
11
12
  ?? join(homedir(), "Library", "LaunchAgents");
12
13
  return join(agentsDir, `${HTTP_SERVICE_LABEL}.plist`);
@@ -5,13 +5,14 @@ import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { isCursorHelperNode, resolveMonitorNodeBin, type InstallResult } from "../../../bridge/src/install";
7
7
  import { configDir, normalizeRelayUrl } from "../../../bridge/src/config";
8
+ import { installWindowsTask } from "../../../bridge/src/windows-service";
8
9
  import { configuredCursorHttpMcpUrl } from "../cursor-config";
9
10
  import { HTTP_SERVICE_LABEL, httpMcpLaunchAgentPath, packageRoot, xml } from "./common";
10
11
  import { inspectHttpMcpService, restoreHttpMcpServiceAfterFailure, snapshotHttpMcpService } from "./snapshot";
11
12
 
12
13
  /** Install/repair and load a per-user, loopback-only OAuth MCP daemon. */
13
14
  export function installHttpMcpService(options: { forceReload?: boolean } = {}): InstallResult {
14
- if (process.platform !== "darwin") return macosOnly();
15
+ if (process.platform !== "darwin" && process.platform !== "win32") return macosOnly();
15
16
  const mcpUrl = configuredMcpUrl();
16
17
  if (mcpUrl instanceof Error) return { status: "manual", detail: mcpUrl.message };
17
18
  const nodeBin = safeNodeBin();
@@ -19,6 +20,12 @@ export function installHttpMcpService(options: { forceReload?: boolean } = {}):
19
20
  const executable = join(packageRoot, "bin", "granttap-mcp.mjs");
20
21
  const installation = validateInstallation(nodeBin, executable);
21
22
  if (installation) return installation;
23
+ if (process.platform === "win32") {
24
+ if (mcpUrl.href !== "http://127.0.0.1:17342/mcp") {
25
+ return { status: "manual", detail: "Windows background service currently uses the default loopback endpoint." };
26
+ }
27
+ return installWindowsTask("http", nodeBin, executable, options.forceReload);
28
+ }
22
29
  const path = httpMcpLaunchAgentPath();
23
30
  const logsDir = join(configDir(), "logs");
24
31
  const plist = createPlist(nodeBin, executable, mcpUrl, logsDir);
@@ -11,6 +11,7 @@ import {
11
11
  writeFileSync,
12
12
  } from "node:fs";
13
13
  import { dirname } from "node:path";
14
+ import { inspectWindowsTask, restoreWindowsTask, snapshotWindowsTask, windowsTaskName } from "../../../bridge/src/windows-service";
14
15
  import {
15
16
  httpMcpLaunchAgentPath,
16
17
  isConfiguredHttpService,
@@ -32,6 +33,15 @@ export type HttpMcpServiceSnapshot = {
32
33
 
33
34
  /** Exact plist bytes/mode and loaded state for transactional repair rollback. */
34
35
  export function snapshotHttpMcpService(): HttpMcpServiceSnapshot {
36
+ if (process.platform === "win32") {
37
+ const task = snapshotWindowsTask("http");
38
+ const state = inspectWindowsTask("http");
39
+ return {
40
+ path: windowsTaskName("http"), exists: task !== null,
41
+ plist: task === null ? null : Buffer.from(task), mode: 0o600,
42
+ owned: state.configured, configured: state.configured, running: state.running,
43
+ };
44
+ }
35
45
  const path = httpMcpLaunchAgentPath();
36
46
  if (!existsSync(path)) return missingSnapshot(path);
37
47
  let plist: Buffer;
@@ -65,6 +75,7 @@ function unreadableSnapshot(path: string): HttpMcpServiceSnapshot {
65
75
 
66
76
  /** Read-only plist/process inspection. Health is probed separately. */
67
77
  export function inspectHttpMcpService(): HttpMcpServiceStatus {
78
+ if (process.platform === "win32") return inspectWindowsTask("http");
68
79
  if (process.platform !== "darwin") return { configured: false, running: false };
69
80
  const snapshot = snapshotHttpMcpService();
70
81
  return { configured: snapshot.configured, running: snapshot.configured && snapshot.running };
@@ -72,6 +83,10 @@ export function inspectHttpMcpService(): HttpMcpServiceStatus {
72
83
 
73
84
  /** Restore a failed repair, deleting the service only when this attempt created it. */
74
85
  export function restoreHttpMcpServiceAfterFailure(before: HttpMcpServiceSnapshot): boolean {
86
+ if (process.platform === "win32") {
87
+ if (before.exists && !before.owned) return false;
88
+ return restoreWindowsTask("http", before.plist?.toString("utf8") ?? null);
89
+ }
75
90
  if (before.exists && !before.owned) return false;
76
91
  const current = currentOwnedPlist(before.path);
77
92
  if (current === undefined) return false;
@@ -2,7 +2,7 @@
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
- import { dirname, join } from "node:path";
5
+ import { delimiter, dirname, join } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
8
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
@@ -107,7 +107,7 @@ const child = spawn(
107
107
  cwd: root,
108
108
  env: {
109
109
  ...process.env,
110
- NODE_PATH: [join(root, "node_modules"), process.env.NODE_PATH].filter(Boolean).join(":"),
110
+ NODE_PATH: [join(root, "node_modules"), process.env.NODE_PATH].filter(Boolean).join(delimiter),
111
111
  },
112
112
  },
113
113
  );
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "GrantTap for Cursor — authorize on granttap.com/connect and approve local tasks from your phone",
9
- "version": "0.1.5"
9
+ "version": "0.1.6"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "granttap",
3
3
  "displayName": "GrantTap",
4
- "version": "0.1.5",
4
+ "version": "0.1.6",
5
5
  "description": "Connect Cursor to GrantTap through granttap.com/connect, pair your phone with a one-time QR, and keep approvals in the originating chat.",
6
6
  "author": {
7
7
  "name": "GrantTap",
@@ -6,8 +6,7 @@ each phone response tied to the exact originating chat and prompt.
6
6
 
7
7
  ## Connect Cursor
8
8
 
9
- Install the supported CLI and the GrantTap plugin from Cursor's marketplace,
10
- then run:
9
+ Install the supported CLI on this computer, then run:
11
10
 
12
11
  ```bash
13
12
  npm install -g granttap-mcp
@@ -25,19 +24,31 @@ The commands have separate jobs:
25
24
 
26
25
  After `granttap setup`, open **Cursor Customize → MCPs → GrantTap** and choose
27
26
  **Authenticate**. The browser opens granttap.com/connect to authorize Cursor.
28
- If the Mac is not paired, the website shows a one-time QR and manual-code
27
+ If this computer is not paired, the website shows a one-time QR and manual-code
29
28
  fallback; a saved pairing is reused unless you explicitly confirm reconnect.
30
29
 
30
+ On Windows, `granttap setup` registers the loopback MCP and phone monitor as
31
+ least-privileged jobs under your signed-in account. Install Node.js 20+ and the
32
+ current `granttap-mcp` package on that same Windows computer; the plugin alone
33
+ cannot start the local server. The jobs restart after a crash and keep running
34
+ when Cursor closes. Run `granttap status` to confirm readiness before choosing
35
+ **Authenticate** in Cursor.
36
+
31
37
  The OAuth service listens only at `http://127.0.0.1:17342/mcp`. Cursor cannot
32
38
  show **Authenticate** for a stdio (`command`/`args`) MCP entry, so do not replace the
33
39
  plugin's HTTP configuration with stdio.
34
40
 
35
- ## Install from the Cursor marketplace
36
-
37
- Install **GrantTap** from Cursor's plugin marketplace, then reload Cursor.
38
- The plugin adds the `/connect` command, a connect skill, and the
39
- exact-correlation rule; `granttap setup` still configures the MCP endpoint
40
- itself.
41
+ ## Install the Cursor plugin
42
+
43
+ A public Cursor Marketplace listing requires an open-source plugin license and
44
+ manual review. The current commercially licensed plugin is available as a
45
+ private Git import: `sergii-ziborov/granttap-mcp`. Reload Cursor after import.
46
+ The private listing is tied to the version imported on that computer; re-import
47
+ it to pick up the current orange icon and plugin changes. If a public listing
48
+ is later approved, install **GrantTap** from Cursor's marketplace to receive
49
+ its reviewed updates. The plugin adds the `/connect` command, a connect
50
+ skill, and the exact-correlation rule; `granttap setup` configures the local
51
+ MCP endpoint separately.
41
52
 
42
53
  ## Exact dual-channel behavior
43
54
 
@@ -1,7 +1,5 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
- <rect width="64" height="64" rx="14" fill="#0B1F17"/>
3
- <circle cx="32" cy="32" r="18" stroke="#3DDC97" stroke-width="3"/>
4
- <path d="M24 28c0-4.4 3.6-8 8-8h2" stroke="#3DDC97" stroke-width="3" stroke-linecap="round"/>
5
- <path d="M40 36c0 4.4-3.6 8-8 8h-2" stroke="#3DDC97" stroke-width="3" stroke-linecap="round"/>
6
- <circle cx="32" cy="32" r="3" fill="#3DDC97"/>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="GrantTap">
2
+ <rect width="64" height="64" rx="10" fill="#f47f42"/>
3
+ <path d="M11 31.5 24.5 45 42 19" fill="none" stroke="#1d130e" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M45.5 29v10M52 25v18M58.5 29v10" fill="none" stroke="#1d130e" stroke-width="5" stroke-linecap="round"/>
7
5
  </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.14",
3
+ "version": "0.8.16",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {