aiblueprint-cli 1.4.103 → 1.4.104

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/README.md CHANGED
@@ -142,6 +142,8 @@ npx skills add Melvynx/aiblueprint --skill skill-manager
142
142
  | `apex` | Structured implementation workflow |
143
143
  | `app-icon` | Generate and prepare app icons |
144
144
  | `appstore-connect` | Manage App Store Connect workflows |
145
+ | `audit-memories` | Manually audit and clean project agent documentation |
146
+ | `audit-skills` | Manually audit skill usage, invocation controls, duplicates, and scope |
145
147
  | `commit` | Quick commit and push with clean messages |
146
148
  | `create-pr` | Auto-generated pull requests |
147
149
  | `fix-pr-comments` | Resolve PR review comments |
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: audit-skills
3
+ description: Audit installed skills for observed usage, explicit-only invocation controls, duplicate discovery, and global-versus-project scope. Use only when the user explicitly invokes `$audit-skills`.
4
+ argument-hint: "[audit|fix] [project-root]"
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Audit Skills
9
+
10
+ Audit skill discovery cost and placement from current files plus observable Codex and Claude session evidence. Treat missing telemetry as uncertainty, not proof that a skill has never been useful.
11
+
12
+ ## Invocation guard
13
+
14
+ Proceed only when the current user message explicitly invokes `$audit-skills`.
15
+
16
+ - `$audit-skills audit [project-root]`: produce a read-only audit. This is the default.
17
+ - `$audit-skills fix [project-root]`: audit first, then apply only high-confidence corrections.
18
+
19
+ Resolve `project-root` from the argument or current repository. Preserve unrelated dirty changes and record Git status for every modified repository.
20
+
21
+ ## Audit
22
+
23
+ Run the deterministic inventory as a bounded job:
24
+
25
+ ```bash
26
+ bun ~/.agents/skills/audit-skills/scripts/audit-skills.mjs \
27
+ --project "$PWD" \
28
+ --format markdown
29
+ ```
30
+
31
+ The script inventories global `~/.agents/skills` and `<project>/.agents/skills`, then scans observable Codex and Claude histories for:
32
+
33
+ - explicit user references such as `$skill-name` or `/skill-name`;
34
+ - Claude `Skill` tool calls;
35
+ - agent reads of a matching `SKILL.md`;
36
+ - working directories associated with observed use;
37
+ - Claude `disable-model-invocation` and Codex `policy.allow_implicit_invocation` controls;
38
+ - name collisions and hardcoded project-root references.
39
+
40
+ Report these evidence classes separately:
41
+
42
+ - `OBSERVED_USER`: explicitly named by a user in scanned history;
43
+ - `OBSERVED_MODEL`: loaded or invoked by an agent in scanned history;
44
+ - `UNOBSERVED`: no matching evidence in scanned sources;
45
+ - `EXPLICIT_ONLY`: protected for both Claude and Codex;
46
+ - `INVOCATION_MISMATCH`: protected on only one platform;
47
+ - `LOCALITY_CANDIDATE`: global skill with a project-specific runtime dependency or at least two observed uses confined to one project;
48
+ - `NAME_COLLISION`: same skill name discovered globally and locally.
49
+
50
+ Include scan coverage and limitations. Cursor ACP stores are opaque blobs and are not counted unless a reliable parser becomes available. Never label `UNOBSERVED` as “never used” without naming the scanned time range and sources.
51
+
52
+ ## Fix
53
+
54
+ Apply changes only after the audit ledger exists.
55
+
56
+ Safe automatic corrections:
57
+
58
+ 1. Mirror an existing explicit-only decision across platforms: add `disable-model-invocation: true` when Codex already blocks implicit invocation, or add `policy.allow_implicit_invocation: false` when Claude already blocks it.
59
+ 2. Mark a skill explicit-only when its own instructions already require direct user invocation but one or both platform controls are missing.
60
+ 3. Repair metadata made stale by a move or rename.
61
+
62
+ Require explicit user direction or a project-specific runtime dependency before moving a skill between scopes. Multiple uses confined to one project justify review, not automatic relocation; a single observed use is insufficient evidence.
63
+
64
+ When moving a skill:
65
+
66
+ 1. copy the complete directory, including scripts, references, assets, and `agents/openai.yaml`;
67
+ 2. rewrite absolute self-paths and dependencies to their new canonical locations;
68
+ 3. validate the destination before removing the source;
69
+ 4. use `trash` for source removal;
70
+ 5. search all skill roots and plugin/profile metadata for stale references;
71
+ 6. confirm the name is discovered exactly once in the intended scope.
72
+
73
+ For unobserved skills, recommend rather than mutate unless their own text clearly establishes user-only intent. Rarely used incident, security, recovery, and migration skills remain protected from usage-only pruning.
74
+
75
+ ## Completion criteria
76
+
77
+ Complete an audit only when every inventoried skill has a row, scan coverage is reported, and every recommendation cites static or usage evidence.
78
+
79
+ Complete a fix only when touched skills validate, moved dependencies resolve, unintended duplicate discovery is absent, and final Git status distinguishes pre-existing changes from this run.
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "Audit Skills"
3
+ short_description: "Audit skill usage, invocation, and scope"
4
+ default_prompt: "Use $audit-skills to audit global and project skill placement."
5
+
6
+ policy:
7
+ allow_implicit_invocation: false
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Database } from "bun:sqlite";
4
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { basename, join, resolve } from "node:path";
7
+
8
+ const args = process.argv.slice(2);
9
+ const valueOf = (flag, fallback) => {
10
+ const index = args.indexOf(flag);
11
+ return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
12
+ };
13
+
14
+ const projectRoot = resolve(valueOf("--project", process.cwd()));
15
+ const format = valueOf("--format", "markdown");
16
+ const home = homedir();
17
+ const globalRoot = join(home, ".agents", "skills");
18
+ const localRoot = join(projectRoot, ".agents", "skills");
19
+
20
+ const walk = (root, accept) => {
21
+ if (!existsSync(root)) return [];
22
+ const found = [];
23
+ const visit = (path) => {
24
+ let entries = [];
25
+ try {
26
+ entries = readdirSync(path, { withFileTypes: true });
27
+ } catch {
28
+ return;
29
+ }
30
+ for (const entry of entries) {
31
+ const child = join(path, entry.name);
32
+ if (entry.isDirectory()) visit(child);
33
+ else if (entry.isFile() && accept(child)) found.push(child);
34
+ }
35
+ };
36
+ visit(root);
37
+ return found;
38
+ };
39
+
40
+ const readText = (path) => {
41
+ try {
42
+ return readFileSync(path, "utf8");
43
+ } catch {
44
+ return "";
45
+ }
46
+ };
47
+
48
+ const parseFrontmatter = (text) => {
49
+ if (!text.startsWith("---\n")) return {};
50
+ const end = text.indexOf("\n---", 4);
51
+ if (end < 0) return {};
52
+ const result = {};
53
+ for (const line of text.slice(4, end).split("\n")) {
54
+ const match = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
55
+ if (!match) continue;
56
+ result[match[1]] = match[2].trim().replace(/^(["'])(.*)\1$/, "$2");
57
+ }
58
+ return result;
59
+ };
60
+
61
+ const inventoryRoot = (root, scope) => {
62
+ if (!existsSync(root)) return [];
63
+ return readdirSync(root, { withFileTypes: true })
64
+ .filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
65
+ .map((entry) => {
66
+ const directory = join(root, entry.name);
67
+ const skillPath = join(directory, "SKILL.md");
68
+ if (!existsSync(skillPath)) return null;
69
+ const body = readText(skillPath);
70
+ const metadata = parseFrontmatter(body);
71
+ const openai = readText(join(directory, "agents", "openai.yaml"));
72
+ const selfFiles = walk(directory, (path) => !/\.(png|jpe?g|gif|webp|mp4|mov|pdf|db|sqlite)$/i.test(path));
73
+ const escapedName = (metadata.name || entry.name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
74
+ const invocationGuard = new RegExp(`(?:manual-only[^\\n]*|proceed only[^\\n]*)\\$${escapedName}(?![a-zA-Z0-9_-])`, "i");
75
+ const projectReferences = selfFiles.filter((path) => readText(path).includes(projectRoot));
76
+ return {
77
+ name: metadata.name || entry.name,
78
+ scope,
79
+ directory,
80
+ skill_path: skillPath,
81
+ description: metadata.description || "",
82
+ claude_explicit_only: metadata["disable-model-invocation"] === "true",
83
+ codex_explicit_only: /allow_implicit_invocation:\s*false\b/.test(openai),
84
+ direct_user_intent: invocationGuard.test(body.slice(0, 2500)),
85
+ project_references: projectReferences,
86
+ project_runtime_references: projectReferences.filter((path) => path.includes("/scripts/") || path.includes("/agents/")),
87
+ };
88
+ })
89
+ .filter(Boolean);
90
+ };
91
+
92
+ const skills = [...inventoryRoot(globalRoot, "global"), ...inventoryRoot(localRoot, "project")];
93
+ const names = [...new Set(skills.map((skill) => skill.name))].sort((a, b) => b.length - a.length);
94
+ const usage = new Map(names.map((name) => [name, {
95
+ user: 0,
96
+ nativeModel: 0,
97
+ manifestRead: 0,
98
+ cwd: new Map(),
99
+ sources: new Set(),
100
+ }]));
101
+
102
+ const record = (name, kind, cwd, source) => {
103
+ const target = usage.get(name);
104
+ if (!target) return;
105
+ target[kind] += 1;
106
+ if (cwd) target.cwd.set(cwd, (target.cwd.get(cwd) || 0) + 1);
107
+ target.sources.add(source);
108
+ };
109
+
110
+ const strictUserNames = (text) => {
111
+ if (typeof text !== "string") return [];
112
+ return names.filter((name) => {
113
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
114
+ return new RegExp(`\\$${escaped}(?![a-zA-Z0-9_-])`, "i").test(text);
115
+ });
116
+ };
117
+
118
+ const manifestNames = (text) => {
119
+ if (typeof text !== "string") return [];
120
+ return names.filter((name) => {
121
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
122
+ return new RegExp(`skills\\/${escaped}\\/SKILL\\.md`, "i").test(text);
123
+ });
124
+ };
125
+
126
+ const codexCwd = new Map();
127
+ const stateDbPath = join(home, ".codex", "sqlite", "state_5.sqlite");
128
+ if (existsSync(stateDbPath)) {
129
+ try {
130
+ const db = new Database(stateDbPath, { readonly: true });
131
+ for (const row of db.query("SELECT rollout_path, cwd FROM threads").all()) {
132
+ codexCwd.set(resolve(String(row.rollout_path)), String(row.cwd || ""));
133
+ }
134
+ db.close();
135
+ } catch {
136
+ // The transcript remains usable; only cwd attribution is reduced.
137
+ }
138
+ }
139
+
140
+ const transcriptRoots = [
141
+ join(home, ".codex", "sessions"),
142
+ join(home, ".codex", "archived_sessions"),
143
+ join(home, ".claude", "projects"),
144
+ join(home, ".cursor", "projects"),
145
+ ].filter(existsSync);
146
+
147
+ const transcriptFiles = transcriptRoots.flatMap((root) => walk(root, (path) => path.endsWith(".jsonl")));
148
+ const escapedNames = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
149
+ const patterns = [
150
+ `\\$(?:${escapedNames})(?:[^a-zA-Z0-9_-]|$)`,
151
+ `\\"name\\":\\"Skill\\"`,
152
+ `skills/(?:${escapedNames})/SKILL\\.md`,
153
+ ];
154
+
155
+ let matchedLines = 0;
156
+ let earliest = null;
157
+ let latest = null;
158
+ const observeTimestamp = (value) => {
159
+ if (typeof value !== "string") return;
160
+ if (!earliest || value < earliest) earliest = value;
161
+ if (!latest || value > latest) latest = value;
162
+ };
163
+
164
+ if (transcriptRoots.length > 0 && names.length > 0) {
165
+ const rgArgs = ["rg", "--json", "--no-messages", ...patterns.flatMap((pattern) => ["-e", pattern]), ...transcriptRoots];
166
+ const process = Bun.spawn(rgArgs, { stdout: "pipe", stderr: "pipe" });
167
+ const [output, errorOutput, exitCode] = await Promise.all([
168
+ new Response(process.stdout).text(),
169
+ new Response(process.stderr).text(),
170
+ process.exited,
171
+ ]);
172
+ if (exitCode > 1) {
173
+ throw new Error(`rg transcript scan failed (${exitCode}): ${errorOutput.trim()}`);
174
+ }
175
+ for (const rgLine of output.split("\n")) {
176
+ if (!rgLine) continue;
177
+ let event;
178
+ try {
179
+ event = JSON.parse(rgLine);
180
+ } catch {
181
+ continue;
182
+ }
183
+ if (event.type !== "match") continue;
184
+ const path = resolve(event.data?.path?.text || "");
185
+ const line = event.data?.lines?.text || "";
186
+ let item;
187
+ try {
188
+ item = JSON.parse(line);
189
+ } catch {
190
+ continue;
191
+ }
192
+ matchedLines += 1;
193
+ observeTimestamp(item.timestamp);
194
+ const isClaude = path.startsWith(join(home, ".claude"));
195
+ const isCodex = path.startsWith(join(home, ".codex"));
196
+ const isCursor = path.startsWith(join(home, ".cursor"));
197
+ const source = isClaude ? "claude" : isCodex ? "codex" : isCursor ? "cursor" : "unknown";
198
+ const cwd = item.cwd || codexCwd.get(path) || "";
199
+
200
+ if (isCodex && item.type === "event_msg" && item.payload?.type === "user_message") {
201
+ for (const name of strictUserNames(item.payload.message || "")) record(name, "user", cwd, source);
202
+ }
203
+
204
+ if (isClaude && item.type === "user" && !item.sourceToolAssistantUUID) {
205
+ const content = item.message?.content;
206
+ const text = typeof content === "string"
207
+ ? content
208
+ : Array.isArray(content)
209
+ ? content.filter((part) => part.type === "text").map((part) => part.text || "").join("\n")
210
+ : "";
211
+ for (const name of strictUserNames(text)) record(name, "user", cwd, source);
212
+ }
213
+
214
+ if (isCursor && item.role === "user") {
215
+ const content = item.message?.content;
216
+ const text = Array.isArray(content)
217
+ ? content.filter((part) => part.type === "text").map((part) => part.text || "").join("\n")
218
+ : "";
219
+ for (const name of strictUserNames(text)) record(name, "user", cwd, source);
220
+ }
221
+
222
+ if (isClaude && item.type === "assistant" && Array.isArray(item.message?.content)) {
223
+ for (const part of item.message.content) {
224
+ if (part.type === "tool_use" && part.name === "Skill" && usage.has(part.input?.skill)) {
225
+ record(part.input.skill, "nativeModel", cwd, source);
226
+ }
227
+ }
228
+ }
229
+
230
+ const isGenericTool = (isCodex && item.type === "response_item" && ["custom_tool_call", "function_call"].includes(item.payload?.type))
231
+ || (isClaude && item.type === "assistant")
232
+ || (isCursor && item.role === "assistant");
233
+ if (isGenericTool) {
234
+ for (const name of manifestNames(line)) record(name, "manifestRead", cwd, source);
235
+ }
236
+ }
237
+ }
238
+
239
+ if (!earliest || !latest) {
240
+ for (const path of transcriptFiles) {
241
+ try {
242
+ const timestamp = statSync(path).mtime.toISOString();
243
+ if (!earliest || timestamp < earliest) earliest = timestamp;
244
+ if (!latest || timestamp > latest) latest = timestamp;
245
+ } catch {
246
+ // Coverage count remains valid even when a file disappears during the audit.
247
+ }
248
+ }
249
+ }
250
+
251
+ const duplicateNames = new Set(
252
+ names.filter((name) => skills.filter((skill) => skill.name === name).length > 1),
253
+ );
254
+
255
+ const rows = skills.map((skill) => {
256
+ const observed = usage.get(skill.name);
257
+ const cwd = Object.fromEntries([...observed.cwd.entries()].sort((a, b) => b[1] - a[1]));
258
+ const classifications = [];
259
+ const recommendations = [];
260
+ if (observed.user > 0) classifications.push("OBSERVED_USER");
261
+ if (observed.nativeModel > 0) classifications.push("OBSERVED_MODEL");
262
+ if (observed.manifestRead > 0) classifications.push("MANIFEST_READ_EVIDENCE");
263
+ if (observed.user + observed.nativeModel + observed.manifestRead === 0) classifications.push("UNOBSERVED");
264
+ if (skill.claude_explicit_only && skill.codex_explicit_only) classifications.push("EXPLICIT_ONLY");
265
+ if (skill.claude_explicit_only !== skill.codex_explicit_only) {
266
+ classifications.push("INVOCATION_MISMATCH");
267
+ recommendations.push("mirror-explicit-only-control");
268
+ }
269
+ if (skill.direct_user_intent && !(skill.claude_explicit_only && skill.codex_explicit_only)) {
270
+ recommendations.push("make-explicit-only");
271
+ }
272
+ if (duplicateNames.has(skill.name)) classifications.push("NAME_COLLISION");
273
+ const observedCwds = Object.keys(cwd);
274
+ const observedCount = observed.user + observed.nativeModel + observed.manifestRead;
275
+ const onlyProjectUsage = observedCount >= 2
276
+ && observedCwds.length > 0
277
+ && observedCwds.every((path) => path === projectRoot || path.startsWith(`${projectRoot}/`));
278
+ const hasProjectRuntimeDependency = skill.project_runtime_references.length > 0;
279
+ if (skill.scope === "global" && (hasProjectRuntimeDependency || onlyProjectUsage)) {
280
+ classifications.push("LOCALITY_CANDIDATE");
281
+ recommendations.push(hasProjectRuntimeDependency ? "review-move-to-project-runtime-evidence" : "review-move-to-project-usage-only");
282
+ }
283
+ return {
284
+ ...skill,
285
+ usage: {
286
+ user_explicit: observed.user,
287
+ native_model: observed.nativeModel,
288
+ manifest_read_evidence: observed.manifestRead,
289
+ cwd,
290
+ sources: [...observed.sources].sort(),
291
+ },
292
+ classifications,
293
+ recommendations: [...new Set(recommendations)],
294
+ };
295
+ }).sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope));
296
+
297
+ const result = {
298
+ schema_version: 1,
299
+ generated_at: new Date().toISOString(),
300
+ project_root: projectRoot,
301
+ coverage: {
302
+ transcript_files: transcriptFiles.length,
303
+ matched_jsonl_records: matchedLines,
304
+ codex_model_invocation: "NOT_INSTRUMENTED; manifest reads are heuristic evidence only",
305
+ claude_model_invocation: "NATIVE Skill tool calls",
306
+ cursor_model_invocation: "NOT_INSTRUMENTED; manifest reads are heuristic evidence only",
307
+ earliest_timestamp: earliest,
308
+ latest_timestamp: latest,
309
+ },
310
+ summary: {
311
+ skill_rows: rows.length,
312
+ unique_names: new Set(rows.map((row) => row.name)).size,
313
+ global: rows.filter((row) => row.scope === "global").length,
314
+ project: rows.filter((row) => row.scope === "project").length,
315
+ unobserved: rows.filter((row) => row.classifications.includes("UNOBSERVED")).length,
316
+ explicit_only: rows.filter((row) => row.classifications.includes("EXPLICIT_ONLY")).length,
317
+ invocation_mismatches: rows.filter((row) => row.classifications.includes("INVOCATION_MISMATCH")).length,
318
+ locality_candidates: rows.filter((row) => row.classifications.includes("LOCALITY_CANDIDATE")).length,
319
+ name_collisions: duplicateNames.size,
320
+ },
321
+ skills: rows,
322
+ };
323
+
324
+ if (format === "json") {
325
+ console.log(JSON.stringify(result, null, 2));
326
+ process.exit(0);
327
+ }
328
+
329
+ console.log("# Skill audit");
330
+ console.log(`\n- Project: \`${projectRoot}\``);
331
+ console.log(`- Coverage: ${result.coverage.transcript_files} transcript files; ${result.coverage.matched_jsonl_records} relevant records parsed`);
332
+ console.log(`- Time range observed: ${earliest || "unknown"} → ${latest || "unknown"}`);
333
+ console.log(`- Skills: ${result.summary.skill_rows} rows / ${result.summary.unique_names} unique names`);
334
+ console.log(`- Unobserved: ${result.summary.unobserved}; explicit-only: ${result.summary.explicit_only}; invocation mismatches: ${result.summary.invocation_mismatches}`);
335
+ console.log(`- Locality candidates: ${result.summary.locality_candidates}; name collisions: ${result.summary.name_collisions}`);
336
+ console.log(`- Codex/Cursor model invocation: NOT INSTRUMENTED; manifest reads are heuristic only`);
337
+ console.log("\n| Skill | Scope | User | Claude native | Manifest read | Invocation | Classification | Recommendation |");
338
+ console.log("|---|---:|---:|---:|---:|---|---|---|");
339
+ for (const row of rows) {
340
+ const invocation = row.claude_explicit_only && row.codex_explicit_only
341
+ ? "explicit-only"
342
+ : row.claude_explicit_only || row.codex_explicit_only
343
+ ? "mismatch"
344
+ : "implicit";
345
+ console.log(`| ${row.name} | ${row.scope} | ${row.usage.user_explicit} | ${row.usage.native_model} | ${row.usage.manifest_read_evidence} | ${invocation} | ${row.classifications.join(", ") || "-"} | ${row.recommendations.join(", ") || "-"} |`);
346
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiblueprint-cli",
3
- "version": "1.4.103",
3
+ "version": "1.4.104",
4
4
  "description": "AIBlueprint CLI for setting up AI coding configurations",
5
5
  "author": "AIBlueprint",
6
6
  "license": "MIT",