granttap-mcp 0.8.116 → 0.8.118
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/apps/bridge/src/capabilities/README.md +4 -0
- package/apps/bridge/src/capabilities/descriptors.ts +23 -1
- package/apps/bridge/src/capabilities/index.ts +2 -0
- package/apps/bridge/src/codex-questions/README.md +6 -0
- package/apps/bridge/src/codex-questions/index.ts +21 -5
- package/apps/bridge/src/codex-questions/records.ts +1 -0
- package/apps/bridge/src/mesh/catalog/index.ts +8 -4
- package/apps/bridge/src/mesh/catalog/monitor-inspection/README.md +14 -0
- package/apps/bridge/src/mesh/catalog/monitor-inspection/index.ts +48 -0
- package/apps/bridge/src/mesh/catalog/repository-inspection.ts +9 -3
- package/apps/bridge/src/mesh/runtime/index.ts +5 -5
- package/apps/bridge/src/monitor/handlers/inbound.ts +5 -1
- package/apps/bridge/src/monitor/support/runtime.ts +12 -6
- package/apps/bridge/src/sessions/native-source.ts +1 -1
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
- package/packages/protocol/messages/interaction/index.ts +1 -0
|
@@ -18,3 +18,7 @@ do not borrow an implementation's description or icon. These are display data,
|
|
|
18
18
|
not instructions or evidence that a capability was used.
|
|
19
19
|
|
|
20
20
|
License: this module is distributed under the GrantTap Commercial Source License in the repository-root `LICENSE` file.
|
|
21
|
+
|
|
22
|
+
Catalog publication warms native Codex MCP configuration asynchronously through
|
|
23
|
+
`refreshCodexMcpDescriptors`, preserving its existing retry window and last known
|
|
24
|
+
rows while allowing request-delivery callbacks to run during a slow native query.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -65,6 +65,28 @@ function codexMcpDescriptors(): McpDescriptor[] {
|
|
|
65
65
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
let codexRefresh: Promise<void> | undefined;
|
|
69
|
+
|
|
70
|
+
/** Warm native configuration asynchronously before a catalog consumes the synchronous projection. */
|
|
71
|
+
export function refreshCodexMcpDescriptors(): Promise<void> {
|
|
72
|
+
if (codexCache && Date.now() - codexCache.at < CACHE_MS) return Promise.resolve();
|
|
73
|
+
if (codexRefresh) return codexRefresh;
|
|
74
|
+
codexRefresh = new Promise<void>(resolve => {
|
|
75
|
+
const finish = (error: Error | null, output = '') => {
|
|
76
|
+
let rows = codexCache?.rows ?? [];
|
|
77
|
+
if (!error) {
|
|
78
|
+
try { const parsed = JSON.parse(output); if (Array.isArray(parsed)) rows = parsed; } catch { /* Keep last known rows. */ }
|
|
79
|
+
}
|
|
80
|
+
codexCache = { at: Date.now(), rows };
|
|
81
|
+
resolve();
|
|
82
|
+
};
|
|
83
|
+
try { execFile(resolveCodexBinary(), ['mcp', 'list', '--json'],
|
|
84
|
+
{ encoding: 'utf8', timeout: 15_000, maxBuffer: 4 * 1024 * 1024 }, finish); }
|
|
85
|
+
catch { finish(new Error('Native configuration is unavailable.')); }
|
|
86
|
+
}).finally(() => { codexRefresh = undefined; });
|
|
87
|
+
return codexRefresh;
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
function codexMcpRows(): CodexMcpRow[] {
|
|
69
91
|
if (codexCache && Date.now() - codexCache.at < CACHE_MS) return codexCache.rows;
|
|
70
92
|
try {
|
|
@@ -32,3 +32,9 @@ answer, image and file payload together; ordinary encrypted attachment transport
|
|
|
32
32
|
Task policy, native-owner admission and stable question idempotency still apply.
|
|
33
33
|
Legacy text-only replies keep their existing contract. Files remain available
|
|
34
34
|
for the admitted native turn; another phone cannot start a second answer.
|
|
35
|
+
|
|
36
|
+
Notification refresh can name one request through `sessions.refresh.notificationRequestId`.
|
|
37
|
+
Recovery checks the private request's owning room, restores a discovered source only
|
|
38
|
+
inside the native Codex sessions root, and sends that exact question or its terminal
|
|
39
|
+
state through the priority device channel. It does not require an admitted catalog,
|
|
40
|
+
start a provider turn, send another wake notification, or schedule a full scan.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RelayClient } from '../../../../packages/core/relay-client';
|
|
2
2
|
import type { SessionInfo, UserMessage } from '../../../../packages/protocol/schema';
|
|
3
|
-
import { nativeTranscriptSource } from '../sessions';
|
|
3
|
+
import { nativeTranscriptSource, restoreNativeTranscriptSource } from '../sessions';
|
|
4
4
|
import { handleInboundUserMessage } from '../monitor/message-delivery';
|
|
5
5
|
import { sendDeliveryReceipt } from '../monitor/handlers/receipts';
|
|
6
6
|
import { nativeAnswerEnvelope } from './observations';
|
|
@@ -12,7 +12,7 @@ const RETENTION_MS = 24 * 60 * 60_000;
|
|
|
12
12
|
|
|
13
13
|
/** Phone questions travel as device-encrypted control events, independently of task-key grants. */
|
|
14
14
|
export async function publishCodexQuestions(client: RelayClient, sessions: SessionInfo[],
|
|
15
|
-
options: { replay?: boolean; peerPublicKey?: string } = {}): Promise<void> {
|
|
15
|
+
options: { replay?: boolean; peerPublicKey?: string; requestId?: string } = {}): Promise<void> {
|
|
16
16
|
for (const session of sessions.filter(s => s.agent === 'codex')) {
|
|
17
17
|
const path = nativeTranscriptSource(session);
|
|
18
18
|
if (!path) continue;
|
|
@@ -22,9 +22,11 @@ export async function publishCodexQuestions(client: RelayClient, sessions: Sessi
|
|
|
22
22
|
for (const question of observed.questions) {
|
|
23
23
|
if (Date.now() - question.createdAt > RETENTION_MS) continue;
|
|
24
24
|
const existing = readQuestion(question.requestId);
|
|
25
|
-
if (!existing) saveQuestion({ ...question, room: client.room });
|
|
25
|
+
if (!existing) saveQuestion({ ...question, room: client.room, sourcePath: path });
|
|
26
|
+
else if (existing.room === client.room && existing.sourcePath !== path) saveQuestion({ ...existing, sourcePath: path });
|
|
26
27
|
}
|
|
27
|
-
const records = storedQuestions().filter(q => q.room === client.room && q.sessionId === session.sessionId
|
|
28
|
+
const records = storedQuestions().filter(q => q.room === client.room && q.sessionId === session.sessionId
|
|
29
|
+
&& (!options.requestId || q.requestId === options.requestId))
|
|
28
30
|
.sort((a, b) => a.createdAt - b.createdAt || a.index - b.index);
|
|
29
31
|
for (const record of records) {
|
|
30
32
|
const expired = Date.now() - record.createdAt > RETENTION_MS;
|
|
@@ -44,7 +46,7 @@ export async function publishCodexQuestions(client: RelayClient, sessions: Sessi
|
|
|
44
46
|
sessionId: session.sessionId, agent: 'codex', projectId: session.projectId,
|
|
45
47
|
taskId: session.taskId, computerId: session.computerId, text, createdAt: record.createdAt },
|
|
46
48
|
'phone', { ttlMs: Math.max(1, record.createdAt + RETENTION_MS - Date.now()),
|
|
47
|
-
wake: !record.announced, peerPublicKey: options.peerPublicKey,
|
|
49
|
+
wake: !record.announced, urgent: true, peerPublicKey: options.peerPublicKey,
|
|
48
50
|
deliveryId: 'native-question-' + record.requestId });
|
|
49
51
|
saveQuestion({ ...record, announced: true });
|
|
50
52
|
}
|
|
@@ -52,6 +54,20 @@ export async function publishCodexQuestions(client: RelayClient, sessions: Sessi
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/** Restore one notification from private owned records; catalog discovery is not a prerequisite. */
|
|
58
|
+
export async function replayCodexQuestion(client: RelayClient, requestId: string, peerPublicKey?: string): Promise<boolean> {
|
|
59
|
+
if (!peerPublicKey) return false;
|
|
60
|
+
const record = readQuestion(requestId);
|
|
61
|
+
if (!record || record.room !== client.room) return false;
|
|
62
|
+
const identity = { sessionId: record.sessionId, agent: 'codex' as const };
|
|
63
|
+
if (!nativeTranscriptSource(identity)
|
|
64
|
+
&& (!record.sourcePath || !restoreNativeTranscriptSource(identity, record.sourcePath))) return false;
|
|
65
|
+
const session: SessionInfo = { ...identity, state: 'idle', startedAt: record.createdAt,
|
|
66
|
+
lastActivityAt: record.createdAt, tokensSession: 0, tokensLastTurn: 0 };
|
|
67
|
+
await publishCodexQuestions(client, [session], { replay: true, peerPublicKey, requestId });
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
/** Reuse ordinary scoped delivery, policy and receipts; only the human's explicit answer is wrapped. */
|
|
56
72
|
export async function handleCodexQuestionReply(client: RelayClient, message: UserMessage,
|
|
57
73
|
publish: () => Promise<void>, peerPublicKey?: string,
|
|
@@ -7,6 +7,7 @@ import { questionId } from './observations';
|
|
|
7
7
|
const Record = z.object({ requestId: z.string(), sessionId: z.string(), callId: z.string(),
|
|
8
8
|
index: z.number().int().min(0).max(3), question: z.string().max(2_000),
|
|
9
9
|
options: z.array(z.string().max(512)).max(8), createdAt: z.number(), room: z.string(),
|
|
10
|
+
sourcePath: z.string().min(1).max(4096).optional(),
|
|
10
11
|
announced: z.boolean().optional(), answered: z.boolean().optional(), resolvedSent: z.boolean().optional() });
|
|
11
12
|
export type QuestionRecord = z.infer<typeof Record>;
|
|
12
13
|
const directory = () => join(configDir(), 'codex-question-records');
|
|
@@ -20,7 +20,10 @@ import type { MeshStore } from "../store";
|
|
|
20
20
|
import { queueProjectBindingSync } from "../../engine/runtime/engine-projects";
|
|
21
21
|
import { isCursorTaskCloneId } from "../../sessions/cursor/catalog";
|
|
22
22
|
import { localTaskTransfers } from '../../task-transfer';
|
|
23
|
-
export { inspectRepositoryAsync } from "./repository-inspection";
|
|
23
|
+
export { inspectRepositoryAsync, inspectWorkingTreeAsync } from "./repository-inspection";
|
|
24
|
+
export { prepareMonitorInspection } from "./monitor-inspection";
|
|
25
|
+
import type { CatalogInspection } from "./monitor-inspection";
|
|
26
|
+
export type { CatalogInspection } from "./monitor-inspection";
|
|
24
27
|
|
|
25
28
|
export type RepositoryFacts = {
|
|
26
29
|
root: string;
|
|
@@ -120,7 +123,7 @@ export function linkSessionsToProjects(
|
|
|
120
123
|
store: MeshStore,
|
|
121
124
|
sessions: SessionInfo[],
|
|
122
125
|
computerId: string,
|
|
123
|
-
inspect: (cwd: string) => RepositoryFacts = inspectRepository,
|
|
126
|
+
inspect: ((cwd: string) => RepositoryFacts) | CatalogInspection = inspectRepository,
|
|
124
127
|
formerNames: string[] = formerComputerNames(),
|
|
125
128
|
): SessionInfo[] {
|
|
126
129
|
localTaskTransfers().reconcileCatalogOwnership(store, computerId);
|
|
@@ -143,13 +146,14 @@ export function linkSessionsToProjects(
|
|
|
143
146
|
const repositories = new Map<string, RepositoryFacts>();
|
|
144
147
|
const workingTrees = new Map<string, boolean | undefined>();
|
|
145
148
|
const inspectWorkingTree = (cwd: string): boolean | undefined => {
|
|
146
|
-
if (!workingTrees.has(cwd)) workingTrees.set(cwd,
|
|
149
|
+
if (!workingTrees.has(cwd)) workingTrees.set(cwd, typeof inspect === "function"
|
|
150
|
+
? hasUncommittedWork(cwd) : inspect.workingTree(cwd));
|
|
147
151
|
return workingTrees.get(cwd);
|
|
148
152
|
};
|
|
149
153
|
const inspectOnce = (cwd: string): RepositoryFacts => {
|
|
150
154
|
let facts = repositories.get(cwd);
|
|
151
155
|
if (!facts) {
|
|
152
|
-
facts = inspect(cwd);
|
|
156
|
+
facts = typeof inspect === "function" ? inspect(cwd) : inspect.repository(cwd);
|
|
153
157
|
repositories.set(cwd, facts);
|
|
154
158
|
}
|
|
155
159
|
return facts;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Monitor repository inspection
|
|
2
|
+
|
|
3
|
+
Public entry: `prepareMonitorInspection` through the catalog entry point. The
|
|
4
|
+
monitor prepares Git observations asynchronously with the existing two-command
|
|
5
|
+
pool. Each catalog reads uncommitted work afresh. Only stable repository metadata
|
|
6
|
+
is reused for at most 30 seconds; missing repositories retry after five seconds.
|
|
7
|
+
An explicit refresh bypasses reuse. Retired directories are removed, older
|
|
8
|
+
in-flight results cannot replace a forced refresh, and cache size is bounded.
|
|
9
|
+
|
|
10
|
+
These are catalog observations. Explicit Git and Task transfer operations retain
|
|
11
|
+
their fresh native inspection and never use this observation cache as authority.
|
|
12
|
+
Tests cover fresh unsaved work, forced refresh, expiry, retirement and ordering.
|
|
13
|
+
|
|
14
|
+
License: GrantTap Commercial Source License in the repository LICENSE.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { inspectRepositoryAsync, inspectWorkingTreeAsync } from '../repository-inspection';
|
|
2
|
+
import { inspectRepository, hasUncommittedWork, type RepositoryFacts } from '..';
|
|
3
|
+
|
|
4
|
+
type Readers = { repository: (cwd: string) => Promise<RepositoryFacts>;
|
|
5
|
+
workingTree: (cwd: string) => Promise<boolean | undefined>; now: () => number };
|
|
6
|
+
export type CatalogInspection = { repository: (cwd: string) => RepositoryFacts;
|
|
7
|
+
workingTree: (cwd: string) => boolean | undefined };
|
|
8
|
+
type Observation = { facts: RepositoryFacts; observedAt: number; expiresAt: number };
|
|
9
|
+
|
|
10
|
+
/** Observation only. Explicit Git and transfer operations continue to inspect native state freshly. */
|
|
11
|
+
export function createMonitorInspection(readers: Readers = {
|
|
12
|
+
repository: inspectRepositoryAsync, workingTree: inspectWorkingTreeAsync, now: Date.now,
|
|
13
|
+
}) {
|
|
14
|
+
const metadata = new Map<string, Observation>();
|
|
15
|
+
const pending = new Map<string, Promise<RepositoryFacts>>();
|
|
16
|
+
let admitted = new Set<string>();
|
|
17
|
+
const repository = (cwd: string, force: boolean): Promise<RepositoryFacts> => {
|
|
18
|
+
const cached = metadata.get(cwd);
|
|
19
|
+
if (!force && cached && readers.now() >= cached.observedAt && readers.now() < cached.expiresAt) return Promise.resolve(cached.facts);
|
|
20
|
+
const active = pending.get(cwd);
|
|
21
|
+
if (!force && active) return active;
|
|
22
|
+
const next = readers.repository(cwd).then(facts => {
|
|
23
|
+
const observed = Object.freeze({ ...facts }), now = readers.now();
|
|
24
|
+
if (pending.get(cwd) === next && admitted.has(cwd) && (metadata.has(cwd) || metadata.size < 512)) {
|
|
25
|
+
metadata.set(cwd, { facts: observed, observedAt: now, expiresAt: now + (facts.worktree ? 30_000 : 5_000) });
|
|
26
|
+
}
|
|
27
|
+
return observed;
|
|
28
|
+
}).finally(() => { if (pending.get(cwd) === next) pending.delete(cwd); });
|
|
29
|
+
pending.set(cwd, next);
|
|
30
|
+
return next;
|
|
31
|
+
};
|
|
32
|
+
return async (directories: string[], force = false): Promise<CatalogInspection> => {
|
|
33
|
+
admitted = new Set(directories);
|
|
34
|
+
for (const cwd of metadata.keys()) if (!admitted.has(cwd)) metadata.delete(cwd);
|
|
35
|
+
const facts = new Map(await Promise.all([...admitted].map(async cwd => [cwd, await repository(cwd, force)] as const)));
|
|
36
|
+
const roots = new Set([...facts.values()].flatMap(value => value.worktree ? [value.worktree] : []));
|
|
37
|
+
const workingTrees = new Map(await Promise.all([...roots].map(async root => [root, await readers.workingTree(root)] as const)));
|
|
38
|
+
const values = new Map([...facts].map(([cwd, value]) => [cwd, { repository: value,
|
|
39
|
+
workingTree: value.worktree ? workingTrees.get(value.worktree) : undefined }] as const));
|
|
40
|
+
for (const value of [...values.values()]) {
|
|
41
|
+
if (value.repository.worktree && !values.has(value.repository.worktree)) values.set(value.repository.worktree, value);
|
|
42
|
+
}
|
|
43
|
+
return { repository: cwd => values.get(cwd)?.repository ?? inspectRepository(cwd),
|
|
44
|
+
workingTree: cwd => values.has(cwd) ? values.get(cwd)!.workingTree : hasUncommittedWork(cwd) };
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const prepareMonitorInspection = createMonitorInspection();
|
|
@@ -7,18 +7,18 @@ const waiting: Array<() => void> = [];
|
|
|
7
7
|
const pending = new Map<string, Promise<RepositoryFacts>>();
|
|
8
8
|
|
|
9
9
|
/** Native read-only Git work stays bounded without blocking relay callbacks. */
|
|
10
|
-
function git(cwd: string, args: string[]): Promise<string | undefined> {
|
|
10
|
+
function git(cwd: string, args: string[], status = false): Promise<string | undefined> {
|
|
11
11
|
return new Promise(resolve => {
|
|
12
12
|
const run = () => {
|
|
13
13
|
running++;
|
|
14
14
|
const done = (error: Error | null, stdout = "") => {
|
|
15
15
|
running--;
|
|
16
16
|
waiting.shift()?.();
|
|
17
|
-
resolve(error ? undefined : stdout.trim() || undefined);
|
|
17
|
+
resolve(error ? undefined : status ? stdout : stdout.trim() || undefined);
|
|
18
18
|
};
|
|
19
19
|
try {
|
|
20
20
|
execFile("git", ["-C", cwd, ...args], {
|
|
21
|
-
encoding: "utf8", timeout: 2_000, maxBuffer: 1024 * 1024, windowsHide: true,
|
|
21
|
+
encoding: "utf8", timeout: status ? 5_000 : 2_000, maxBuffer: (status ? 4 : 1) * 1024 * 1024, windowsHide: true,
|
|
22
22
|
}, done);
|
|
23
23
|
} catch { done(new Error("Repository command could not start")); }
|
|
24
24
|
};
|
|
@@ -47,3 +47,9 @@ export function inspectRepositoryAsync(cwd: string): Promise<RepositoryFacts> {
|
|
|
47
47
|
pending.set(cwd, next);
|
|
48
48
|
return next;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
/** A fresh working-tree observation shares the bounded, nonblocking Git command pool. */
|
|
52
|
+
export async function inspectWorkingTreeAsync(cwd: string): Promise<boolean | undefined> {
|
|
53
|
+
const output = await git(cwd, ['status', '--porcelain=v1', '--untracked-files=normal', '-z'], true);
|
|
54
|
+
return output === undefined ? undefined : output.length > 0;
|
|
55
|
+
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from "../../reply";
|
|
22
22
|
import { isProviderEnabled } from "../../config/runtime";
|
|
23
23
|
import { sendMeshPayload } from "../../host/session-keys";
|
|
24
|
-
import { linkSessionsToProjects } from "../catalog";
|
|
24
|
+
import { linkSessionsToProjects, type CatalogInspection } from "../catalog";
|
|
25
25
|
import { catalogFromEndpoint } from "../catalog/models";
|
|
26
26
|
import { saveProjectCapabilityRequest } from "../catalog/project/requests";
|
|
27
27
|
import { computerId } from "../identity/computer";
|
|
@@ -84,8 +84,8 @@ const defaultDependencies: MeshRuntimeDependencies = {
|
|
|
84
84
|
export function createMeshRuntime(deps: MeshRuntimeDependencies) {
|
|
85
85
|
const flow = createHandoffFlow(deps);
|
|
86
86
|
const { capsulePrompt, acceptHandoff, answerAgentQuestion } = flow;
|
|
87
|
-
const catalog = (sessions: SessionInfo[]) =>
|
|
88
|
-
linkSessionsToProjects(deps.store(), sessions, deps.computer());
|
|
87
|
+
const catalog = (sessions: SessionInfo[], inspection?: CatalogInspection) =>
|
|
88
|
+
linkSessionsToProjects(deps.store(), sessions, deps.computer(), inspection);
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
return {
|
|
@@ -165,8 +165,8 @@ export function createLocalMeshRuntime(overrides: Partial<MeshRuntimeDependencie
|
|
|
165
165
|
|
|
166
166
|
const defaultRuntime = createLocalMeshRuntime();
|
|
167
167
|
|
|
168
|
-
export function meshCatalog(sessions: SessionInfo[]): SessionInfo[] {
|
|
169
|
-
return defaultRuntime.catalog(sessions);
|
|
168
|
+
export function meshCatalog(sessions: SessionInfo[], inspection?: CatalogInspection): SessionInfo[] {
|
|
169
|
+
return defaultRuntime.catalog(sessions, inspection);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
export function meshSnapshots(): MeshSnapshot[] {
|
|
@@ -47,7 +47,7 @@ import { handleControllerPairRequest, handleInboundKnowledgeWrite } from "../con
|
|
|
47
47
|
import { acceptPhoneAccountLink } from "../../account-recovery/phone-link";
|
|
48
48
|
import { approvalsStatus } from '../../approvals/state';
|
|
49
49
|
import { handleDeliveryReceiptQuery } from '../message-delivery/status-query';
|
|
50
|
-
import { handleCodexQuestionReply, publishCodexQuestions } from '../../codex-questions';
|
|
50
|
+
import { handleCodexQuestionReply, publishCodexQuestions, replayCodexQuestion } from '../../codex-questions';
|
|
51
51
|
import { publishedCatalog } from '../../session-catalog';
|
|
52
52
|
|
|
53
53
|
export async function handleMonitorMessage(
|
|
@@ -205,6 +205,10 @@ async function handleInboundRefresh(
|
|
|
205
205
|
// Notification actions must not wait behind provider scans or graph analysis.
|
|
206
206
|
await client.send(approvalsStatus(), 'phone', { peerPublicKey, reliable: false, ttlMs: 90_000 })
|
|
207
207
|
.catch(() => {});
|
|
208
|
+
if (payload.notificationRequestId) {
|
|
209
|
+
await replayCodexQuestion(client, payload.notificationRequestId, peerPublicKey).catch(() => false);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
208
212
|
const known = publishedCatalog(client);
|
|
209
213
|
if (known) await publishCodexQuestions(client, [...known.sessions, ...(known.history ?? [])],
|
|
210
214
|
{ replay: true, peerPublicKey }).catch(() => {});
|
|
@@ -7,7 +7,8 @@ import { RelayClient } from "../../../../../packages/core/relay-client";
|
|
|
7
7
|
import type { Payload, SessionInfo, SessionsStatus } from "../../../../../packages/protocol/schema";
|
|
8
8
|
import { loadRuntimeConfig, autoAcceptLevelFor } from "../../config";
|
|
9
9
|
import { monitorLeadership } from "./leadership";
|
|
10
|
-
import { mcpServersForSession, workspaceSkills } from "../../capabilities";
|
|
10
|
+
import { mcpServersForSession, workspaceSkills, refreshCodexMcpDescriptors } from "../../capabilities";
|
|
11
|
+
import { prepareMonitorInspection, type CatalogInspection } from "../../mesh/catalog";
|
|
11
12
|
import { inspectAgentIntegrations } from "../../install";
|
|
12
13
|
import { withCloudRunners } from "../../cloud-runners";
|
|
13
14
|
import { currentConfigRevision } from "../../config/commands";
|
|
@@ -48,12 +49,13 @@ export type SessionMonitor = {
|
|
|
48
49
|
function decorateSessions(
|
|
49
50
|
sessions: SessionsStatus["sessions"],
|
|
50
51
|
subscriptions: Set<string>,
|
|
52
|
+
inspection?: CatalogInspection,
|
|
51
53
|
): SessionsStatus["sessions"] {
|
|
52
54
|
const runtime = loadRuntimeConfig();
|
|
53
55
|
const visible = sessions.filter((session) =>
|
|
54
56
|
runtime.providerSettings[session.agent as keyof typeof runtime.providerSettings] !== false
|
|
55
57
|
);
|
|
56
|
-
const coordinated = runtime.meshEnabled ? meshCatalog(visible) : visible;
|
|
58
|
+
const coordinated = runtime.meshEnabled ? meshCatalog(visible, inspection) : visible;
|
|
57
59
|
return coordinated.map((session) => {
|
|
58
60
|
const base: SessionInfo = {
|
|
59
61
|
...session,
|
|
@@ -88,13 +90,17 @@ async function monitorSnapshot(input: {
|
|
|
88
90
|
if (loadRuntimeConfig().meshEnabled) localMeshStore().flush();
|
|
89
91
|
const { sessions, tokensRecent } = await scanSessionsCooperatively();
|
|
90
92
|
const runtime = loadRuntimeConfig();
|
|
93
|
+
const historical = includeHistory ? boundedCatalogHistory(await history(forceHistory), subscriptions) : undefined;
|
|
94
|
+
const all = [...sessions, ...(historical ?? [])].filter(session =>
|
|
95
|
+
runtime.providerSettings[session.agent as keyof typeof runtime.providerSettings] !== false);
|
|
96
|
+
if (all.some(session => session.agent === 'codex')) await refreshCodexMcpDescriptors();
|
|
97
|
+
const directories = all.flatMap(session => [session.worktree, session.cwd].filter((path): path is string => !!path));
|
|
98
|
+
const inspection = runtime.meshEnabled ? await prepareMonitorInspection(directories, forceHistory) : undefined;
|
|
91
99
|
return {
|
|
92
100
|
type: "sessions.status",
|
|
93
101
|
machine: computerDisplayName(),
|
|
94
|
-
sessions: decorateSessions(sessions, subscriptions),
|
|
95
|
-
history:
|
|
96
|
-
? decorateSessions(boundedCatalogHistory(await history(forceHistory), subscriptions), subscriptions)
|
|
97
|
-
: undefined,
|
|
102
|
+
sessions: decorateSessions(sessions, subscriptions, inspection),
|
|
103
|
+
history: historical ? decorateSessions(historical, subscriptions, inspection) : undefined,
|
|
98
104
|
tokensRecent,
|
|
99
105
|
tokenWindowHours: TOKEN_WINDOW_HOURS,
|
|
100
106
|
tokensAllTime: tokensRecent,
|
|
@@ -17,7 +17,7 @@ export function nativeTranscriptSource(session: Pick<SessionInfo, 'agent' | 'ses
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Restore an owned native source without walking every provider's logs. */
|
|
20
|
-
export function restoreNativeTranscriptSource(session: SessionInfo, path: string): boolean {
|
|
20
|
+
export function restoreNativeTranscriptSource(session: Pick<SessionInfo, 'agent' | 'sessionId'>, path: string): boolean {
|
|
21
21
|
if (!["claude", "codex"].includes(session.agent)
|
|
22
22
|
|| !/^[a-zA-Z0-9-]{1,128}$/.test(session.sessionId)) return false;
|
|
23
23
|
const name = basename(path);
|
package/cursor-plugin/README.md
CHANGED
|
@@ -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.
|
|
13
|
+
npm install -g granttap-mcp@0.8.118
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
package/cursor-plugin/mcp.json
CHANGED
|
@@ -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.
|
|
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.118\";\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.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.118";
|
|
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
|
@@ -161,5 +161,6 @@ export const SessionsRefresh = z.object({
|
|
|
161
161
|
createdAt: z.number(),
|
|
162
162
|
graphProjectId: z.string().min(1).max(128).optional(),
|
|
163
163
|
graphAnalysisRequestId: z.string().min(1).max(128).optional(),
|
|
164
|
+
notificationRequestId: z.string().min(1).max(256).optional(),
|
|
164
165
|
});
|
|
165
166
|
export type SessionsRefresh = z.infer<typeof SessionsRefresh>;
|