granttap-mcp 0.8.90 → 0.8.92

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.
Files changed (25) hide show
  1. package/apps/bridge/src/host/session-keys.ts +2 -1
  2. package/apps/bridge/src/mesh/knowledge/README.md +8 -0
  3. package/apps/bridge/src/mesh/knowledge/read.ts +65 -0
  4. package/apps/bridge/src/mesh/knowledge/runtime-read.ts +49 -0
  5. package/apps/bridge/src/monitor/handlers/inbound.ts +3 -0
  6. package/apps/bridge/src/policy/governed-projects.ts +1 -1
  7. package/apps/bridge/src/provider-account-status/README.md +21 -4
  8. package/apps/bridge/src/provider-account-status/index.ts +1 -0
  9. package/apps/bridge/src/provider-account-status/native-usage/README.md +12 -0
  10. package/apps/bridge/src/provider-account-status/native-usage/credentials.ts +69 -0
  11. package/apps/bridge/src/provider-account-status/native-usage/index.ts +30 -0
  12. package/apps/bridge/src/provider-account-status/native-usage/projection.ts +52 -0
  13. package/apps/bridge/src/provider-account-status/native-usage/reader.ts +85 -0
  14. package/apps/bridge/src/provider-model-catalog/index.ts +2 -3
  15. package/apps/bridge/src/task-routing/admission/index.ts +2 -1
  16. package/apps/mcp/src/desktop/engine-bridge.ts +1 -0
  17. package/apps/mcp/src/desktop/operations/index.ts +2 -0
  18. package/cursor-plugin/README.md +1 -1
  19. package/cursor-plugin/mcp.json +1 -1
  20. package/cursor-plugin/stdio-bootstrap.cjs +1 -1
  21. package/package.json +2 -2
  22. package/packages/protocol/messages/approval-questions/index.ts +1 -1
  23. package/packages/protocol/messages/mesh/knowledge/refresh.ts +44 -0
  24. package/packages/protocol/messages/sessions/index.ts +2 -2
  25. package/packages/protocol/schema.ts +4 -0
@@ -8,6 +8,7 @@ import type {
8
8
  MeshEvent,
9
9
  MeshSnapshot,
10
10
  MeshInvocationPage,
11
+ ProjectKnowledgePage,
11
12
  Payload,
12
13
  ProjectPolicyPayload,
13
14
  Role,
@@ -53,7 +54,7 @@ export async function sendMeshPayload(
53
54
 
54
55
  export async function sendProjectPayload(
55
56
  client: RelayClient,
56
- payload: ProjectPolicyPayload | MeshClaimReleaseResult | MeshInvocationPage,
57
+ payload: ProjectPolicyPayload | MeshClaimReleaseResult | MeshInvocationPage | ProjectKnowledgePage,
57
58
  to: Role | "all" = "phone",
58
59
  options: SendOptions = {},
59
60
  ): Promise<void> {
@@ -12,3 +12,11 @@ Engine validates its scope. The phone may replay the same ID after a lost reply.
12
12
  This is not a migration of old chats into verified knowledge.
13
13
 
14
14
  GrantTap Commercial Source License.
15
+
16
+ `read.ts` is the bounded Project evidence read: Memory and the invocation journal
17
+ run in parallel with independent deadlines. Calls may belong to older Tasks
18
+ outside the current phone snapshot. A failed source leaves the other source
19
+ available. Whole records are omitted to fit the encrypted frame budget; record
20
+ content is never shortened. `runtime-read.ts` exposes this read over local Mac
21
+ IPC and an authenticated, requester-scoped Project relay response. It does not
22
+ rescan provider chats or rebuild architecture.
@@ -0,0 +1,65 @@
1
+ import type { MeshSnapshot, ProjectKnowledgeRecord } from "../../../../../packages/protocol/schema";
2
+ import { ProjectKnowledgePage, ProjectKnowledgeQuery } from "../../../../../packages/protocol/messages/mesh/knowledge/refresh";
3
+ import type { InvocationHistoryPage } from "../../engine/protocol/engine-invocation-protocol";
4
+ import { activeProjectKnowledge, supersededProjectKnowledgeIds } from "./active";
5
+
6
+ export type KnowledgeReadDependencies = {
7
+ snapshot: (projectId: string) => MeshSnapshot | undefined;
8
+ memory: (projectId: string) => Promise<ProjectKnowledgeRecord[] | undefined>;
9
+ history: (projectId: string) => Promise<InvocationHistoryPage | undefined>;
10
+ endpointId: string;
11
+ now: () => number;
12
+ timeoutMs?: number;
13
+ };
14
+
15
+ export async function readProjectKnowledge(
16
+ query: ProjectKnowledgeQuery, dependencies: KnowledgeReadDependencies,
17
+ ): Promise<ProjectKnowledgePage | undefined> {
18
+ if (!ProjectKnowledgeQuery.safeParse(query).success) return undefined;
19
+ const snapshot = dependencies.snapshot(query.projectId);
20
+ if (!snapshot || snapshot.projectId !== query.projectId) return undefined;
21
+ const timeout = dependencies.timeoutMs ?? 2_000;
22
+ let [memory, history] = await Promise.all([
23
+ boundedSource(() => dependencies.memory(query.projectId), timeout),
24
+ boundedSource(() => dependencies.history(query.projectId), timeout),
25
+ ]);
26
+ if (memory?.some(record => record.projectId !== query.projectId || record.visibility !== "project")) {
27
+ memory = undefined;
28
+ }
29
+ if (history?.events.some(row => row.event.project_id !== query.projectId)) history = undefined;
30
+ const records = memory ?? [];
31
+ const knowledge = activeProjectKnowledge(query.projectId, records)
32
+ .sort((a, b) => b.recordedAt - a.recordedAt).slice(0, 32);
33
+ const invocations = (history?.events ?? []).slice(-32);
34
+ const page: ProjectKnowledgePage = {
35
+ type: "mesh.knowledge.page", sessionId: query.projectId, projectId: query.projectId,
36
+ requestId: query.requestId, sourceEndpointId: dependencies.endpointId,
37
+ memoryAvailability: memory ? "ready" : "unavailable",
38
+ invocationAvailability: history ? "ready" : "unavailable",
39
+ knowledge, invocations,
40
+ supersededKnowledgeRecordIds: supersededProjectKnowledgeIds(query.projectId, records),
41
+ incomplete: records.length > knowledge.length || history?.has_older === true
42
+ || history?.has_more === true,
43
+ generatedAt: dependencies.now(),
44
+ };
45
+ // Keep encrypted frames below the transport limit without shortening a fact.
46
+ while (Buffer.byteLength(JSON.stringify(page)) > 128 * 1_024
47
+ && (knowledge.length > 0 || invocations.length > 0)) {
48
+ const oldestRecord = knowledge.at(-1)?.recordedAt ?? Infinity;
49
+ const oldestCall = invocations[0]?.event.occurred_at ?? Infinity;
50
+ if (oldestRecord <= oldestCall) knowledge.pop();
51
+ else invocations.shift();
52
+ page.incomplete = true;
53
+ }
54
+ return ProjectKnowledgePage.parse(page);
55
+ }
56
+
57
+ async function boundedSource<T>(read: () => Promise<T>, timeoutMs: number): Promise<T | undefined> {
58
+ let timer: ReturnType<typeof setTimeout> | undefined;
59
+ try {
60
+ return await Promise.race([Promise.resolve().then(read), new Promise<undefined>(resolve => {
61
+ timer = setTimeout(() => resolve(undefined), timeoutMs);
62
+ })]);
63
+ } catch { return undefined; }
64
+ finally { if (timer) clearTimeout(timer); }
65
+ }
@@ -0,0 +1,49 @@
1
+ import { join } from "node:path";
2
+ import type { RelayClient } from "../../../../../packages/core/relay-client";
3
+ import { ProjectKnowledgeQuery } from "../../../../../packages/protocol/messages/mesh/knowledge/refresh";
4
+ import { configDir } from "../../config/runtime/paths";
5
+ import { EngineClient } from "../../engine/runtime/engine-client";
6
+ import { engineFeatureEnabled } from "../../engine/runtime/engine-supervisor";
7
+ import { projectKnowledge } from "../../engine/runtime/engine-memory";
8
+ import { sendProjectPayload } from "../../host/session-keys";
9
+ import { computerId } from "../identity/computer";
10
+ import { localMeshStore } from "../local-remote/local";
11
+ import { readProjectKnowledge } from "./read";
12
+
13
+ const engine = new EngineClient({ socketPath: join(configDir(), "engine.sock") });
14
+
15
+ export async function currentProjectKnowledge(query: ProjectKnowledgeQuery) {
16
+ return readProjectKnowledge(query, {
17
+ snapshot: projectId => localMeshStore().snapshot(projectId),
18
+ endpointId: computerId(), now: Date.now,
19
+ memory: projectId => projectKnowledge(projectId),
20
+ history: async projectId => {
21
+ if (!engineFeatureEnabled()) return undefined;
22
+ const result = await engine.request({ operation: "invocation.history",
23
+ input: { project_id: projectId, tail: true, limit: 32 } }, { timeoutMs: 1_000 });
24
+ return result.operation === "invocation.history" ? result.page : undefined;
25
+ },
26
+ });
27
+ }
28
+
29
+ export async function handleProjectKnowledgeQuery(
30
+ client: RelayClient, query: ProjectKnowledgeQuery, peerPublicKey?: string,
31
+ ): Promise<boolean> {
32
+ if (!peerPublicKey) return false;
33
+ const page = await currentProjectKnowledge(query);
34
+ if (!page) return false;
35
+ if (page.memoryAvailability === "ready") {
36
+ localMeshStore().cacheKnowledge(page.projectId, page.knowledge);
37
+ }
38
+ await sendProjectPayload(client, page, "phone", {
39
+ peerPublicKey, ttlMs: 15_000, reliable: false,
40
+ }).catch(() => {});
41
+ return true;
42
+ }
43
+
44
+ export async function desktopProjectKnowledge(input: unknown) {
45
+ const value = input as { project_id?: unknown; request_id?: unknown } | undefined;
46
+ const query = ProjectKnowledgeQuery.safeParse({ type: "mesh.knowledge.query",
47
+ projectId: value?.project_id, sessionId: value?.project_id, requestId: value?.request_id });
48
+ return query.success ? currentProjectKnowledge(query.data) : undefined;
49
+ }
@@ -18,6 +18,7 @@ import {
18
18
  } from "../../mesh/runtime";
19
19
  import { releaseClaimByPerson, releaseResult } from "../../mesh/admin";
20
20
  import { handleInvocationQuery } from "../../engine/invocation/query";
21
+ import { handleProjectKnowledgeQuery } from "../../mesh/knowledge/runtime-read";
21
22
  import { applyPairingJoin } from "../../pairing";
22
23
  import { recordPhoneSeen } from "../../pairing/presence";
23
24
  import { handleProjectPolicySet } from "../../project-policy/runtime";
@@ -123,6 +124,8 @@ export async function handleMonitorMessage(
123
124
  const accepted = requestProjectCapability(payload);
124
125
  if (accepted) void publish().catch(() => {});
125
126
  return accepted;
127
+ } else if (payload.type === "mesh.knowledge.query" && loadRuntimeConfig().meshEnabled) {
128
+ return handleProjectKnowledgeQuery(client, payload, input.peerPublicKey);
126
129
  } else if (payload.type === "mesh.invocation.query" && loadRuntimeConfig().meshEnabled) {
127
130
  return handleInvocationQuery(client, payload);
128
131
  } else if ((payload.type === "mesh.event" || payload.type === "mesh.snapshot")
@@ -17,7 +17,7 @@ const MAX_PROJECTS = 256;
17
17
 
18
18
  const GovernedFile = z.object({
19
19
  version: z.literal(1),
20
- projects: z.record(z.object({
20
+ projects: z.record(z.string(), z.object({
21
21
  revision: z.number().int().nonnegative(),
22
22
  at: z.number().nonnegative(),
23
23
  })),
@@ -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
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { TaskRoutingNeeds } from '../../../../../packages/protocol/task-resources';
2
3
  import { loadConfig, loadRuntimeConfig, machineConfigPath } from '../../config';
3
4
  import { applyNetworkRoute } from '../../device-network/settings';
4
5
  import { scanSessions } from '../../sessions';
@@ -14,7 +15,7 @@ import { observedRoutingSession } from '../observed-turn';
14
15
 
15
16
  export const OffloadInput = z.object({ action: z.enum(['request', 'cancel', 'status']).optional(),
16
17
  operationId: z.string().trim().min(1).max(128).optional(),
17
- requirements: RoutingRequirementsInput.partial().extend({ reason: RoutingRequirementsInput.shape.reason }).optional() }).strict();
18
+ requirements: TaskRoutingNeeds.partial().extend({ reason: RoutingRequirementsInput.shape.reason }).optional() }).strict();
18
19
  export type OffloadArguments = z.infer<typeof OffloadInput>;
19
20
  type Proof = { sessionId: string; provider: RoutingRequestSource['provider'] };
20
21
  type Ports = { store: RoutingRequests; enabled: () => boolean;
@@ -21,6 +21,7 @@ export const DESKTOP_OPERATIONS = new Set([
21
21
  "desktop.machine_load", "desktop.capability_usage", "desktop.task_image", "desktop.task_workspace",
22
22
  "desktop.installed_skills", "desktop.policy_status", "desktop.policy_set", "desktop.project_auto_accept",
23
23
  "desktop.mesh_snapshots",
24
+ "desktop.project_knowledge",
24
25
  "desktop.task_send", "desktop.task_queue", "desktop.transfer_continue", "desktop.task_control", "desktop.task_preferences",
25
26
  "desktop.task_handoff",
26
27
  "desktop.project_transport",
@@ -20,6 +20,7 @@ import { desktopTaskHandoff } from "../delivery/task-handoff";
20
20
  import { desktopTaskCreate } from "../delivery/task-create";
21
21
  import { desktopMeshCreate } from "../delivery/mesh-create";
22
22
  import { desktopInvocationHistory } from "../invocation";
23
+ import { desktopProjectKnowledge } from "../../../../bridge/src/mesh/knowledge/runtime-read";
23
24
  import { desktopUsage } from "../projection/usage";
24
25
  import { desktopInstalledSkills } from "../projection/installed-skills";
25
26
  import type { DesktopControllerEnrollment } from "../pairing";
@@ -74,6 +75,7 @@ export function desktopReadOperation(input: {
74
75
  if (operation === "desktop.invocation_history") {
75
76
  return desktopInvocationHistory(queryInput, engine, storePath);
76
77
  }
78
+ if (operation === "desktop.project_knowledge") return desktopProjectKnowledge(queryInput);
77
79
  if (operation === "desktop.project") {
78
80
  return Promise.resolve(desktopMeshProject(queryInput, storePath));
79
81
  }
@@ -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.90
13
+ npm install -g granttap-mcp@0.8.92
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.90\";\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.92\";\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.90";
10
+ const PACKAGE = "granttap-mcp@0.8.92";
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.90",
3
+ "version": "0.8.92",
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": {
@@ -42,7 +42,7 @@
42
42
  "tsx": "^4.23.4",
43
43
  "tweetnacl": "^1.0.3",
44
44
  "ws": "^8.18.0",
45
- "zod": "^3.24.1"
45
+ "zod": "4.6.7"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/express": "^5.0.3",
@@ -15,7 +15,7 @@ export function questionAnswers(questions: z.infer<typeof ApprovalQuestions> | u
15
15
  note: string | null | undefined): Record<string, string> | undefined {
16
16
  if (!questions || !note || note.length > 20_000) return undefined;
17
17
  try {
18
- const answers = z.record(z.string().trim().min(1).max(4_000)).parse(JSON.parse(note));
18
+ const answers = z.record(z.string(), z.string().trim().min(1).max(4_000)).parse(JSON.parse(note));
19
19
  if (Object.keys(answers).length !== questions.length
20
20
  || questions.some(item => !Object.hasOwn(answers, item.question))) return undefined;
21
21
  return answers;
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { Identifier } from "../endpoint";
3
+ import { MeshInvocationEvent } from "../invocations";
4
+ import { ProjectKnowledgeRecord } from "./index";
5
+
6
+ const Sequence = z.number().int().nonnegative().safe();
7
+ export const ProjectKnowledgeQuery = z.object({
8
+ type: z.literal("mesh.knowledge.query"),
9
+ sessionId: Identifier, projectId: Identifier, requestId: Identifier,
10
+ }).strict().refine(value => value.sessionId === value.projectId, "Project scope mismatch");
11
+ export type ProjectKnowledgeQuery = z.infer<typeof ProjectKnowledgeQuery>;
12
+
13
+ /** One bounded Project read; no architecture rebuild or per-Task fan-out. */
14
+ export const ProjectKnowledgePage = z.object({
15
+ type: z.literal("mesh.knowledge.page"),
16
+ sessionId: Identifier, projectId: Identifier, requestId: Identifier,
17
+ sourceEndpointId: Identifier,
18
+ memoryAvailability: z.enum(["ready", "unavailable"]),
19
+ invocationAvailability: z.enum(["ready", "unavailable"]),
20
+ knowledge: z.array(ProjectKnowledgeRecord).max(32),
21
+ supersededKnowledgeRecordIds: z.array(Identifier).max(128),
22
+ invocations: z.array(z.object({ sequence: Sequence, event: MeshInvocationEvent }).strict()).max(32),
23
+ incomplete: z.boolean(), generatedAt: Sequence,
24
+ }).strict().superRefine((value, ctx) => {
25
+ let previous = 0;
26
+ const seenCalls = new Set<string>();
27
+ const orderedCalls = value.invocations.every(row => {
28
+ const valid = row.sequence > previous && !seenCalls.has(row.event.event_id);
29
+ previous = row.sequence;
30
+ seenCalls.add(row.event.event_id);
31
+ return valid;
32
+ });
33
+ if (!orderedCalls
34
+ || new Set(value.knowledge.map(row => row.recordId)).size !== value.knowledge.length
35
+ || new Set(value.supersededKnowledgeRecordIds).size !== value.supersededKnowledgeRecordIds.length
36
+ || value.sessionId !== value.projectId
37
+ || value.knowledge.some(record => record.projectId !== value.projectId || record.visibility !== "project")
38
+ || value.invocations.some(row => row.event.project_id !== value.projectId)
39
+ || value.memoryAvailability === "unavailable" && value.knowledge.length > 0
40
+ || value.invocationAvailability === "unavailable" && value.invocations.length > 0) {
41
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Knowledge source or Project scope mismatch" });
42
+ }
43
+ });
44
+ export type ProjectKnowledgePage = z.infer<typeof ProjectKnowledgePage>;
@@ -149,8 +149,8 @@ export const SessionsStatus = z.object({
149
149
  gatingEnabled: z.boolean().optional(),
150
150
  excludedSessions: z.array(z.string()).optional(),
151
151
  autoAcceptDefault: AutoAcceptLevel.optional(),
152
- autoAcceptBySession: z.record(AutoAcceptLevel).optional(),
153
- autoAcceptByProject: z.record(AutoAcceptLevel).optional(),
152
+ autoAcceptBySession: z.record(z.string(), AutoAcceptLevel).optional(),
153
+ autoAcceptByProject: z.record(z.string(), AutoAcceptLevel).optional(),
154
154
  autoAcceptPaused: z.boolean().optional(),
155
155
  providerSettings: ProviderRuntimeSettings.optional(),
156
156
  meshEnabled: z.boolean().optional(),
@@ -45,6 +45,7 @@ import { PairingJoin } from "./messages/pairing-join";
45
45
  import { ControllerPairOffer, ControllerPairRequest } from "./messages/device/controller-pairing";
46
46
  import { AccountMachineLink } from "./messages/device/account-machine-link";
47
47
  import { KnowledgeWrite, KnowledgeWriteResult } from "./messages/mesh/knowledge/write";
48
+ import { ProjectKnowledgeQuery, ProjectKnowledgePage } from "./messages/mesh/knowledge/refresh";
48
49
  import { MeshInvocationPage, MeshInvocationQuery } from "./messages/mesh/invocations";
49
50
  import {
50
51
  ProjectPolicyAck,
@@ -82,6 +83,7 @@ export * from "./messages/pairing-join";
82
83
  export * from "./messages/device/controller-pairing";
83
84
  export * from "./messages/device/account-machine-link";
84
85
  export * from "./messages/mesh/knowledge/write";
86
+ export * from "./messages/mesh/knowledge/refresh";
85
87
  export * from "./messages/mesh";
86
88
  export * from "./messages/mesh/invocations";
87
89
  export * from "./messages/primitives";
@@ -165,6 +167,8 @@ export const Payload = z.union([
165
167
  ProjectCapabilityRequestSet,
166
168
  MeshInvocationQuery,
167
169
  MeshInvocationPage,
170
+ ProjectKnowledgeQuery,
171
+ ProjectKnowledgePage,
168
172
  MeshEndpointPolicy,
169
173
  ProjectPolicySet,
170
174
  ProjectPolicyStatus,