predicate-skill 1.2.0 → 2.0.1
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 +6 -5
- package/README.md +88 -17
- package/cli.bundle.mjs +29269 -234
- package/dashboard/index.html +180 -0
- package/hooks/codex-cli/README.md +30 -0
- package/hooks/codex-cli/config.toml.template +10 -0
- package/hooks/codex-cli/pre-compact.sh +4 -0
- package/hooks/codex-cli/session-start.sh +7 -0
- package/hooks/codex-cli/stop.sh +4 -0
- package/hooks/cursor/README.md +46 -0
- package/hooks/cursor/mcp.json.template +12 -0
- package/hooks/cursor/pre-compact.sh +7 -0
- package/hooks/cursor/session-start.sh +7 -0
- package/hooks/cursor/stop.sh +5 -0
- package/hooks/gemini-cli/README.md +38 -0
- package/hooks/gemini-cli/pre-compact.sh +5 -0
- package/hooks/gemini-cli/session-start.sh +6 -0
- package/hooks/gemini-cli/settings.json.template +17 -0
- package/hooks/gemini-cli/stop.sh +20 -0
- package/hooks/hooks.json +25 -7
- package/hooks/opencode/README.md +40 -0
- package/hooks/opencode/opencode.json.template +18 -0
- package/hooks/opencode/pre-compact.sh +5 -0
- package/hooks/opencode/session-start.sh +6 -0
- package/hooks/opencode/stop.sh +20 -0
- package/hooks/post-tool-use.sh +10 -0
- package/hooks/pre-tool-use.sh +11 -0
- package/hooks/session-start.sh +7 -19
- package/hooks/stop.sh +20 -0
- package/hooks/vscode-copilot/README.md +43 -0
- package/hooks/vscode-copilot/pre-compact.sh +4 -0
- package/hooks/vscode-copilot/session-start.sh +7 -0
- package/hooks/vscode-copilot/settings.json.template +12 -0
- package/hooks/vscode-copilot/stop.sh +5 -0
- package/package.json +2 -1
- package/server.bundle.mjs +28422 -18694
- package/skills/predicate/SKILL.md +193 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Predicate Dashboard</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root { --bg:#0e0e10; --fg:#e8e8e8; --muted:#888; --accent:#2dd4bf; --warn:#f59e0b; --bad:#ef4444; --row:#1a1a1d; }
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body { background:var(--bg); color:var(--fg); font:14px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif; margin:0; padding:24px; }
|
|
10
|
+
h1 { margin:0 0 4px; font-size:22px; }
|
|
11
|
+
.sub { color:var(--muted); margin-bottom:24px; }
|
|
12
|
+
.grid { display:grid; gap:24px; grid-template-columns:repeat(auto-fit,minmax(420px,1fr)); }
|
|
13
|
+
.card { background:var(--row); border-radius:8px; padding:16px; }
|
|
14
|
+
.card h2 { margin:0 0 8px; font-size:14px; color:var(--accent); text-transform:uppercase; letter-spacing:.05em; }
|
|
15
|
+
.card .empty { color:var(--muted); font-style:italic; }
|
|
16
|
+
table { width:100%; border-collapse:collapse; font-size:13px; }
|
|
17
|
+
th { text-align:left; color:var(--muted); padding:4px 8px 4px 0; font-weight:500; }
|
|
18
|
+
td { padding:4px 8px 4px 0; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; vertical-align:top; word-break:break-all; }
|
|
19
|
+
td.num { text-align:right; font-variant-numeric:tabular-nums; }
|
|
20
|
+
.warn { color:var(--warn); }
|
|
21
|
+
.bad { color:var(--bad); }
|
|
22
|
+
.stats { display:grid; gap:8px; grid-template-columns:1fr 1fr; }
|
|
23
|
+
.stat { display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px solid #2a2a2e; }
|
|
24
|
+
.stat:last-child { border-bottom:none; }
|
|
25
|
+
.stat .k { color:var(--muted); }
|
|
26
|
+
.stat .v { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
|
|
27
|
+
.err { background:#3a1212; color:#f5c2c7; padding:12px; border-radius:6px; margin-bottom:16px; }
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<h1>Predicate Dashboard</h1>
|
|
32
|
+
<div class="sub">Session-history + reasoning over Fuseki @ <span id="ep"></span></div>
|
|
33
|
+
<div id="err" class="err" style="display:none"></div>
|
|
34
|
+
<div class="grid">
|
|
35
|
+
<div class="card"><h2>Stats</h2><div class="stats" id="stats"></div></div>
|
|
36
|
+
<div class="card"><h2>Recent Sessions</h2><div id="sessions"></div></div>
|
|
37
|
+
<div class="card"><h2>Hotspots <span class="sub">(files modified in ≥3 sessions)</span></h2><div id="hotspots"></div></div>
|
|
38
|
+
<div class="card"><h2>Flaky Commands <span class="sub">(failed in ≥2 sessions)</span></h2><div id="flaky"></div></div>
|
|
39
|
+
<div class="card"><h2>Active Files <span class="sub">(modified in latest session)</span></h2><div id="active"></div></div>
|
|
40
|
+
</div>
|
|
41
|
+
<script>
|
|
42
|
+
const EP = '/api/query';
|
|
43
|
+
document.getElementById('ep').textContent = location.host;
|
|
44
|
+
|
|
45
|
+
async function ask(sparql) {
|
|
46
|
+
const r = await fetch(EP, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/sparql-results+json' },
|
|
49
|
+
body: 'query=' + encodeURIComponent(sparql),
|
|
50
|
+
});
|
|
51
|
+
if (!r.ok) throw new Error('SPARQL ' + r.status + ': ' + (await r.text()));
|
|
52
|
+
return (await r.json()).results.bindings;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tbl(headers, rows, opts = {}) {
|
|
56
|
+
if (!rows.length) return '<div class="empty">' + (opts.empty || 'no data') + '</div>';
|
|
57
|
+
return '<table><thead><tr>'
|
|
58
|
+
+ headers.map(h => '<th>' + h + '</th>').join('')
|
|
59
|
+
+ '</tr></thead><tbody>'
|
|
60
|
+
+ rows.map(r => '<tr>' + r.map((c, i) => '<td' + (opts.numCols && opts.numCols.includes(i) ? ' class="num"' : '') + '>' + c + '</td>').join('') + '</tr>').join('')
|
|
61
|
+
+ '</tbody></table>';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function trim(s, n) { return s.length > n ? s.slice(0, n) + '…' : s; }
|
|
65
|
+
function shortFile(f) { return f.replace(/^file:\/\//, '').split('/').slice(-3).join('/'); }
|
|
66
|
+
|
|
67
|
+
async function loadStats() {
|
|
68
|
+
const q = `
|
|
69
|
+
PREFIX pred: <https://predicate.dev/meta#>
|
|
70
|
+
PREFIX cb: <https://predicate.dev/codebase#>
|
|
71
|
+
SELECT
|
|
72
|
+
(COUNT(DISTINCT ?session) AS ?sessions)
|
|
73
|
+
(COUNT(DISTINCT ?file) AS ?files)
|
|
74
|
+
(COUNT(DISTINCT ?cmd) AS ?cmds)
|
|
75
|
+
WHERE {
|
|
76
|
+
GRAPH <kg:abox> {
|
|
77
|
+
OPTIONAL { ?session a pred:Session }
|
|
78
|
+
OPTIONAL { ?file cb:modifiedIn ?_s1 }
|
|
79
|
+
OPTIONAL { ?cmd a cb:Command }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
const b = (await ask(q))[0];
|
|
84
|
+
const ti = await ask('SELECT (COUNT(*) AS ?n) WHERE { GRAPH <kg:abox> { ?s ?p ?o } }');
|
|
85
|
+
const ii = await ask('SELECT (COUNT(*) AS ?n) WHERE { GRAPH <kg:inferred> { ?s ?p ?o } }');
|
|
86
|
+
const stats = [
|
|
87
|
+
['sessions', b.sessions.value],
|
|
88
|
+
['files modified', b.files.value],
|
|
89
|
+
['commands', b.cmds.value],
|
|
90
|
+
['abox triples', ti[0].n.value],
|
|
91
|
+
['inferred triples', ii[0].n.value],
|
|
92
|
+
];
|
|
93
|
+
document.getElementById('stats').innerHTML = stats.map(([k, v]) => `<div class="stat"><span class="k">${k}</span><span class="v">${v}</span></div>`).join('');
|
|
94
|
+
|
|
95
|
+
// v2.0 ontology + schema-learning badges
|
|
96
|
+
const cfg = await ask(`
|
|
97
|
+
PREFIX pred: <https://predicate.dev/meta#>
|
|
98
|
+
SELECT ?ontology ?learning WHERE {
|
|
99
|
+
GRAPH <kg:meta> {
|
|
100
|
+
OPTIONAL { <urn:predicate:config> pred:initOntology ?ontology }
|
|
101
|
+
OPTIONAL { <urn:predicate:config> pred:schemaLearningEnabled ?learning }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`);
|
|
105
|
+
const c = cfg[0] ?? {};
|
|
106
|
+
const ontology = c.ontology?.value ?? '(uninitialized)';
|
|
107
|
+
const learning = c.learning?.value === 'true' ? 'on' : (c.learning?.value === 'false' ? 'off' : 'on');
|
|
108
|
+
document.title = `Predicate Dashboard — ${ontology}`;
|
|
109
|
+
const subEl = document.querySelector('.sub');
|
|
110
|
+
if (subEl) subEl.innerHTML = `Session-history + reasoning over Fuseki @ <span id="ep">${location.host}</span> · ontology: <b>${ontology}</b> · learning: <b>${learning}</b>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function loadSessions() {
|
|
114
|
+
const q = `
|
|
115
|
+
PREFIX pred: <https://predicate.dev/meta#>
|
|
116
|
+
PREFIX cb: <https://predicate.dev/codebase#>
|
|
117
|
+
SELECT ?sid ?at
|
|
118
|
+
(COUNT(DISTINCT ?file) AS ?nFiles)
|
|
119
|
+
(COUNT(DISTINCT ?ok) AS ?nOk)
|
|
120
|
+
(COUNT(DISTINCT ?bad) AS ?nBad)
|
|
121
|
+
WHERE {
|
|
122
|
+
GRAPH <kg:abox> {
|
|
123
|
+
?s a pred:Session ; pred:sessionId ?sid ; pred:at ?at .
|
|
124
|
+
OPTIONAL { ?file cb:modifiedIn ?s }
|
|
125
|
+
OPTIONAL { ?ok cb:succeededIn ?s }
|
|
126
|
+
OPTIONAL { ?bad cb:failedIn ?s }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
GROUP BY ?sid ?at
|
|
130
|
+
ORDER BY DESC(?at)
|
|
131
|
+
LIMIT 20
|
|
132
|
+
`;
|
|
133
|
+
const rows = (await ask(q)).map(b => [
|
|
134
|
+
trim(b.sid.value, 32),
|
|
135
|
+
b.at.value.slice(0, 19).replace('T', ' '),
|
|
136
|
+
b.nFiles.value,
|
|
137
|
+
b.nOk.value,
|
|
138
|
+
`<span class="${parseInt(b.nBad.value) > 0 ? 'bad' : ''}">${b.nBad.value}</span>`,
|
|
139
|
+
]);
|
|
140
|
+
document.getElementById('sessions').innerHTML = tbl(['session', 'at', 'files', 'ok', 'fail'], rows, { numCols: [2, 3, 4], empty: 'no sessions extracted yet' });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function loadDerived(varName, klass, predicate, divId, label, empty) {
|
|
144
|
+
const q = `
|
|
145
|
+
PREFIX cb: <https://predicate.dev/codebase#>
|
|
146
|
+
SELECT ?${varName} (COUNT(DISTINCT ?session) AS ?n)
|
|
147
|
+
WHERE {
|
|
148
|
+
{ GRAPH <kg:inferred> { ?${varName} a cb:${klass} } }
|
|
149
|
+
OPTIONAL { GRAPH <kg:abox> { ?${varName} cb:${predicate} ?session } }
|
|
150
|
+
}
|
|
151
|
+
GROUP BY ?${varName}
|
|
152
|
+
ORDER BY DESC(?n)
|
|
153
|
+
LIMIT 15
|
|
154
|
+
`;
|
|
155
|
+
const bindings = await ask(q);
|
|
156
|
+
const rows = bindings.map(b => {
|
|
157
|
+
const v = b[varName].value;
|
|
158
|
+
const display = klass === 'FlakyCommand' ? trim(v, 80) : shortFile(v);
|
|
159
|
+
return [display, b.n.value];
|
|
160
|
+
});
|
|
161
|
+
document.getElementById(divId).innerHTML = tbl([label, 'n'], rows, { numCols: [1], empty });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function load() {
|
|
165
|
+
try {
|
|
166
|
+
await loadStats();
|
|
167
|
+
await loadSessions();
|
|
168
|
+
await loadDerived('file', 'Hotspot', 'modifiedIn', 'hotspots', 'file', 'no hotspots — need ≥3 sessions touching the same file');
|
|
169
|
+
await loadDerived('cmd', 'FlakyCommand', 'failedIn', 'flaky', 'command', 'no flaky commands — need ≥2 sessions with a failed command');
|
|
170
|
+
await loadDerived('file', 'ActiveFile', 'modifiedIn', 'active', 'file', 'no active files — run a session, then `predicate maintain`');
|
|
171
|
+
} catch (e) {
|
|
172
|
+
document.getElementById('err').textContent = 'Failed to load: ' + e.message;
|
|
173
|
+
document.getElementById('err').style.display = 'block';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
load();
|
|
177
|
+
setInterval(load, 30000); // refresh every 30s
|
|
178
|
+
</script>
|
|
179
|
+
</body>
|
|
180
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Codex CLI adapter
|
|
2
|
+
|
|
3
|
+
## Install MCP server
|
|
4
|
+
|
|
5
|
+
Merge `config.toml.template` into `~/.codex/config.toml`, replacing
|
|
6
|
+
`__PLUGIN_DIR__` with the absolute path to this package. The 8 `kg_*`
|
|
7
|
+
tools will be available the next time you launch `codex`.
|
|
8
|
+
|
|
9
|
+
## Hooks
|
|
10
|
+
|
|
11
|
+
Codex CLI does not expose SessionStart, PreCompact, or Stop lifecycle
|
|
12
|
+
events as of writing. The three scripts in this directory are provided
|
|
13
|
+
so you can:
|
|
14
|
+
|
|
15
|
+
1. Run `session-start.sh` manually and paste output into your initial
|
|
16
|
+
Codex prompt. Or alias:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
# in ~/.zshrc or ~/.bashrc
|
|
20
|
+
codex() { command codex --context "$(predicate sessionstart 2>/dev/null)" "$@"; }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
2. Wire `pre-compact.sh` and `stop.sh` to cron for periodic maintenance:
|
|
24
|
+
|
|
25
|
+
```cron
|
|
26
|
+
*/30 * * * * /absolute/path/hooks/codex-cli/pre-compact.sh >/dev/null 2>&1
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If Codex CLI adds lifecycle hooks in the future, this adapter is ready
|
|
30
|
+
to wire them — script logic is unchanged.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Merge into ~/.codex/config.toml — replace __PLUGIN_DIR__ with the
|
|
2
|
+
# absolute path to packages/predicate-skill.
|
|
3
|
+
|
|
4
|
+
[mcp_servers.predicate]
|
|
5
|
+
command = "node"
|
|
6
|
+
args = ["__PLUGIN_DIR__/server.bundle.mjs"]
|
|
7
|
+
|
|
8
|
+
[mcp_servers.predicate.env]
|
|
9
|
+
FUSEKI_URL = "http://localhost:3030"
|
|
10
|
+
PREDICATE_DATASET = "predicate"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Codex CLI session-start adapter. Codex has no native SessionStart event;
|
|
3
|
+
# run this manually before a session and paste the output as initial context,
|
|
4
|
+
# or alias it to `codex` in your shell rc.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
predicate sessionstart 2>/dev/null || \
|
|
7
|
+
echo "Predicate: Fuseki not reachable; run \`predicate up\` first."
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Cursor adapter
|
|
2
|
+
|
|
3
|
+
Predicate exposes its 8 `kg_*` tools to Cursor over MCP, plus three optional
|
|
4
|
+
maintenance scripts you can wire into cron.
|
|
5
|
+
|
|
6
|
+
## 1. MCP server
|
|
7
|
+
|
|
8
|
+
Copy `mcp.json.template` to `.cursor/mcp.json` (project-local) or
|
|
9
|
+
`~/.cursor/mcp.json` (global), replacing `__PLUGIN_DIR__` with the absolute
|
|
10
|
+
path to your local clone, e.g. `/Users/you/code/predicate/packages/predicate-skill`.
|
|
11
|
+
|
|
12
|
+
Then in Cursor restart MCP (Cmd-Shift-P → "Reload MCP servers") and the 8
|
|
13
|
+
`kg_*` tools will be available.
|
|
14
|
+
|
|
15
|
+
## 2. Optional: SessionStart context
|
|
16
|
+
|
|
17
|
+
Cursor has no native SessionStart event. Two options:
|
|
18
|
+
|
|
19
|
+
**a. Manual:** Run `bash session-start.sh` in your terminal; paste the
|
|
20
|
+
output into `.cursor/rules/predicate.md`.
|
|
21
|
+
|
|
22
|
+
**b. Cron:** Refresh the rule file periodically:
|
|
23
|
+
|
|
24
|
+
```cron
|
|
25
|
+
*/10 * * * * bash /absolute/path/hooks/cursor/session-start.sh > /project/.cursor/rules/predicate.md
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 3. Optional: PreCompact maintenance
|
|
29
|
+
|
|
30
|
+
Cursor has no native PreCompact event. Wire `pre-compact.sh` to cron so the
|
|
31
|
+
KG stays tidy between sessions:
|
|
32
|
+
|
|
33
|
+
```cron
|
|
34
|
+
*/30 * * * * /absolute/path/hooks/cursor/pre-compact.sh >/dev/null 2>&1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 4. Optional: Stop maintenance
|
|
38
|
+
|
|
39
|
+
Run `bash stop.sh` manually after a long session, or wire it into a shell
|
|
40
|
+
shutdown alias.
|
|
41
|
+
|
|
42
|
+
## Notes
|
|
43
|
+
|
|
44
|
+
All scripts require `predicate` on `$PATH`. Install with
|
|
45
|
+
`npm install -g predicate-skill`, or use the absolute path:
|
|
46
|
+
`/abs/path/to/predicate/packages/predicate-skill/cli.bundle.mjs`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cursor pre-compact adapter: trims low-confidence stale facts and
|
|
3
|
+
# promotes any matured staged TBox proposals before context compaction.
|
|
4
|
+
# Cursor has no native PreCompact event — run manually or via cron, e.g.:
|
|
5
|
+
# */30 * * * * /path/to/hooks/cursor/pre-compact.sh
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
predicate maintain
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cursor session-start adapter: emits a plain text status line.
|
|
3
|
+
# Cursor reads stdout when invoked from a custom rule script;
|
|
4
|
+
# can also be run manually and pasted into .cursor/rules/predicate.md.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
predicate sessionstart 2>/dev/null || \
|
|
7
|
+
echo "Predicate: Fuseki not reachable; run \`predicate up\` first."
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Gemini CLI adapter
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
Merge `settings.json.template` into `~/.gemini/settings.json`, replacing
|
|
6
|
+
`__PLUGIN_DIR__` with the absolute path to this package
|
|
7
|
+
(e.g. `/Users/you/code/predicate/packages/predicate-skill`).
|
|
8
|
+
|
|
9
|
+
Restart Gemini CLI. The 8 `kg_*` tools will be available; the three hook
|
|
10
|
+
scripts will fire on `sessionStart`, `preCompress`, and `stop`.
|
|
11
|
+
|
|
12
|
+
## Hooks reference
|
|
13
|
+
|
|
14
|
+
| Event | Script | What it does |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `sessionStart` | `session-start.sh` | Prints KG status line; Gemini reads stdout as context. |
|
|
17
|
+
| `preCompress` | `pre-compact.sh` | Runs `predicate maintain` before context compression. |
|
|
18
|
+
| `stop` | `stop.sh` | Reads the Stop-hook JSON payload from stdin, pipes it to `predicate extract --from-stdin --platform gemini` to assert typed triples for the turn, then runs `predicate maintain`. Fail-open: any error exits 0. |
|
|
19
|
+
|
|
20
|
+
> **Stop-hook extraction (v1.8.0+):** `stop.sh` now invokes
|
|
21
|
+
> `predicate extract --from-stdin --platform gemini` before maintenance.
|
|
22
|
+
> The `--platform gemini` flag selects the Gemini-specific transcript
|
|
23
|
+
> adapter that maps Gemini's `{type:"tool_call", toolUse:{...}}` /
|
|
24
|
+
> `{type:"tool_result", toolResult:{...}}` events into the canonical
|
|
25
|
+
> shape the deterministic extractor understands. The adapter is
|
|
26
|
+
> permissive and falls through silently on unrecognized shapes, so it
|
|
27
|
+
> never blocks your next prompt.
|
|
28
|
+
|
|
29
|
+
## If your Gemini version doesn't expose hooks
|
|
30
|
+
|
|
31
|
+
The `hooks` block is harmless if unsupported. You can still run each script
|
|
32
|
+
manually or via cron — see `../cursor/README.md` for cron examples; the
|
|
33
|
+
syntax is identical.
|
|
34
|
+
|
|
35
|
+
## Verify wiring
|
|
36
|
+
|
|
37
|
+
Run `gemini --debug` and start a fresh session; you should see Predicate's
|
|
38
|
+
KG status line printed in the debug output before your first prompt.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gemini CLI session-start adapter. Gemini reads stdout as additional context
|
|
3
|
+
# when wired via the `hooks` block in ~/.gemini/settings.json (event: "sessionStart").
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
predicate sessionstart 2>/dev/null || \
|
|
6
|
+
echo "Predicate: Fuseki not reachable; run \`predicate up\` first."
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"predicate": {
|
|
4
|
+
"command": "node",
|
|
5
|
+
"args": ["__PLUGIN_DIR__/server.bundle.mjs"],
|
|
6
|
+
"env": {
|
|
7
|
+
"FUSEKI_URL": "http://localhost:3030",
|
|
8
|
+
"PREDICATE_DATASET": "predicate"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"hooks": [
|
|
13
|
+
{ "event": "sessionStart", "command": "bash __PLUGIN_DIR__/hooks/gemini-cli/session-start.sh" },
|
|
14
|
+
{ "event": "preCompress", "command": "bash __PLUGIN_DIR__/hooks/gemini-cli/pre-compact.sh" },
|
|
15
|
+
{ "event": "stop", "command": "bash __PLUGIN_DIR__/hooks/gemini-cli/stop.sh" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gemini CLI stop hook: reads the Stop-hook JSON payload from stdin,
|
|
3
|
+
# runs structured turn extraction (predicate extract --platform gemini),
|
|
4
|
+
# then a maintenance sweep. Fail-open: any error returns exit 0 so
|
|
5
|
+
# capture never blocks the user's next prompt.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
if ! command -v predicate >/dev/null 2>&1; then
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Buffer stdin so we can pipe it into extract.
|
|
13
|
+
payload="$(cat || true)"
|
|
14
|
+
|
|
15
|
+
if [ -n "$payload" ]; then
|
|
16
|
+
printf '%s' "$payload" | predicate extract --from-stdin --platform gemini >/dev/null 2>&1 || true
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
predicate maintain >/dev/null 2>&1 || true
|
|
20
|
+
exit 0
|
package/hooks/hooks.json
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"description": "Predicate hooks: SessionStart context banner, Stop-hook turn extraction → kg:abox via kg_assert. PreToolUse/PostToolUse raw-capture scripts are shipped but not wired by default; enable them manually if you set PREDICATE_RAW_CAPTURE=1.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh\""
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"Stop": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/stop.sh\""
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
9
27
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# OpenCode adapter
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
Merge `opencode.json.template` into `~/.config/opencode/opencode.json`
|
|
6
|
+
(or your project-local `opencode.json`), replacing `__PLUGIN_DIR__` with
|
|
7
|
+
the absolute path to this package.
|
|
8
|
+
|
|
9
|
+
Restart OpenCode. The 8 `kg_*` tools will be available, and the three
|
|
10
|
+
hook scripts will fire on `session.started`, `session.compacted`, and
|
|
11
|
+
`session.stopped`.
|
|
12
|
+
|
|
13
|
+
## Hooks reference
|
|
14
|
+
|
|
15
|
+
| Event | Script | What it does |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `session.started` | `session-start.sh` | Prints KG status line; OpenCode reads stdout as context. |
|
|
18
|
+
| `session.compacted` | `pre-compact.sh` | Runs `predicate maintain` before context compression. |
|
|
19
|
+
| `session.stopped` | `stop.sh` | Reads the Stop-hook JSON payload from stdin, pipes it to `predicate extract --from-stdin --platform opencode` to assert typed triples for the turn, then runs `predicate maintain`. Fail-open: any error exits 0. |
|
|
20
|
+
|
|
21
|
+
> **Stop-hook extraction (v1.8.0+):** `stop.sh` now invokes
|
|
22
|
+
> `predicate extract --from-stdin --platform opencode` before
|
|
23
|
+
> maintenance. The `--platform opencode` flag selects the OpenCode
|
|
24
|
+
> transcript adapter that maps `{event:"tool.before", tool:{...}}` /
|
|
25
|
+
> `{event:"tool.after", result|error}` events into the canonical shape
|
|
26
|
+
> the deterministic extractor understands. The adapter is permissive
|
|
27
|
+
> and falls through silently on unrecognized shapes, so it never blocks
|
|
28
|
+
> your next prompt.
|
|
29
|
+
|
|
30
|
+
## Verify wiring
|
|
31
|
+
|
|
32
|
+
Start an OpenCode session and check the debug log; you should see
|
|
33
|
+
Predicate's KG status line in the initial context, and `predicate maintain`
|
|
34
|
+
output when the session compacts or stops.
|
|
35
|
+
|
|
36
|
+
## If event names changed in your OpenCode version
|
|
37
|
+
|
|
38
|
+
Consult `opencode --help events` (or the OpenCode docs) for the current
|
|
39
|
+
event names. The scripts are event-agnostic — only the template's `on:`
|
|
40
|
+
keys need to match.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"mcp": {
|
|
4
|
+
"predicate": {
|
|
5
|
+
"type": "local",
|
|
6
|
+
"command": ["node", "__PLUGIN_DIR__/server.bundle.mjs"],
|
|
7
|
+
"environment": {
|
|
8
|
+
"FUSEKI_URL": "http://localhost:3030",
|
|
9
|
+
"PREDICATE_DATASET": "predicate"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"events": [
|
|
14
|
+
{ "on": "session.started", "run": "bash __PLUGIN_DIR__/hooks/opencode/session-start.sh" },
|
|
15
|
+
{ "on": "session.compacted", "run": "bash __PLUGIN_DIR__/hooks/opencode/pre-compact.sh" },
|
|
16
|
+
{ "on": "session.stopped", "run": "bash __PLUGIN_DIR__/hooks/opencode/stop.sh" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# OpenCode session-start adapter. OpenCode reads stdout as additional context
|
|
3
|
+
# when wired to the session.started event.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
predicate sessionstart 2>/dev/null || \
|
|
6
|
+
echo "Predicate: Fuseki not reachable; run \`predicate up\` first."
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# OpenCode stop hook: reads the Stop-hook JSON payload from stdin,
|
|
3
|
+
# runs structured turn extraction (predicate extract --platform opencode),
|
|
4
|
+
# then a maintenance sweep. Fail-open: any error returns exit 0 so
|
|
5
|
+
# capture never blocks the user's next prompt.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
if ! command -v predicate >/dev/null 2>&1; then
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Buffer stdin so we can pipe it into extract.
|
|
13
|
+
payload="$(cat || true)"
|
|
14
|
+
|
|
15
|
+
if [ -n "$payload" ]; then
|
|
16
|
+
printf '%s' "$payload" | predicate extract --from-stdin --platform opencode >/dev/null 2>&1 || true
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
predicate maintain >/dev/null 2>&1 || true
|
|
20
|
+
exit 0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Claude Code PostToolUse hook: records {toolName, input, output, sessionId,
|
|
3
|
+
# phase:"post"} in kg:usage. Reads Claude Code's hook payload JSON from stdin
|
|
4
|
+
# and delegates to `predicate capture --from-stdin`. Fails open.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
if command -v predicate >/dev/null 2>&1; then
|
|
8
|
+
predicate capture --from-stdin --phase post >/dev/null 2>&1 || true
|
|
9
|
+
fi
|
|
10
|
+
exit 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Claude Code PreToolUse hook: records {toolName, input, sessionId, phase:"pre"}
|
|
3
|
+
# in kg:usage. Reads Claude Code's hook payload JSON from stdin and delegates
|
|
4
|
+
# to `predicate capture --from-stdin`. Fails open: any error returns exit 0
|
|
5
|
+
# so the user's tool invocation is never blocked by capture logic.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
if command -v predicate >/dev/null 2>&1; then
|
|
9
|
+
predicate capture --from-stdin --phase pre >/dev/null 2>&1 || true
|
|
10
|
+
fi
|
|
11
|
+
exit 0
|
package/hooks/session-start.sh
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SessionStart hook: emits a short context block telling
|
|
3
|
-
#
|
|
2
|
+
# SessionStart hook for Claude Code: emits a short context block telling
|
|
3
|
+
# the agent what's in the KG. Delegates to `predicate sessionstart` so the
|
|
4
|
+
# message format stays in one place.
|
|
4
5
|
set -euo pipefail
|
|
5
|
-
FUSEKI="${FUSEKI_URL:-http://localhost:3030}"
|
|
6
|
-
DS="${PREDICATE_DATASET:-predicate}"
|
|
7
6
|
|
|
8
|
-
if
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
if MSG="$(predicate sessionstart 2>/dev/null)"; then
|
|
8
|
+
:
|
|
9
|
+
else
|
|
10
|
+
MSG="Predicate: Fuseki not reachable; KG tools may fail. Start it with \`predicate up\`."
|
|
11
11
|
fi
|
|
12
12
|
|
|
13
|
-
GOALS=$(curl -fsS "$FUSEKI/$DS/query" \
|
|
14
|
-
--data-urlencode "query=PREFIX pred: <https://predicate.dev/meta#>
|
|
15
|
-
SELECT (COUNT(*) AS ?n) WHERE { GRAPH <kg:goals> { ?g pred:status \"active\" } }" \
|
|
16
|
-
--header "Accept: application/sparql-results+json" \
|
|
17
|
-
| jq -r '.results.bindings[0].n.value // "0"')
|
|
18
|
-
|
|
19
|
-
CONCEPTS=$(curl -fsS "$FUSEKI/$DS/query" \
|
|
20
|
-
--data-urlencode "query=SELECT (COUNT(DISTINCT ?c) AS ?n) WHERE { GRAPH <kg:tbox> { ?c a <http://www.w3.org/2002/07/owl#Class> } }" \
|
|
21
|
-
--header "Accept: application/sparql-results+json" \
|
|
22
|
-
| jq -r '.results.bindings[0].n.value // "0"')
|
|
23
|
-
|
|
24
|
-
MSG="Predicate ready: ${GOALS} active goals, ${CONCEPTS} TBox classes. Use kg_explore_schema before drafting SPARQL."
|
|
25
13
|
jq -n --arg m "$MSG" '{ additional_context: $m }'
|
package/hooks/stop.sh
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Claude Code Stop hook: reads the Stop-hook JSON payload from stdin,
|
|
3
|
+
# runs structured turn extraction (predicate extract), then a
|
|
4
|
+
# maintenance sweep. Fail-open: any error returns exit 0 so capture
|
|
5
|
+
# never blocks the user's next prompt.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
if ! command -v predicate >/dev/null 2>&1; then
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Buffer stdin so we can tee it into extract.
|
|
13
|
+
payload="$(cat || true)"
|
|
14
|
+
|
|
15
|
+
if [ -n "$payload" ]; then
|
|
16
|
+
printf '%s' "$payload" | predicate extract --from-stdin >/dev/null 2>&1 || true
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
predicate maintain >/dev/null 2>&1 || true
|
|
20
|
+
exit 0
|