granttap-mcp 0.8.91 → 0.8.93

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.
@@ -86,7 +86,7 @@ export function buildTaskCapsule(
86
86
  const capsule = {
87
87
  taskId: request.taskId,
88
88
  goal: task.goal,
89
- currentStatus: session.summary ?? session.title ?? task.title,
89
+ currentStatus: (session.summary?.trim() || session.title?.trim() || task.title).slice(0, 1_000),
90
90
  sourceProvider: session.agent,
91
91
  sourceComputer: computerId,
92
92
  targetProvider: request.targetProvider,
@@ -166,8 +166,17 @@ function linkSession(input: {
166
166
  const agent = provider(session.agent);
167
167
  const cwd = session.cwd?.trim();
168
168
  if (!agent || !cwd) return session;
169
- const repository = inspect(session.worktree ?? cwd);
170
169
  const knownTaskId = store.taskForExecution(computerId, agent, session.sessionId);
170
+ const knownProjectId = knownTaskId ? store.task(knownTaskId)?.projectId : undefined;
171
+ const previous = knownProjectId ? store.snapshot(knownProjectId)?.executions.find(row =>
172
+ row.taskId === knownTaskId && row.provider === agent && row.sessionId === session.sessionId
173
+ && row.computerId === computerId && row.workspace === cwd) : undefined;
174
+ let repository = inspect(session.worktree ?? cwd);
175
+ if (!repository.worktree && previous?.worktree) {
176
+ const retained = inspect(previous.worktree);
177
+ if (retained.worktree && retained.revision
178
+ && retained.canonicalRepositoryId === previous.repositoryId) repository = retained;
179
+ }
171
180
  const projectId = (knownTaskId ? store.task(knownTaskId)?.projectId : undefined)
172
181
  ?? store.projectIdForRepository(repository.canonicalRepositoryId, computerId)
173
182
  ?? projectIdentity(repository.canonicalRepositoryId);
@@ -229,13 +238,14 @@ function linkSession(input: {
229
238
  provider: agent,
230
239
  computerId,
231
240
  workspace: cwd,
232
- repositoryId: repository.canonicalRepositoryId,
241
+ repositoryId: repository.worktree ? repository.canonicalRepositoryId
242
+ : previous?.repositoryId ?? repository.canonicalRepositoryId,
233
243
  activeAt: session.lastActivityAt,
234
244
  state: session.state, continuedAt: session.transfer?.continuedAt,
235
245
  branch: repository.branch ?? session.branch,
236
- worktree: repository.worktree,
246
+ worktree: repository.worktree ?? previous?.worktree,
237
247
  uncommitted: input.inspectWorkingTree(repository.worktree ?? cwd),
238
248
  startedAt: session.startedAt,
239
249
  });
240
- return { ...session, projectId, taskId, computerId, worktree: repository.worktree };
250
+ return { ...session, projectId, taskId, computerId, worktree: repository.worktree ?? previous?.worktree };
241
251
  }
@@ -12,13 +12,20 @@ Catalog batches share a working-tree probe for chats in the same checkout.
12
12
  The next batch reads it again, including unknown results, so this never hides
13
13
  new uncommitted work behind a persistent cache.
14
14
 
15
- For a chat started above its repositories, `source-binding.ts` resolves an
16
- explicit `sourceBindingId` only within the owning endpoint's available Project
17
- bindings. It verifies the actual Git root and identity before capturing context.
15
+ For a chat started above its repositories, `source-binding.ts` first uses the
16
+ Task's own execution, current resource claim, Project repository, or sole owned
17
+ checkout. Multiple unresolved choices require an explicit `sourceBindingId`.
18
+ Both paths stay within the owning endpoint's available Project bindings and
19
+ verify the actual Git root and identity before capturing context. A catalog
20
+ refresh preserves an established checkout when the native chat still reports
21
+ the same parent folder; missing known checkouts never select another repository.
18
22
  An exact repository binding takes precedence over a Project's parent execution
19
23
  folder. Preparing keeps the native source folder unchanged and launches no
20
24
  provider. Rejected preparations also publish their retained Project state.
21
25
 
26
+ Capsule status is bounded independently from the complete conversation capture;
27
+ a long native summary cannot invalidate an otherwise transferable Git Task.
28
+
22
29
  Tests: `tests/metadata.test.ts`, `tests/source-checkout.test.ts`,
23
30
  `tests/mesh/mesh-handoff-safety.test.ts`, and catalog
24
31
  `tests/working-tree-batch.test.ts`.
@@ -1,16 +1,27 @@
1
1
  import { realpathSync } from 'node:fs';
2
- import type { MeshHandoffPrepare, SessionInfo } from '../../../../../../packages/protocol/schema';
2
+ import type { MeshHandoffPrepare, MeshSnapshot, ProjectBindingSummary, SessionInfo } from '../../../../../../packages/protocol/schema';
3
3
  import { inspectRepository } from '../../catalog';
4
4
  import type { MeshStore } from '../../store';
5
5
  import { matchesLocalCheckout } from '../graph/checkout-identity';
6
6
 
7
- /** The controller chooses an already admitted binding, never an arbitrary path. */
7
+ /** Resolve the Task's admitted checkout; an explicit choice can replace that default. */
8
8
  export function resolveHandoffSource(store: MeshStore, session: SessionInfo,
9
9
  request: MeshHandoffPrepare, computer: string): SessionInfo {
10
- if (!request.sourceBindingId) return session;
11
- const binding = store.snapshot(request.projectId)?.bindings?.find(row =>
12
- row.bindingId === request.sourceBindingId && row.projectId === request.projectId
13
- && row.endpointId === computer && row.available);
10
+ const snapshot = store.snapshot(request.projectId);
11
+ if (!request.sourceBindingId) {
12
+ const cwd = session.worktree ?? session.cwd;
13
+ if (cwd) {
14
+ const native = inspectRepository(cwd);
15
+ if (native.worktree && native.revision) {
16
+ return { ...session, worktree: native.root, branch: session.branch ?? native.branch };
17
+ }
18
+ }
19
+ }
20
+ const binding = request.sourceBindingId
21
+ ? snapshot?.bindings?.find(row => row.bindingId === request.sourceBindingId
22
+ && row.projectId === request.projectId && row.endpointId === computer && row.available)
23
+ : inferBinding(snapshot, session, request, computer);
24
+ if (!binding && !request.sourceBindingId) return session;
14
25
  if (binding?.localPathHint) {
15
26
  const facts = inspectRepository(binding.localPathHint);
16
27
  try {
@@ -22,3 +33,50 @@ export function resolveHandoffSource(store: MeshStore, session: SessionInfo,
22
33
  }
23
34
  throw new Error('The selected repository is not available on this computer. Refresh the Project and choose its checkout again.');
24
35
  }
36
+
37
+ function samePath(left: string | undefined, right: string | undefined): boolean {
38
+ if (!left || !right) return false;
39
+ try { return realpathSync(left) === realpathSync(right); } catch { return false; }
40
+ }
41
+
42
+ function inferBinding(snapshot: MeshSnapshot | undefined, session: SessionInfo,
43
+ request: MeshHandoffPrepare, computer: string): ProjectBindingSummary | undefined {
44
+ const paths = new Set<string>();
45
+ const bindings = (snapshot?.bindings ?? []).filter(row => {
46
+ if (row.projectId !== request.projectId || row.endpointId !== computer
47
+ || !row.available || !row.revision || !row.localPathHint) return false;
48
+ try {
49
+ const path = realpathSync(row.localPathHint);
50
+ if (paths.has(path)) return false;
51
+ paths.add(path);
52
+ return true;
53
+ } catch { return false; }
54
+ });
55
+ if (!bindings.length) return undefined;
56
+ const execution = snapshot?.executions.find(row => row.taskId === request.taskId
57
+ && row.provider === session.agent && row.sessionId === session.sessionId
58
+ && row.computerId === computer && row.endedAt == null);
59
+ if (execution?.worktree) {
60
+ const exact = bindings.find(row => samePath(row.localPathHint, execution.worktree));
61
+ if (exact) return exact;
62
+ throw new Error('This Task\'s source checkout is unavailable. Refresh its computer before transferring.');
63
+ }
64
+ const byExecution = execution?.repositoryId
65
+ ? bindings.filter(row => row.repositoryId === execution.repositoryId) : [];
66
+ if (byExecution.length === 1) return byExecution[0];
67
+ const parent = snapshot?.bindings?.some(row => row.repositoryId === execution?.repositoryId
68
+ && row.endpointId === computer && !row.revision && samePath(row.localPathHint, session.cwd));
69
+ if (execution?.repositoryId && !parent) {
70
+ throw new Error('This Task\'s source repository is unavailable. Refresh its computer before transferring.');
71
+ }
72
+ const claims = snapshot?.claims.filter(row => row.taskId === request.taskId
73
+ && row.ownerSessionId === session.sessionId && row.expiresAt > Date.now()
74
+ && (!row.endpointId || row.endpointId === computer)) ?? [];
75
+ const byClaim = bindings.filter(row => claims.some(claim =>
76
+ claim.repositoryId === row.repositoryId && (!claim.worktree || samePath(claim.worktree, row.localPathHint))));
77
+ if (byClaim.length === 1) return byClaim[0];
78
+ const byProject = bindings.filter(row => row.repositoryId === snapshot?.project.canonicalRepositoryId);
79
+ if (byProject.length === 1) return byProject[0];
80
+ if (bindings.length === 1) return bindings[0];
81
+ throw new Error('Multiple Project checkouts are available. Choose the repository this Task should continue in.');
82
+ }
@@ -9,10 +9,27 @@ expire after five minutes and changing the native auth file clears the cache.
9
9
  Only visible picker metadata and reported usage percentages, window durations,
10
10
  reset times and observation times cross the existing encrypted Mesh channel.
11
11
  Identity, credentials, credits, instructions and raw provider responses stay out.
12
- Missing windows remain unknown. Claude CLI aliases remain available. The GrantTap status-line wrapper observes
12
+ Missing windows remain unknown. `native-usage/reader.ts` independently reads the
13
+ already signed-in Claude, Cursor and Grok account interfaces, with an eight-second
14
+ request bound, a 64 KiB response bound and no redirects. Requests coalesce and
15
+ refresh at most once every two minutes. Expired windows disappear without
16
+ inventing a reset allowance. `native-usage/credentials.ts` reads existing native files
17
+ or the native macOS keychain entries; it never starts login, refreshes credentials
18
+ or changes provider settings. A relocated profile cannot use another account's
19
+ keychain. Temporary HOME directories never read the OS user's keychain.
20
+
21
+ Claude CLI aliases remain available. The GrantTap status-line wrapper observes
13
22
  Claude's documented `rate_limits` after a native response, preserves the prior
14
23
  status command/options, and stores only independently present 5h/7d windows.
15
- Cursor and Grok subscription limits remain unavailable when their installed
16
- interfaces do not report them; their session tokens and tools are still counted.
24
+ Its newest real observation takes precedence over an older direct read. Expired
25
+ Claude credentials require a native Claude refresh; they are not a full allowance.
26
+
27
+ Cursor reports its weekly account usage, with the native billing-period pool as
28
+ a fallback for older plans. Grok reports its current subscription period and
29
+ credit usage percentage through its CLI billing interface. Unified subscriptions
30
+ may share a pool across Cursor and Grok: these are provider views, not additive
31
+ allowances. Grok's reported percentage can be rounded by the provider. A missing
32
+ or unauthenticated account report stays unknown. Session tokens, context size and
33
+ tool activity do not stand in for subscription limits.
17
34
 
18
- Tests: `tests/status.test.ts`. GrantTap Commercial Source License, see root LICENSE.
35
+ Tests live in `tests/`. GrantTap Commercial Source License, see root LICENSE.
@@ -6,6 +6,7 @@ import { codexHookRpc, type HookRpc } from "../codex-hook-trust";
6
6
  import { projectAccountModels, projectAccountUsage } from "./projection";
7
7
 
8
8
  export { projectAccountModels, projectAccountUsage } from "./projection";
9
+ export { cachedNativeAccountUsage } from "./native-usage";
9
10
 
10
11
  /** A bounded read of an already signed-in provider; no threads, turns or login. */
11
12
  export function createProviderAccountReader(rpc: HookRpc, clock = Date.now) {
@@ -0,0 +1,12 @@
1
+ # Native account allowances
2
+
3
+ Public entry: `index.ts`, exported by the parent Provider Account Status module.
4
+ Reads the existing Claude, Cursor and Grok account interfaces without creating
5
+ sessions or changing credentials. `projection.ts` retains only usage windows;
6
+ `credentials.ts` bounds local native credential reads; `reader.ts` coalesces,
7
+ throttles and expires the independent requests. Native status-line observations
8
+ keep their actual timestamps and can supersede direct Claude observations.
9
+
10
+ Tests live in `tests/`; the parent README documents data sources and boundaries.
11
+
12
+ GrantTap Commercial Source License. See the repository root LICENSE.
@@ -0,0 +1,69 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { homedir, userInfo } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { NativeUsageProvider } from "./projection";
6
+
7
+ type Options = {
8
+ home?: string;
9
+ env?: NodeJS.ProcessEnv;
10
+ platform?: NodeJS.Platform;
11
+ keychain?: (service: string, account?: string) => Promise<string | undefined>;
12
+ now?: number;
13
+ };
14
+
15
+ /** The same existing native entries used by Claude Code and Cursor. Never refresh or write credentials. */
16
+ export function readNativeKeychain(service: string, account?: string, run = execFile): Promise<string | undefined> {
17
+ return new Promise(resolve => run("/usr/bin/security",
18
+ ["find-generic-password", "-s", service, ...(account ? ["-a", account] : []), "-w"],
19
+ { timeout: 3_000, maxBuffer: 65_536, windowsHide: true },
20
+ (error, stdout) => resolve(error ? undefined : stdout.trim() || undefined)));
21
+ }
22
+
23
+ async function jsonFile(path: string): Promise<any> {
24
+ try {
25
+ if ((await stat(path)).size > 65_536) return undefined;
26
+ return JSON.parse(await readFile(path, "utf8"));
27
+ } catch { return undefined; }
28
+ }
29
+
30
+ function token(value: unknown): string | undefined {
31
+ return typeof value === "string" && value.length > 0 && value.length <= 32_768
32
+ && !/[\r\n]/.test(value) ? value : undefined;
33
+ }
34
+
35
+ function claudeToken(value: any, now: number): string | undefined {
36
+ const row = value?.claudeAiOauth;
37
+ return typeof row?.expiresAt === "number" && row.expiresAt <= now ? undefined : token(row?.accessToken);
38
+ }
39
+
40
+ export async function readNativeCredential(provider: NativeUsageProvider, options: Options = {}): Promise<string | undefined> {
41
+ const home = options.home ?? homedir();
42
+ const env = options.env ?? process.env;
43
+ const platform = options.platform ?? process.platform;
44
+ const keychain = options.keychain ?? readNativeKeychain;
45
+ const keychainAllowed = options.keychain !== undefined || home === userInfo().homedir;
46
+ if (provider === "grok") {
47
+ const root = env.GRANTTAP_GROK_DIR ?? join(home, ".grok");
48
+ const auth = await jsonFile(join(root, "auth.json"));
49
+ return auth && Object.entries(auth).flatMap(([id, row]: [string, any]) =>
50
+ id.startsWith("https://auth.x.ai::") && row?.auth_mode === "oidc" ? [token(row.key)] : [])
51
+ .find(Boolean);
52
+ }
53
+ if (provider === "claude") {
54
+ const override = env.GRANTTAP_CLAUDE_DIR ?? env.NODVOX_CLAUDE_DIR ?? env.CLAUDE_CONFIG_DIR;
55
+ const saved = claudeToken(await jsonFile(join(override ?? join(home, ".claude"), ".credentials.json")), options.now ?? Date.now());
56
+ if (saved || platform !== "darwin" || override || !keychainAllowed) return saved;
57
+ if (!await jsonFile(join(home, ".claude", "settings.json"))) return undefined;
58
+ try {
59
+ const raw = await keychain("Claude Code-credentials");
60
+ return raw ? claudeToken(JSON.parse(raw), options.now ?? Date.now()) : undefined;
61
+ } catch { return undefined; }
62
+ }
63
+ const root = platform === "win32" ? join(env.APPDATA ?? join(home, "AppData", "Roaming"), "Cursor")
64
+ : platform === "darwin" ? join(home, ".cursor") : join(env.XDG_CONFIG_HOME ?? join(home, ".config"), "cursor");
65
+ const saved = token((await jsonFile(join(root, "auth.json")))?.accessToken);
66
+ if (saved || platform !== "darwin" || !keychainAllowed || !await jsonFile(join(root, "cli-config.json"))) return saved;
67
+ try { return token(await keychain("cursor-access-token", "cursor-user")); }
68
+ catch { return undefined; }
69
+ }
@@ -0,0 +1,30 @@
1
+ import { homedir } from "node:os";
2
+ import type { ProviderAccountUsage } from "../../../../../packages/protocol/schema";
3
+ import { readClaudeUsage } from "../claude-statusline";
4
+ import { readNativeCredential } from "./credentials";
5
+ import { createNativeAccountUsageReader } from "./reader";
6
+
7
+ let scope = "";
8
+ let reader = createNativeAccountUsageReader({ credential: readNativeCredential });
9
+
10
+ export function mergeNativeUsage(direct: ProviderAccountUsage[], statusLine: ProviderAccountUsage[],
11
+ now = Date.now()): ProviderAccountUsage[] {
12
+ const newest = new Map<string, ProviderAccountUsage>();
13
+ for (const report of [...direct, ...statusLine]) {
14
+ const key = `${report.provider}\u001f${report.limitId}`;
15
+ const windows = report.windows.filter(window => window.resetsAt > now);
16
+ if (!windows.length || now - report.observedAt > 300_000 || report.observedAt > now + 60_000) continue;
17
+ if ((newest.get(key)?.observedAt ?? -Infinity) <= report.observedAt) newest.set(key, { ...report, windows });
18
+ }
19
+ return [...newest.values()];
20
+ }
21
+
22
+ /** Account reads are scheduled beside the catalog; provider credentials never enter its result. */
23
+ export function cachedNativeAccountUsage(): ProviderAccountUsage[] {
24
+ const env = process.env;
25
+ const current = [homedir(), env.GRANTTAP_CLAUDE_DIR, env.NODVOX_CLAUDE_DIR, env.CLAUDE_CONFIG_DIR,
26
+ env.GRANTTAP_GROK_DIR, env.XDG_CONFIG_HOME, env.APPDATA].join("\u001f");
27
+ if (scope !== current) { scope = current; reader = createNativeAccountUsageReader({ credential: readNativeCredential }); }
28
+ void reader.refresh();
29
+ return mergeNativeUsage(reader.current(), readClaudeUsage());
30
+ }
@@ -0,0 +1,52 @@
1
+ import type { ProviderAccountUsage } from "../../../../../packages/protocol/schema";
2
+
3
+ type ProviderUsageWindow = ProviderAccountUsage["windows"][number];
4
+
5
+ export type NativeUsageProvider = "claude" | "cursor" | "grok";
6
+
7
+ function percent(value: unknown): value is number {
8
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100;
9
+ }
10
+
11
+ function timestamp(value: unknown): number {
12
+ return typeof value === "string" ? Date.parse(value) : NaN;
13
+ }
14
+
15
+ /** Only allowance windows cross the wire; identity, money and top-up settings never do. */
16
+ export function projectNativeAccountUsage(provider: NativeUsageProvider, input: any,
17
+ now = Date.now()): ProviderAccountUsage | undefined {
18
+ let windows: ProviderUsageWindow[] = [];
19
+ if (provider === "claude") {
20
+ windows = ([["five_hour", "primary", 300], ["seven_day", "secondary", 10_080]] as const)
21
+ .flatMap(([key, id, windowMinutes]) => {
22
+ const row = input?.[key];
23
+ const resetsAt = timestamp(row?.resets_at);
24
+ return percent(row?.utilization) && resetsAt > now
25
+ ? [{ id, usedPercent: row.utilization, windowMinutes, resetsAt }] : [];
26
+ });
27
+ } else {
28
+ const start = timestamp(provider === "cursor" ? input?.currentPeriodStart : input?.config?.currentPeriod?.start);
29
+ const resetsAt = timestamp(provider === "cursor" ? input?.nextResetTimestampUtc : input?.config?.currentPeriod?.end);
30
+ const usedPercent = provider === "cursor" ? input?.usagePercent : input?.config?.creditUsagePercent;
31
+ if (percent(usedPercent) && Number.isFinite(start) && start <= now && resetsAt > now && resetsAt > start) {
32
+ const windowMinutes = Math.ceil((resetsAt - start) / 60_000);
33
+ if (Number.isSafeInteger(windowMinutes) && windowMinutes > 0) {
34
+ windows = [{ id: "primary", usedPercent, windowMinutes, resetsAt }];
35
+ }
36
+ }
37
+ }
38
+ return windows.length ? { provider, limitId: provider, observedAt: now, windows } : undefined;
39
+ }
40
+
41
+ /** Older Cursor plans report a billing-period pool rather than a weekly pool. */
42
+ export function projectCursorPeriodUsage(input: any, now = Date.now()): ProviderAccountUsage | undefined {
43
+ const start = Number(input?.billingCycleStart), end = Number(input?.billingCycleEnd);
44
+ const plan = input?.planUsage;
45
+ const usedPercent = percent(plan?.totalPercentUsed) ? plan.totalPercentUsed
46
+ : Number.isFinite(plan?.totalSpend) && plan.totalSpend >= 0 && Number.isFinite(plan?.limit) && plan.limit > 0
47
+ ? plan.totalSpend / plan.limit * 100 : undefined;
48
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= start
49
+ || start > 8.64e15 || end > 8.64e15) return undefined;
50
+ return projectNativeAccountUsage("cursor", { usagePercent: usedPercent,
51
+ currentPeriodStart: new Date(start).toISOString(), nextResetTimestampUtc: new Date(end).toISOString() }, now);
52
+ }
@@ -0,0 +1,85 @@
1
+ import type { ProviderAccountUsage } from "../../../../../packages/protocol/schema";
2
+ import { projectCursorPeriodUsage, projectNativeAccountUsage, type NativeUsageProvider } from "./projection";
3
+
4
+ type Options = {
5
+ credential: (provider: NativeUsageProvider) => Promise<string | undefined>;
6
+ fetcher?: (url: string, init: RequestInit) => Promise<Response>;
7
+ clock?: () => number;
8
+ };
9
+
10
+ const requests = {
11
+ claude: { url: "https://api.anthropic.com/api/oauth/usage", method: "GET" },
12
+ cursor: { url: "https://api2.cursor.sh/aiserver.v1.DashboardService/GetSandUsageStatus", method: "POST" },
13
+ grok: { url: "https://cli-chat-proxy.grok.com/v1/billing?format=credits", method: "GET" },
14
+ } as const;
15
+
16
+ async function boundedResponse(response: Response): Promise<unknown> {
17
+ if (!response.ok || !response.body) return undefined;
18
+ const reader = response.body.getReader();
19
+ const chunks: Uint8Array[] = [];
20
+ let length = 0;
21
+ try {
22
+ while (true) {
23
+ const chunk = await reader.read();
24
+ if (chunk.done) break;
25
+ length += chunk.value.byteLength;
26
+ if (length > 65_536) return undefined;
27
+ chunks.push(chunk.value);
28
+ }
29
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
30
+ } finally { await reader.cancel(); }
31
+ }
32
+
33
+ /** Independent, bounded account reads. A catalog never waits for a native login or a turn. */
34
+ export function createNativeAccountUsageReader(options: Options) {
35
+ const clock = options.clock ?? Date.now;
36
+ const fetcher = options.fetcher ?? fetch;
37
+ const reports = new Map<NativeUsageProvider, ProviderAccountUsage>();
38
+ let pending: Promise<void> | undefined;
39
+ let attemptedAt = -Infinity;
40
+
41
+ async function update(provider: NativeUsageProvider): Promise<void> {
42
+ try {
43
+ const token = await options.credential(provider);
44
+ if (!token) { reports.delete(provider); return; }
45
+ const request = requests[provider];
46
+ const response = await fetcher(request.url, {
47
+ method: request.method, redirect: "error", signal: AbortSignal.timeout(8_000),
48
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json",
49
+ ...(provider === "claude" ? { "anthropic-beta": "oauth-2025-04-20" } : {}),
50
+ ...(provider === "cursor" ? { "Connect-Protocol-Version": "1" } : {}),
51
+ },
52
+ ...(request.method === "POST" ? { body: "{}" } : {}),
53
+ });
54
+ const value = await boundedResponse(response);
55
+ let report = projectNativeAccountUsage(provider, value, clock());
56
+ if (provider === "cursor" && !report && value !== undefined) {
57
+ const legacy = await fetcher("https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
58
+ method: "POST", redirect: "error", signal: AbortSignal.timeout(8_000), body: "{}",
59
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
60
+ });
61
+ report = projectCursorPeriodUsage(await boundedResponse(legacy), clock());
62
+ }
63
+ if (report) reports.set(provider, report);
64
+ else reports.delete(provider);
65
+ } catch { reports.delete(provider); }
66
+ }
67
+
68
+ return {
69
+ current(): ProviderAccountUsage[] {
70
+ const now = clock();
71
+ return [...reports.values()].flatMap(report => {
72
+ const windows = report.windows.filter(window => window.resetsAt > now);
73
+ return now - report.observedAt <= 300_000 && windows.length ? [{ ...report, windows }] : [];
74
+ });
75
+ },
76
+ refresh(): Promise<void> {
77
+ if (pending) return pending;
78
+ if (clock() - attemptedAt < 120_000) return Promise.resolve();
79
+ attemptedAt = clock();
80
+ pending = Promise.allSettled((Object.keys(requests) as NativeUsageProvider[]).map(update))
81
+ .then(() => undefined).finally(() => { pending = undefined; });
82
+ return pending;
83
+ },
84
+ };
85
+ }
@@ -3,9 +3,8 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { AdvertisedModel, EndpointModelCatalog } from "../../../../packages/protocol/schema";
5
5
 
6
- import { readClaudeUsage } from "../provider-account-status/claude-statusline";
7
6
  import { cachedCLIModels } from "./cli-models";
8
- import { cachedProviderAccount } from "../provider-account-status";
7
+ import { cachedNativeAccountUsage, cachedProviderAccount } from "../provider-account-status";
9
8
 
10
9
  const MAX_BYTES = 2_000_000;
11
10
  const MAX_AGE = 24 * 60 * 60_000;
@@ -82,6 +81,6 @@ export function readProviderModelCatalog(endpointId: string): EndpointModelCatal
82
81
  const codex = readCodexModelCatalog(endpointId);
83
82
  const cli = cachedCLIModels(endpointId);
84
83
  return { ...codex, observedAt: Math.max(codex.observedAt, ...cli.map(row => row.observedAt)),
85
- accountUsage: [...(codex.accountUsage ?? []), ...readClaudeUsage()],
84
+ accountUsage: [...(codex.accountUsage ?? []), ...cachedNativeAccountUsage()],
86
85
  stale: codex.stale === true && cli.length === 0, models: [...codex.models, ...cli].slice(0, 256) };
87
86
  }
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
10
10
  Install the supported CLI on this computer, then run:
11
11
 
12
12
  ```bash
13
- npm install -g granttap-mcp@0.8.91
13
+ npm install -g granttap-mcp@0.8.93
14
14
  granttap setup
15
15
  granttap status
16
16
  ```
@@ -4,7 +4,7 @@
4
4
  "command": "node",
5
5
  "args": [
6
6
  "-e",
7
- "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.91\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 22.13.0+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
7
+ "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.93\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 22.13.0+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
8
8
  ]
9
9
  }
10
10
  }
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
7
7
  const { existsSync } = require("node:fs");
8
8
  const { delimiter, dirname, join } = require("node:path");
9
9
 
10
- const PACKAGE = "granttap-mcp@0.8.91";
10
+ const PACKAGE = "granttap-mcp@0.8.93";
11
11
  const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
12
12
  const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
13
13
  ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.91",
3
+ "version": "0.8.93",
4
4
  "description": "Commercial computer runtime connecting local coding agents to GrantTap on Mac, iPhone, iPad and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {