@graphit/cli 0.1.36 → 0.1.39
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/marketplace.json +12 -5
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/dist/commands/plugin.d.ts +2 -0
- package/dist/commands/plugin.js +39 -0
- package/dist/commands/plugin.js.map +1 -0
- package/dist/commands/setup.js +94 -5
- package/dist/commands/setup.js.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/skill-version.d.ts +20 -0
- package/dist/skill-version.js +139 -0
- package/dist/skill-version.js.map +1 -0
- package/dist/update-check.js +10 -6
- package/dist/update-check.js.map +1 -1
- package/hooks/hooks.json +30 -0
- package/package.json +8 -3
- package/scripts/plugin-status.mjs +285 -0
- package/scripts/sync-plugin-version.mjs +123 -0
- package/skills/graphit/SKILL.md +48 -10
- package/skills/graphit/VERSION.json +5 -0
- package/skills/graphit/graphit.mdc +165 -48
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const REGISTRY_URL = "https://registry.npmjs.org/@graphit/cli/latest";
|
|
9
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
const args = new Set(process.argv.slice(2));
|
|
11
|
+
const hookIndex = process.argv.indexOf("--hook");
|
|
12
|
+
const hookEvent = hookIndex >= 0 ? process.argv[hookIndex + 1] : null;
|
|
13
|
+
const pluginRoot =
|
|
14
|
+
process.env.CLAUDE_PLUGIN_ROOT ??
|
|
15
|
+
process.env.CODEX_PLUGIN_ROOT ??
|
|
16
|
+
process.env.GRAPHIT_PLUGIN_ROOT ??
|
|
17
|
+
dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
|
+
const cacheRoot =
|
|
19
|
+
process.env.CLAUDE_PLUGIN_DATA ??
|
|
20
|
+
process.env.CODEX_PLUGIN_DATA ??
|
|
21
|
+
process.env.GRAPHIT_PLUGIN_DATA ??
|
|
22
|
+
join(homedir(), ".graphit");
|
|
23
|
+
|
|
24
|
+
function readJson(path) {
|
|
25
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function tryReadJson(path) {
|
|
29
|
+
try {
|
|
30
|
+
if (!existsSync(path)) return null;
|
|
31
|
+
return readJson(path);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readFrontmatterVersion(path) {
|
|
38
|
+
try {
|
|
39
|
+
const content = readFileSync(path, "utf-8");
|
|
40
|
+
return content.match(/^skill_version:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1] ?? null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compareVersions(left, right) {
|
|
47
|
+
const leftParts = left.split(".").map(Number);
|
|
48
|
+
const rightParts = right.split(".").map(Number);
|
|
49
|
+
|
|
50
|
+
for (let index = 0; index < 3; index += 1) {
|
|
51
|
+
const leftPart = leftParts[index] ?? 0;
|
|
52
|
+
const rightPart = rightParts[index] ?? 0;
|
|
53
|
+
if (leftPart > rightPart) return 1;
|
|
54
|
+
if (leftPart < rightPart) return -1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function copiedSkillTargets(home = homedir(), cwd = process.cwd()) {
|
|
61
|
+
return [
|
|
62
|
+
{ kind: "claude-code", label: "Claude Code global", scope: "global", path: join(home, ".claude", "skills", "graphit", "SKILL.md") },
|
|
63
|
+
{ kind: "codex", label: "Codex global", scope: "global", path: join(home, ".codex", "skills", "graphit", "SKILL.md") },
|
|
64
|
+
{ kind: "cursor", label: "Cursor global", scope: "global", path: join(home, ".cursor", "rules", "graphit.mdc") },
|
|
65
|
+
{ kind: "claude-code", label: "Claude Code project", scope: "project", path: join(cwd, ".claude", "skills", "graphit", "SKILL.md") },
|
|
66
|
+
{ kind: "codex", label: "Codex project", scope: "project", path: join(cwd, ".codex", "skills", "graphit", "SKILL.md") },
|
|
67
|
+
{ kind: "cursor", label: "Cursor project", scope: "project", path: join(cwd, ".cursor", "rules", "graphit.mdc") },
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPluginManagedKind(kind) {
|
|
72
|
+
return kind === "claude-code" || kind === "codex";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readCopiedSkillVersion(path) {
|
|
76
|
+
if (!existsSync(path)) return null;
|
|
77
|
+
const versionJson = path.endsWith("SKILL.md")
|
|
78
|
+
? tryReadJson(join(dirname(path), "VERSION.json"))
|
|
79
|
+
: null;
|
|
80
|
+
return versionJson?.version ?? readFrontmatterVersion(path);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readCache(path) {
|
|
84
|
+
try {
|
|
85
|
+
if (!existsSync(path)) return null;
|
|
86
|
+
return readJson(path);
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function writeCache(path, entry) {
|
|
93
|
+
try {
|
|
94
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
95
|
+
writeFileSync(path, JSON.stringify(entry), "utf-8");
|
|
96
|
+
} catch {
|
|
97
|
+
// Cache writes should never break startup hooks.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function getLatestVersion(currentVersion) {
|
|
102
|
+
if (args.has("--skip-network")) return null;
|
|
103
|
+
|
|
104
|
+
const cachePath = join(cacheRoot, "plugin-status.json");
|
|
105
|
+
const cache = readCache(cachePath);
|
|
106
|
+
if (cache?.latestVersion && Date.now() - cache.checkedAt < TTL_MS) {
|
|
107
|
+
return cache.latestVersion;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const response = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(1500) });
|
|
112
|
+
if (!response.ok) return cache?.latestVersion ?? null;
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
if (data && typeof data.version === "string") {
|
|
115
|
+
writeCache(cachePath, { latestVersion: data.version, checkedAt: Date.now() });
|
|
116
|
+
return data.version;
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
return cache?.latestVersion ?? null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return currentVersion;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function shouldRunForPrompt() {
|
|
126
|
+
if (hookEvent !== "UserPromptSubmit") return true;
|
|
127
|
+
|
|
128
|
+
let input = "";
|
|
129
|
+
try {
|
|
130
|
+
input = readFileSync(0, "utf-8");
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const parsed = tryParseJson(input);
|
|
136
|
+
const prompt = String(parsed?.prompt ?? parsed?.user_prompt ?? input).toLowerCase();
|
|
137
|
+
return (
|
|
138
|
+
prompt.includes("graphit") &&
|
|
139
|
+
/\b(update|version|status|doctor|plugin|stale|staleness)\b/.test(prompt)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function tryParseJson(value) {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(value);
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function collectStatus() {
|
|
152
|
+
const packageJson = readJson(join(pluginRoot, "package.json"));
|
|
153
|
+
const currentVersion = packageJson.version;
|
|
154
|
+
const findings = [];
|
|
155
|
+
const metadata = [];
|
|
156
|
+
const claudePlugin = tryReadJson(join(pluginRoot, ".claude-plugin", "plugin.json"));
|
|
157
|
+
const codexPlugin = tryReadJson(join(pluginRoot, ".codex-plugin", "plugin.json"));
|
|
158
|
+
const marketplace = tryReadJson(join(pluginRoot, ".claude-plugin", "marketplace.json"));
|
|
159
|
+
const marketplacePlugin = marketplace?.plugins?.find((plugin) => plugin.name === "graphit");
|
|
160
|
+
|
|
161
|
+
const versionChecks = [
|
|
162
|
+
[".claude-plugin/plugin.json", claudePlugin?.version],
|
|
163
|
+
[".codex-plugin/plugin.json", codexPlugin?.version],
|
|
164
|
+
[".claude-plugin/marketplace.json metadata", marketplace?.metadata?.version],
|
|
165
|
+
[".claude-plugin/marketplace.json plugin", marketplacePlugin?.version],
|
|
166
|
+
["skills/graphit/VERSION.json", tryReadJson(join(pluginRoot, "skills", "graphit", "VERSION.json"))?.version],
|
|
167
|
+
["skills/graphit/SKILL.md", readFrontmatterVersion(join(pluginRoot, "skills", "graphit", "SKILL.md"))],
|
|
168
|
+
["skills/graphit/graphit.mdc", readFrontmatterVersion(join(pluginRoot, "skills", "graphit", "graphit.mdc"))],
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
for (const [label, version] of versionChecks) {
|
|
172
|
+
if (version !== currentVersion) {
|
|
173
|
+
findings.push({
|
|
174
|
+
type: "metadata-drift",
|
|
175
|
+
message: `${label} is ${version ?? "missing"}, expected ${currentVersion}`,
|
|
176
|
+
remediation: "Run `npm run sync:version` before publishing @graphit/cli.",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
metadata.push({ label, version: version ?? null });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const marketplaceSource = marketplacePlugin?.source;
|
|
183
|
+
const sourceMatches =
|
|
184
|
+
marketplaceSource?.source === "npm" &&
|
|
185
|
+
marketplaceSource.package === packageJson.name &&
|
|
186
|
+
marketplaceSource.version === currentVersion;
|
|
187
|
+
metadata.push({
|
|
188
|
+
label: ".claude-plugin/marketplace.json source",
|
|
189
|
+
version: marketplaceSource?.version ?? null,
|
|
190
|
+
source: marketplaceSource ?? null,
|
|
191
|
+
});
|
|
192
|
+
if (!sourceMatches) {
|
|
193
|
+
findings.push({
|
|
194
|
+
type: "metadata-drift",
|
|
195
|
+
message: `.claude-plugin/marketplace.json source is ${
|
|
196
|
+
marketplaceSource ? JSON.stringify(marketplaceSource) : "missing"
|
|
197
|
+
}, expected npm source ${packageJson.name}@${currentVersion}`,
|
|
198
|
+
remediation: "Run `npm run sync:version` before publishing @graphit/cli.",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const latestVersion = await getLatestVersion(currentVersion);
|
|
203
|
+
if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
|
|
204
|
+
findings.push({
|
|
205
|
+
type: "package-update",
|
|
206
|
+
message: `@graphit/cli update available: ${currentVersion} -> ${latestVersion}`,
|
|
207
|
+
remediation: "Update Graphit through your assistant's plugin manager. Claude Code users can run `claude plugin update graphit`; Codex plugin users should use the Codex plugin update command. Standalone npm users can run `npm update -g @graphit/cli`.",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const target of copiedSkillTargets()) {
|
|
212
|
+
const { label, path } = target;
|
|
213
|
+
if (!existsSync(path)) continue;
|
|
214
|
+
const version = readCopiedSkillVersion(path);
|
|
215
|
+
if (isPluginManagedKind(target.kind)) {
|
|
216
|
+
findings.push({
|
|
217
|
+
type: "legacy-copied-skill-present",
|
|
218
|
+
message: `${label} legacy copied skill exists at ${path} (${version ?? "unversioned"})`,
|
|
219
|
+
remediation: "Claude Code and Codex should use the Graphit plugin bundle. Remove legacy copied snapshots with `graphit setup --remove-legacy-copies` after confirming the plugin is installed.",
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!version || compareVersions(version, currentVersion) < 0) {
|
|
225
|
+
findings.push({
|
|
226
|
+
type: "copied-skill-stale",
|
|
227
|
+
message: `${label} copied skill is ${version ?? "unversioned"}, expected ${currentVersion}`,
|
|
228
|
+
remediation: "Cursor currently uses copied rules; refresh them with `graphit setup --editor cursor --update`.",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
packageName: packageJson.name,
|
|
235
|
+
sourceOfTruth: "plugin-bundle",
|
|
236
|
+
currentVersion,
|
|
237
|
+
latestVersion,
|
|
238
|
+
metadata,
|
|
239
|
+
findings,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatPlain(status) {
|
|
244
|
+
if (status.findings.length === 0) {
|
|
245
|
+
return `Graphit plugin status OK (${status.currentVersion}).`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return [
|
|
249
|
+
`Graphit plugin status needs attention (${status.currentVersion}):`,
|
|
250
|
+
...status.findings.map((finding) => `- ${finding.message}\n ${finding.remediation}`),
|
|
251
|
+
].join("\n");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function formatHookContext(status) {
|
|
255
|
+
if (status.findings.length === 0) return null;
|
|
256
|
+
|
|
257
|
+
return [
|
|
258
|
+
"Graphit plugin status check found actionable update/version information.",
|
|
259
|
+
...status.findings.map((finding) => `- ${finding.message}. ${finding.remediation}`),
|
|
260
|
+
].join("\n");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (hookEvent && !shouldRunForPrompt()) {
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const status = await collectStatus();
|
|
268
|
+
|
|
269
|
+
if (args.has("--json")) {
|
|
270
|
+
console.log(JSON.stringify(status, null, 2));
|
|
271
|
+
} else if (hookEvent) {
|
|
272
|
+
const additionalContext = formatHookContext(status);
|
|
273
|
+
if (additionalContext) {
|
|
274
|
+
console.log(JSON.stringify({
|
|
275
|
+
hookSpecificOutput: {
|
|
276
|
+
hookEventName: hookEvent,
|
|
277
|
+
additionalContext,
|
|
278
|
+
},
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
} else if (!args.has("--quiet") || status.findings.length > 0) {
|
|
282
|
+
console.log(formatPlain(status));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
process.exit(status.findings.length > 0 && args.has("--fail-on-findings") ? 1 : 0);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const CHECK_FLAG = "--check";
|
|
8
|
+
const cliRoot =
|
|
9
|
+
process.env.GRAPHIT_CLI_ROOT ??
|
|
10
|
+
join(fileURLToPath(new URL(".", import.meta.url)), "..");
|
|
11
|
+
const checkOnly = process.argv.includes(CHECK_FLAG);
|
|
12
|
+
|
|
13
|
+
function readJson(path) {
|
|
14
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatJson(value) {
|
|
18
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeOrReport(path, nextContent, changes) {
|
|
22
|
+
const previous = existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
23
|
+
if (previous === nextContent) return;
|
|
24
|
+
|
|
25
|
+
changes.push(path);
|
|
26
|
+
if (!checkOnly) {
|
|
27
|
+
writeFileSync(path, nextContent, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function upsertFrontmatterField(content, field, value) {
|
|
32
|
+
if (!content.startsWith("---\n")) {
|
|
33
|
+
throw new Error(`Expected frontmatter in file with ${field}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const closeIndex = content.indexOf("\n---", 4);
|
|
37
|
+
if (closeIndex === -1) {
|
|
38
|
+
throw new Error(`Could not find closing frontmatter marker for ${field}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const frontmatter = content.slice(4, closeIndex);
|
|
42
|
+
const body = content.slice(closeIndex);
|
|
43
|
+
const line = `${field}: "${value}"`;
|
|
44
|
+
const pattern = new RegExp(`^${field}:.*$`, "m");
|
|
45
|
+
|
|
46
|
+
if (pattern.test(frontmatter)) {
|
|
47
|
+
return `---\n${frontmatter.replace(pattern, line)}${body}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `---\n${line}\n${frontmatter}${body}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function updatePluginManifest(path, version, changes) {
|
|
54
|
+
const manifest = readJson(path);
|
|
55
|
+
manifest.version = version;
|
|
56
|
+
writeOrReport(path, formatJson(manifest), changes);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function updateMarketplace(path, version, packageName, changes) {
|
|
60
|
+
const marketplace = readJson(path);
|
|
61
|
+
marketplace.metadata = {
|
|
62
|
+
...(marketplace.metadata ?? {}),
|
|
63
|
+
version,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (const plugin of marketplace.plugins ?? []) {
|
|
67
|
+
if (plugin.name !== "graphit") continue;
|
|
68
|
+
plugin.version = version;
|
|
69
|
+
plugin.source = {
|
|
70
|
+
source: "npm",
|
|
71
|
+
package: packageName,
|
|
72
|
+
version,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
writeOrReport(path, formatJson(marketplace), changes);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function updateVersionJson(path, packageName, version, changes) {
|
|
80
|
+
writeOrReport(
|
|
81
|
+
path,
|
|
82
|
+
formatJson({
|
|
83
|
+
package: packageName,
|
|
84
|
+
version,
|
|
85
|
+
source: "cli/package.json",
|
|
86
|
+
}),
|
|
87
|
+
changes,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function updateFrontmatterVersion(path, version, changes) {
|
|
92
|
+
const content = readFileSync(path, "utf-8");
|
|
93
|
+
writeOrReport(path, upsertFrontmatterField(content, "skill_version", version), changes);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const packageJson = readJson(join(cliRoot, "package.json"));
|
|
97
|
+
const version = packageJson.version;
|
|
98
|
+
const packageName = packageJson.name;
|
|
99
|
+
const changes = [];
|
|
100
|
+
|
|
101
|
+
updatePluginManifest(join(cliRoot, ".claude-plugin", "plugin.json"), version, changes);
|
|
102
|
+
updatePluginManifest(join(cliRoot, ".codex-plugin", "plugin.json"), version, changes);
|
|
103
|
+
updateMarketplace(join(cliRoot, ".claude-plugin", "marketplace.json"), version, packageName, changes);
|
|
104
|
+
updateVersionJson(join(cliRoot, "skills", "graphit", "VERSION.json"), packageName, version, changes);
|
|
105
|
+
updateFrontmatterVersion(join(cliRoot, "skills", "graphit", "SKILL.md"), version, changes);
|
|
106
|
+
updateFrontmatterVersion(join(cliRoot, "skills", "graphit", "graphit.mdc"), version, changes);
|
|
107
|
+
|
|
108
|
+
if (changes.length > 0) {
|
|
109
|
+
const rel = (path) => path.startsWith(`${cliRoot}/`) ? path.slice(cliRoot.length + 1) : path;
|
|
110
|
+
if (checkOnly) {
|
|
111
|
+
console.error(
|
|
112
|
+
`Plugin version metadata is out of sync with package.json ${version}:\n` +
|
|
113
|
+
changes.map((path) => ` - ${rel(path)}`).join("\n") +
|
|
114
|
+
"\nRun `npm run sync:version` in cli/.",
|
|
115
|
+
);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(`Synced Graphit plugin metadata to ${version}:`);
|
|
120
|
+
for (const path of changes) {
|
|
121
|
+
console.log(` - ${rel(path)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
package/skills/graphit/SKILL.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
skill_version: "0.1.39"
|
|
2
3
|
name: graphit
|
|
3
4
|
description: >
|
|
4
5
|
Build HTML dashboards with Graphit. KB-aware queries, entity wrapping, cached data sources.
|
|
@@ -14,12 +15,20 @@ Build custom HTML dashboards from real data using the Graphit CLI.
|
|
|
14
15
|
|
|
15
16
|
## Session Start
|
|
16
17
|
|
|
17
|
-
Run `graphit
|
|
18
|
-
If
|
|
18
|
+
Run `graphit plugin status` at the start of every Graphit session and whenever the user asks about Graphit updates, plugin health, or stale instructions. This command is the source of truth for CLI, plugin-bundle, skill, ref, hook, and legacy copied-file version health; do not invent a manual version check.
|
|
19
|
+
If it reports action needed, tell the user the exact remediation it prints before proceeding.
|
|
20
|
+
If `graphit plugin status` fails with "command not found" or "unknown command", the CLI is too old. Tell the user to update immediately: `npm update -g @graphit/cli`. Do not proceed with an outdated CLI - commands and flags in this skill may not exist in old versions.
|
|
19
21
|
|
|
20
|
-
##
|
|
22
|
+
## Install / Update Model
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
The Graphit plugin bundle is the default and source of truth for Claude Code and Codex. It contains the CLI, skills, refs, hooks, status script, and manifests. Claude Code/Codex users should update Graphit through their assistant plugin manager, not by running copied-skill setup.
|
|
25
|
+
|
|
26
|
+
`graphit setup` is only for legacy/fallback copied-file installs, mainly Cursor or environments without plugin support. If `graphit plugin status` reports Claude Code/Codex copied snapshots, tell the user to remove them with `graphit setup --remove-legacy-copies` after confirming the plugin is installed. Use `graphit setup --legacy-copy` only when the plugin path is unavailable.
|
|
27
|
+
|
|
28
|
+
## CLI Health Gate
|
|
29
|
+
|
|
30
|
+
ALWAYS run `graphit plugin status` before diagnosing, retrying, or inventing a workaround when Graphit CLI behavior looks wrong. This includes unknown commands, unrecognized flags, missing commands described in this skill, stale-looking output, copied-skill warnings, non-zero exits with unclear messages, or the user saying Graphit/CLI/plugin/skill is not working.
|
|
31
|
+
If `graphit plugin status` reports action needed, stop and tell the user the exact remediation first. Do not continue with stale instructions unless the user explicitly asks you to proceed anyway.
|
|
23
32
|
Do NOT suggest updating for normal operational failures (expired auth, bad SQL syntax, network timeout, entity not found).
|
|
24
33
|
|
|
25
34
|
## Permission Errors
|
|
@@ -32,9 +41,9 @@ The CLI enforces the same permission model as the platform. Three error codes to
|
|
|
32
41
|
|
|
33
42
|
Connector create/delete are restricted to org admins (owner/admin role). Non-admin analysts get 403 on these commands.
|
|
34
43
|
|
|
35
|
-
##
|
|
44
|
+
## Legacy Setup
|
|
36
45
|
|
|
37
|
-
After
|
|
46
|
+
After an intentional legacy copied-file setup completes successfully, offer to add a Graphit section to the project instructions so future sessions know Graphit is available. Do not suggest legacy copied setup for Claude Code or Codex when the Graphit plugin is available. Suggested snippet:
|
|
38
47
|
|
|
39
48
|
```
|
|
40
49
|
## Graphit
|
|
@@ -114,6 +123,12 @@ NEVER embed query results as static JS variables. The dashboard iframe provides
|
|
|
114
123
|
**Wrong:** Running queries at build time, embedding results as `const data = [{...}, ...]` in the HTML.
|
|
115
124
|
**Right:** Using `graphit.resolve({sql, dataSourceId})` in a `<script>` block so data refreshes automatically.
|
|
116
125
|
|
|
126
|
+
### 6. NEVER build a dashboard on ad-hoc SQL when KB assets are missing
|
|
127
|
+
The KB-readiness gate in Workflow Step 4 is BLOCKING. When you check the KB and find it is missing metrics or dimensions the dashboard needs, you MUST create them before writing any HTML. Do not offer to skip. Do not ask "should I use ad-hoc SQL instead?" Do not frame it as the user's choice. The gate exists because ad-hoc SQL bypasses governance, breaks provenance, and produces untrustworthy graphs. The only question to ask is "here's my KB plan - approve?" not "should I follow my own instructions?"
|
|
128
|
+
|
|
129
|
+
**Wrong:** "The KB has no metrics defined for caching performance. Should I build the dashboard with ad-hoc SQL or create KB assets first? Your call."
|
|
130
|
+
**Right:** "The KB is missing 3 metrics this dashboard needs: RESULT_CACHE_HIT_RATE, DS_QUERY_LATENCY, QUERY_FAILURE_RATE. Creating them now - here's the plan: [tree]. Approve?"
|
|
131
|
+
|
|
117
132
|
---
|
|
118
133
|
|
|
119
134
|
## How to Work (governs both workflows below)
|
|
@@ -124,6 +139,11 @@ You are a colleague building WITH the user, not a batch job that explores in sil
|
|
|
124
139
|
|
|
125
140
|
**Always narrate.** The user cannot see your command output: the KB you listed, the SQL you ran, the rows that came back are invisible unless you surface them. So after each step, say what you found, what it means, and what you are about to do next. The one exception: if the user says "just build it" or "go", drop the running commentary and work straight through. Match the user's mode.
|
|
126
141
|
|
|
142
|
+
**Workflow gates are not optional.** When a step says BLOCKING or STOP, execute it. Do not offer to skip, do not say "your call", do not reframe it as optional. The KB-readiness gate (Step 4) is the most common one: if the KB is missing metrics or dimensions the dashboard needs, create them before writing HTML. The only question to ask is "here's my KB plan - approve?" not "should I follow the plan?"
|
|
143
|
+
|
|
144
|
+
- **Wrong:** "The KB is empty. Should I skip KB creation and use ad-hoc SQL, or build assets first?"
|
|
145
|
+
- **Right:** "The KB is empty. Here are the 7 metrics and 4 dimensions this dashboard needs: [tree]. Approve so I can create them?"
|
|
146
|
+
|
|
127
147
|
The difference in practice:
|
|
128
148
|
- **Weak (solo):** silently list the KB, silently run several queries, then save a complete dashboard and announce "Done - here's your dashboard."
|
|
129
149
|
- **Strong (collaborative):** "Found a **Marketing UA** data source with `{{metric:CPI}}` and `{{metric:ROAS}}` already defined. Starting with a spend-vs-installs trend - querying now." Then, after showing the rows: "Spend tracks installs except in March. Want that as the first graph, or should I look at ROAS first?"
|
|
@@ -134,10 +154,12 @@ The numbered steps in each workflow run *inside* this loop, not instead of it.
|
|
|
134
154
|
|
|
135
155
|
1. **Understand.** Ask what the dashboard should answer. Don't query until you know the goal - one clarifying question beats a wrong dashboard.
|
|
136
156
|
2. **Scope to the domain, then work down: domain -> data source -> assets.** Start narrow, not broad. Ask the user which business domain this dashboard is about (or infer it and confirm) - that scopes everything that follows. Then `graphit kb explore domain <NAME>` to see that domain's data sources and the metrics and dimensions defined on them. Reach for broad `graphit kb search` only as a fallback - when the domain is unclear or a concept spans domains. See `kb-discovery.md` and `kb-traversal.md`. Then tell the user what you found - the data source, the metrics and dimensions you'll use (by name), and the graphs you plan - before building.
|
|
137
|
-
3. **
|
|
138
|
-
4. **
|
|
139
|
-
5. **
|
|
140
|
-
6. **
|
|
157
|
+
3. **Understand the business question.** Before touching SQL, apply `domain-lenses.md` to classify the user's goal. Match column names and user intent against the five lens detection signals (Marketing, Finance, Product/Growth, Operational, Sales). Once the lens is identified: check its canonical metrics and clarification questions. Ask at most ONE clarifying question using collaborative phrasing - state your assumption and offer to redirect ("I'll compute ROAS as gross revenue over spend - redirect me if you need margin-adjusted"). Map the user's goal to specific metric and dimension concepts the dashboard needs - name them. Show the user: "This dashboard needs: ROAS_D7, CPI, LTV_CAC_RATIO (metrics) and MEDIA_SOURCE, CAMPAIGN_NAME (dimensions). Let me check if these exist in the KB." This concept list feeds the next step.
|
|
158
|
+
4. **KB-readiness gate (BLOCKING).** Check the KB against the specific concepts from step 3 - not a vague "does the KB have stuff?" but "does the KB have the ROAS, CPI, and LTV:CAC that this marketing dashboard needs?" Show a compact gap table: which concepts exist in the KB (with their formula) and which are missing. If the KB covers the needed concepts, proceed to step 5. If the KB is missing key metrics or dimensions, STOP - do not build a dashboard on raw ad-hoc SQL. Tell the user what is missing, propose creating the assets, then route to the **KB Build / Onboarding** workflow below to define the semantic layer first. Return here only after the KB has the assets the dashboard needs. A dashboard built on ad-hoc SQL instead of `{{metric:NAME}}` / `{{dim:NAME}}` references bypasses governance, breaks provenance, and produces untrustworthy results.
|
|
159
|
+
5. **Pick the data source, and handle gaps.** Use the cached data source in that domain (~100ms, preferred over live warehouse at ~10s); use its name in SQL (`FROM MARKETING_UA_DS`). If that data source is missing something you need, first look for a connection to another data source you can join (`graphit kb list relationships`); only if the data genuinely does not exist yet, propose building a new data source rather than forcing a bad query.
|
|
160
|
+
6. **Validate each query, and show the result.** Write SQL with `{{metric:NAME}}` / `{{dim:NAME}}` reference syntax for KB assets, run it with `--verbose`, and for EVERY query show the user a compact result: the reference-syntax query, a small markdown table of rows, the row count, and the trust tier. If a query returns zero rows or nulls, say so and diagnose (wrong table? filter too narrow?).
|
|
161
|
+
7. **Get approval, then build.** Once the user is happy with the data and plan, assemble the HTML: `graphit.resolve()` for live data, `graphit.chart/table/kpi` for rendering, every entity wrapped (see HARD CONSTRAINTS), all CSS in `<style>`, all JS in `<script>`. For a large dashboard, add entities incrementally - build one, show it, add the next - rather than generating everything at once. Write to a local `.html` file.
|
|
162
|
+
8. **Save - prefer entity updates - then hand off.** New dashboard: `graphit dashboard update-html <id> --file <path>`. Changing an existing one: prefer `graphit dashboard update-entity <id> <entity_id>`, which updates a single entity without regenerating (and destroying) the rest. If the response contains `entity_sql_warnings`, an entity's `data-graphit-sql` is missing, matches no data source, or fails the DS schema - fix the flagged entities and save again before reporting done. Confirm the save succeeded, then give the user the dashboard URL.
|
|
141
163
|
|
|
142
164
|
## Workflow: KB Build / Onboarding
|
|
143
165
|
|
|
@@ -219,6 +241,7 @@ For bulk creation (10+ assets), use `--skip-validate` to avoid per-asset latency
|
|
|
219
241
|
| `graphit kb get <type> <name>` | Full entity details by name |
|
|
220
242
|
| `graphit kb search <query>` | Search across all KB types (optional `--type` filter) |
|
|
221
243
|
| `graphit kb explore metric <name>` | Metric -> tables -> dimensions graph |
|
|
244
|
+
| `graphit kb explore domain <name>` | Domain -> tables -> assets full tree (preferred starting point) |
|
|
222
245
|
| `graphit kb create metric --name X --sql "..." --table T` | Create a metric (optional `--default-dimensions "D1,D2"`). Validates formula against real data before creation; blocks on failure (422). Use `--skip-validate` to bypass. |
|
|
223
246
|
| `graphit kb create dimension --name X --expr "..." --table T` | Create a dimension (type auto-inferred from schema; override with `--type` / `--output-type`). Validates expression against real data; shows cardinality + NULL rate. Use `--skip-validate` to bypass. |
|
|
224
247
|
| `graphit kb create rule --name X --sql "..." --table T` | Create a rule. Validates as WHERE clause against real data; warns if zero rows match. Use `--skip-validate` to bypass. |
|
|
@@ -232,6 +255,11 @@ For bulk creation (10+ assets), use `--skip-validate` to avoid per-asset latency
|
|
|
232
255
|
| `graphit kb update metric NAME --secondary-tables "T1,T2"` | Reference metric onto additional tables (also dimension, rule) |
|
|
233
256
|
| `graphit kb update metric NAME --parameters '<json>'` | Set parameterized metric template (JSON array, or `--parameters-file`) |
|
|
234
257
|
| `graphit kb update table NAME --domain DOMAIN` | Assign domain to table (cascades to all assets on table) |
|
|
258
|
+
| `graphit kb update domain NAME --color "#hex"` | Update domain color |
|
|
259
|
+
| `graphit kb update synonym TERM --canonical Y` | Update synonym target |
|
|
260
|
+
| `graphit kb update relationship NAME --primary-column C` | Update relationship columns |
|
|
261
|
+
| `graphit kb update topic NAME --description "..."` | Update topic description |
|
|
262
|
+
| `graphit kb update template NAME --file template.html` | Update template render code |
|
|
235
263
|
| `graphit kb update <type> <name> --description "..."` | Update description (all types) |
|
|
236
264
|
| `graphit kb delete <type> <name> --yes` | Delete any entity (all types supported) |
|
|
237
265
|
| `graphit ds list` | List cached data sources (use these for fast queries) |
|
|
@@ -262,7 +290,17 @@ For bulk creation (10+ assets), use `--skip-validate` to avoid per-asset latency
|
|
|
262
290
|
| `graphit team list` | List teams you belong to (org admins see all) |
|
|
263
291
|
| `graphit dashboard export <id>` | Export dashboard as PNG (default) or PDF (`--format pdf`, `--output <path>`). Presentations export as multi-page PDF (one slide per page) automatically. For custom multi-page layouts, add `data-graphit-page="N"` (0-indexed) to each section element. |
|
|
264
292
|
| `graphit metadata schemas --connection <id>` | List Snowflake schemas |
|
|
293
|
+
| `graphit metadata tables --schema <name> --connection <id>` | List tables in a Snowflake schema |
|
|
265
294
|
| `graphit connector list` | List active connections |
|
|
295
|
+
| `graphit connector add snowflake-keypair --account X --user Y --key <path> --warehouse W` | Add Snowflake keypair connection (admin only). OAuth and GitHub connections are configured via the web app. |
|
|
296
|
+
| `graphit connector test <id>` | Test a connection |
|
|
297
|
+
| `graphit connector remove <id> --yes` | Remove a connection (admin only) |
|
|
298
|
+
| `graphit auth login` | Authenticate with Graphit |
|
|
299
|
+
| `graphit auth status` | Show current auth status |
|
|
300
|
+
| `graphit auth logout` | Clear stored credentials |
|
|
301
|
+
| `graphit plugin status` | Check plugin bundle, CLI, skill, refs, hooks, copied legacy files, and update health |
|
|
302
|
+
| `graphit setup` | Legacy copied-file fallback for Cursor/non-plugin environments |
|
|
303
|
+
| `graphit setup --remove-legacy-copies` | Remove old Claude Code/Codex copied snapshots so the plugin bundle is source of truth |
|
|
266
304
|
|
|
267
305
|
## Presenting Results to the User
|
|
268
306
|
|