@polygraph/claude-plugin 0.4.23 → 0.4.25
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/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/README.md +1 -1
- package/hooks/hooks.json +11 -0
- package/hooks/reinject-polygraph-context.mjs +161 -0
- package/package.json +1 -1
package/.mcp.json
CHANGED
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ For the strictest release flow, do not allow direct `npm publish` for the truste
|
|
|
119
119
|
## Learn More
|
|
120
120
|
|
|
121
121
|
- **[Polygraph](https://nx.dev/features/polygraph)** — Multi-repo coordination with Polygraph
|
|
122
|
-
- **[polygraph
|
|
122
|
+
- **[@polygraph/mcp](https://www.npmjs.com/package/@polygraph/mcp)** — The MCP server that powers Polygraph tools
|
|
123
123
|
- **[Nx AI Agent Skills](https://github.com/nrwl/nx-ai-agents-config)** — The main Nx AI agent skills repo
|
|
124
124
|
|
|
125
125
|
## License
|
package/hooks/hooks.json
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// SessionStart hook: when the calling agent (Claude Code or Codex) is running
|
|
2
|
+
// inside a Polygraph session, re-inject the Polygraph session id and basic
|
|
3
|
+
// session info as context. This restores facts that context compaction may
|
|
4
|
+
// have dropped, and re-establishes them on resume.
|
|
5
|
+
//
|
|
6
|
+
// Shared by the Claude and Codex plugins — both fire a SessionStart hook whose
|
|
7
|
+
// stdin carries `session_id` and whose stdout `additionalContext` is injected
|
|
8
|
+
// into the model.
|
|
9
|
+
//
|
|
10
|
+
// Everything is read from local Polygraph state — no network calls:
|
|
11
|
+
// ~/.polygraph/sidecars/<polygraphSessionId>/parent-<agentSessionId>.json
|
|
12
|
+
// maps this agent session id -> Polygraph session id (the "parent log
|
|
13
|
+
// sidecar" the CLI uses to stream parent-agent activity to the UI).
|
|
14
|
+
// ~/.polygraph/sessions/<polygraphSessionId>/session/session.json
|
|
15
|
+
// holds the session's repos, agentType, and orgId.
|
|
16
|
+
// ~/.polygraph/config.json
|
|
17
|
+
// holds selectedUrl, used to build the session URL.
|
|
18
|
+
//
|
|
19
|
+
// Outside a Polygraph session (no matching sidecar) the hook is a silent no-op.
|
|
20
|
+
|
|
21
|
+
import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
export function polygraphRoot(home = homedir()) {
|
|
27
|
+
return path.join(home, '.polygraph');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJson(file) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Find the sidecar that maps an agent session id to a Polygraph session.
|
|
39
|
+
// Returns the parsed sidecar object, or null when none matches.
|
|
40
|
+
export function findSidecar(agentSessionId, root = polygraphRoot()) {
|
|
41
|
+
if (!agentSessionId) return null;
|
|
42
|
+
|
|
43
|
+
const sidecarsDir = path.join(root, 'sidecars');
|
|
44
|
+
if (!existsSync(sidecarsDir)) return null;
|
|
45
|
+
|
|
46
|
+
const fileName = `parent-${agentSessionId}.json`;
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = readdirSync(sidecarsDir, { withFileTypes: true });
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (!entry.isDirectory()) continue;
|
|
56
|
+
const candidate = path.join(sidecarsDir, entry.name, fileName);
|
|
57
|
+
if (existsSync(candidate)) {
|
|
58
|
+
return readJson(candidate);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Build the context block for a Polygraph session, or null when the agent is
|
|
65
|
+
// not running inside a Polygraph session.
|
|
66
|
+
export function buildPolygraphContext(agentSessionId, root = polygraphRoot()) {
|
|
67
|
+
const sidecar = findSidecar(agentSessionId, root);
|
|
68
|
+
if (!sidecar || !sidecar.sessionId) return null;
|
|
69
|
+
|
|
70
|
+
const polygraphSessionId = sidecar.sessionId;
|
|
71
|
+
const agentType = sidecar.parentAgentType || 'agent';
|
|
72
|
+
const session =
|
|
73
|
+
readJson(
|
|
74
|
+
path.join(root, 'sessions', polygraphSessionId, 'session', 'session.json')
|
|
75
|
+
) ?? {};
|
|
76
|
+
const config = readJson(path.join(root, 'config.json')) ?? {};
|
|
77
|
+
|
|
78
|
+
const baseUrl = config.selectedUrl;
|
|
79
|
+
const orgId = session.orgId;
|
|
80
|
+
const sessionUrl =
|
|
81
|
+
baseUrl && orgId
|
|
82
|
+
? `${baseUrl}/orgs/${orgId}/sessions/${polygraphSessionId}`
|
|
83
|
+
: null;
|
|
84
|
+
|
|
85
|
+
const repos = Array.isArray(session.repos) ? session.repos : [];
|
|
86
|
+
const repoLines = repos.map((repo) => {
|
|
87
|
+
const role = repo.isInitiator ? ' (initiator)' : '';
|
|
88
|
+
const strategy = repo.materialization?.strategy
|
|
89
|
+
? ` [${repo.materialization.strategy}]`
|
|
90
|
+
: '';
|
|
91
|
+
return ` - ${repo.repoFullName}${role}${strategy}`;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const lines = [
|
|
95
|
+
'You are running inside a Polygraph session. Keep this in mind across compaction:',
|
|
96
|
+
`- Polygraph session id: ${polygraphSessionId}`,
|
|
97
|
+
sessionUrl ? `- Session URL: ${sessionUrl}` : null,
|
|
98
|
+
`- Parent agent (${agentType}) session id: ${agentSessionId}`,
|
|
99
|
+
repoLines.length
|
|
100
|
+
? ['- Repositories in this session:', ...repoLines].join('\n')
|
|
101
|
+
: '- Repositories in this session: (none recorded)',
|
|
102
|
+
'- To act in this session (delegating work, monitoring CI, opening PRs, etc.), load the polygraph skill for guidance.',
|
|
103
|
+
].filter((line) => line != null);
|
|
104
|
+
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readStdin() {
|
|
109
|
+
try {
|
|
110
|
+
// fd 0 — Claude Code / Codex pipe the hook payload as JSON on stdin.
|
|
111
|
+
return readFileSync(0, 'utf8');
|
|
112
|
+
} catch {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function main() {
|
|
118
|
+
let payload = {};
|
|
119
|
+
const raw = readStdin();
|
|
120
|
+
if (raw) {
|
|
121
|
+
try {
|
|
122
|
+
payload = JSON.parse(raw);
|
|
123
|
+
} catch {
|
|
124
|
+
payload = {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const agentSessionId =
|
|
129
|
+
payload.session_id || process.env.CLAUDE_CODE_SESSION_ID || '';
|
|
130
|
+
|
|
131
|
+
const context = buildPolygraphContext(agentSessionId);
|
|
132
|
+
if (!context) return; // not a Polygraph session — stay silent
|
|
133
|
+
|
|
134
|
+
process.stdout.write(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
hookSpecificOutput: {
|
|
137
|
+
hookEventName: 'SessionStart',
|
|
138
|
+
additionalContext: context,
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Run only when executed directly as a hook, not when imported (e.g. by tests).
|
|
145
|
+
// realpath both sides so the check holds when the plugin lives under a symlinked
|
|
146
|
+
// path (e.g. macOS /tmp -> /private/tmp, or a symlinked plugin install dir),
|
|
147
|
+
// where import.meta.url is realpath'd by Node but process.argv[1] is not.
|
|
148
|
+
function isMainModule() {
|
|
149
|
+
if (!process.argv[1]) return false;
|
|
150
|
+
try {
|
|
151
|
+
return (
|
|
152
|
+
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
|
|
153
|
+
);
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isMainModule()) {
|
|
160
|
+
main();
|
|
161
|
+
}
|