@polygraph/claude-plugin 0.4.24 → 0.4.26
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/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
|
+
}
|
package/package.json
CHANGED
|
@@ -122,7 +122,7 @@ The subagent will:
|
|
|
122
122
|
4. Call `show_session` to retrieve session details
|
|
123
123
|
5. Return a summary with session URL and repo info
|
|
124
124
|
|
|
125
|
-
**Case C only — new session just created:** After the init subagent completes and creates the new session, render the session welcome card (skip the session-details block below for this case). Prefer the `session_intro` MCP tool — call it with the session ID; it returns the card as markdown. If that tool is unavailable, run `polygraph session intro -s <sessionId>` via the CLI instead. Either way, print the result to the user verbatim as markdown — do NOT wrap it in a code block or reformat it (the logo is pre-fenced; the rest is live markdown). It needs no other input, and you do not need to call `show_session` first. Then continue (ask the user what they want, or start the requested task).
|
|
125
|
+
**Case C only — new session just created:** After the init subagent completes and creates the new session, render the session welcome card (skip the session-details block below for this case). Prefer the `session_intro` MCP tool — call it with the session ID; it returns the card as markdown. If that tool is unavailable, run `polygraph session intro -s <sessionId>` via the CLI instead. The CLI command is intentionally hidden/internal and may not appear in public command listings, but it remains the correct skill fallback for rendering the welcome card. Either way, print the result to the user verbatim as markdown — do NOT wrap it in a code block or reformat it (the logo is pre-fenced; the rest is live markdown). It needs no other input, and you do not need to call `show_session` first. Then continue (ask the user what they want, or start the requested task).
|
|
126
126
|
|
|
127
127
|
**After receiving the subagent's summary (Case B) or after calling `show_session` for an existing session (Case A), print the session details:**
|
|
128
128
|
|