@justinmoon/agent-tools 0.1.0
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 +57 -0
- package/bin/agent-tools.mjs +298 -0
- package/package.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @justinmoon/agent-tools
|
|
2
|
+
|
|
3
|
+
CLI tooling for AI agent project hygiene.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @justinmoon/agent-tools <command>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
### list-docs
|
|
14
|
+
|
|
15
|
+
List docs in `./docs/` with frontmatter summaries and "read when" hints.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @justinmoon/agent-tools list-docs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### check-docs
|
|
22
|
+
|
|
23
|
+
Validate all docs have proper frontmatter. Use in CI.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @justinmoon/agent-tools check-docs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
#### Doc format
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
---
|
|
33
|
+
summary: Brief description of what this doc covers
|
|
34
|
+
read_when:
|
|
35
|
+
- when working on feature X
|
|
36
|
+
- when debugging Y
|
|
37
|
+
---
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### check-justfile
|
|
41
|
+
|
|
42
|
+
Verify all justfile recipes have a `#` summary comment. Use in CI.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx @justinmoon/agent-tools check-justfile
|
|
46
|
+
npx @justinmoon/agent-tools check-justfile path/to/justfile
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## CI Integration
|
|
50
|
+
|
|
51
|
+
Add to your `pre-merge` justfile recipe:
|
|
52
|
+
|
|
53
|
+
```just
|
|
54
|
+
pre-merge: lint test
|
|
55
|
+
npx -y @justinmoon/agent-tools check-docs
|
|
56
|
+
npx -y @justinmoon/agent-tools check-justfile
|
|
57
|
+
```
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join, relative, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const command = args[0];
|
|
8
|
+
const commandArgs = args.slice(1);
|
|
9
|
+
|
|
10
|
+
// ── Dispatch ────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const commands = {
|
|
13
|
+
'list-docs': listDocs,
|
|
14
|
+
'check-docs': checkDocs,
|
|
15
|
+
'check-justfile': checkJustfile,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (!command || command === '--help' || command === '-h') {
|
|
19
|
+
console.log(`agent-tools - AI agent project hygiene.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
agent-tools <command>
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
list-docs List docs in ./docs/ with summaries and read-when hints
|
|
26
|
+
check-docs Validate all docs have proper frontmatter (CI)
|
|
27
|
+
check-justfile Verify all justfile recipes have a summary comment (CI)
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--help Show this help`);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!commands[command]) {
|
|
35
|
+
console.error(`Unknown command: ${command}\nRun 'agent-tools --help' for usage.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
commands[command](commandArgs);
|
|
40
|
+
|
|
41
|
+
// ── Docs shared ─────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const EXCLUDED_DIRS = new Set(['archive', 'research']);
|
|
44
|
+
|
|
45
|
+
function walkMarkdownFiles(dir, base = dir) {
|
|
46
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
47
|
+
const files = [];
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (entry.name.startsWith('.')) continue;
|
|
50
|
+
const fullPath = join(dir, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
53
|
+
files.push(...walkMarkdownFiles(fullPath, base));
|
|
54
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
55
|
+
files.push(relative(base, fullPath));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractMetadata(fullPath) {
|
|
62
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
63
|
+
|
|
64
|
+
if (!content.startsWith('---')) {
|
|
65
|
+
return { summary: null, readWhen: [], error: 'missing front matter' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const endIndex = content.indexOf('\n---', 3);
|
|
69
|
+
if (endIndex === -1) {
|
|
70
|
+
return { summary: null, readWhen: [], error: 'unterminated front matter' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const frontMatter = content.slice(3, endIndex).trim();
|
|
74
|
+
const lines = frontMatter.split('\n');
|
|
75
|
+
|
|
76
|
+
let summaryLine = null;
|
|
77
|
+
const readWhen = [];
|
|
78
|
+
let collectingField = null;
|
|
79
|
+
|
|
80
|
+
for (const rawLine of lines) {
|
|
81
|
+
const line = rawLine.trim();
|
|
82
|
+
|
|
83
|
+
if (line.startsWith('summary:')) {
|
|
84
|
+
summaryLine = line;
|
|
85
|
+
collectingField = null;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (line.startsWith('read_when:')) {
|
|
90
|
+
collectingField = 'read_when';
|
|
91
|
+
const inline = line.slice('read_when:'.length).trim();
|
|
92
|
+
if (inline.startsWith('[') && inline.endsWith(']')) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(inline.replace(/'/g, '"'));
|
|
95
|
+
if (Array.isArray(parsed)) {
|
|
96
|
+
readWhen.push(...parsed.filter(v => v != null).map(v => String(v).trim()).filter(Boolean));
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore malformed inline arrays
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (collectingField === 'read_when') {
|
|
106
|
+
if (line.startsWith('- ')) {
|
|
107
|
+
const hint = line.slice(2).trim();
|
|
108
|
+
if (hint) readWhen.push(hint);
|
|
109
|
+
} else if (line === '') {
|
|
110
|
+
// skip blank lines within field
|
|
111
|
+
} else {
|
|
112
|
+
collectingField = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!summaryLine) {
|
|
118
|
+
return { summary: null, readWhen, error: 'summary key missing' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const summaryValue = summaryLine.slice('summary:'.length).trim();
|
|
122
|
+
const normalized = summaryValue
|
|
123
|
+
.replace(/^['"]|['"]$/g, '')
|
|
124
|
+
.replace(/\s+/g, ' ')
|
|
125
|
+
.trim();
|
|
126
|
+
|
|
127
|
+
if (!normalized) {
|
|
128
|
+
return { summary: null, readWhen, error: 'summary is empty' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { summary: normalized, readWhen };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── list-docs ───────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function listDocs(args) {
|
|
137
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
138
|
+
console.log(`agent-tools list-docs - List docs with summaries and read-when hints.
|
|
139
|
+
|
|
140
|
+
Usage:
|
|
141
|
+
agent-tools list-docs
|
|
142
|
+
|
|
143
|
+
Scans ./docs/ for markdown files with YAML frontmatter.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const docsDir = join(process.cwd(), 'docs');
|
|
148
|
+
if (!existsSync(docsDir)) {
|
|
149
|
+
console.log('No docs/ folder found.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const files = walkMarkdownFiles(docsDir);
|
|
154
|
+
console.log('Listing all markdown files in docs folder:');
|
|
155
|
+
|
|
156
|
+
for (const relativePath of files) {
|
|
157
|
+
const fullPath = join(docsDir, relativePath);
|
|
158
|
+
const { summary, readWhen, error } = extractMetadata(fullPath);
|
|
159
|
+
if (summary) {
|
|
160
|
+
console.log(`${relativePath} - ${summary}`);
|
|
161
|
+
if (readWhen.length > 0) {
|
|
162
|
+
console.log(` Read when: ${readWhen.join('; ')}`);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const reason = error ? ` - [${error}]` : '';
|
|
166
|
+
console.log(`${relativePath}${reason}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(
|
|
171
|
+
'\nReminder: keep docs up to date as behavior changes. When your task matches any "Read when" hint above, read that doc before coding, and suggest new coverage when it is missing.'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── check-docs ──────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function checkDocs(args) {
|
|
178
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
179
|
+
console.log(`agent-tools check-docs - Validate docs frontmatter (for CI).
|
|
180
|
+
|
|
181
|
+
Usage:
|
|
182
|
+
agent-tools check-docs
|
|
183
|
+
|
|
184
|
+
Ensures every .md in ./docs/ has a summary and read_when in frontmatter.`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const docsDir = join(process.cwd(), 'docs');
|
|
189
|
+
if (!existsSync(docsDir)) {
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const files = walkMarkdownFiles(docsDir);
|
|
194
|
+
const failures = [];
|
|
195
|
+
|
|
196
|
+
for (const relativePath of files) {
|
|
197
|
+
const fullPath = join(docsDir, relativePath);
|
|
198
|
+
const { summary, error } = extractMetadata(fullPath);
|
|
199
|
+
if (!summary && error) {
|
|
200
|
+
failures.push({ file: relativePath, error });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (failures.length > 0) {
|
|
205
|
+
console.error('check-docs failed:');
|
|
206
|
+
for (const { file, error } of failures) {
|
|
207
|
+
console.error(` ${file}: ${error}`);
|
|
208
|
+
}
|
|
209
|
+
console.error(`\nAll docs must have frontmatter with 'summary' and 'read_when' fields.`);
|
|
210
|
+
console.error('Example:');
|
|
211
|
+
console.error('---');
|
|
212
|
+
console.error('summary: Brief description of what this doc covers');
|
|
213
|
+
console.error('read_when:');
|
|
214
|
+
console.error(' - when to read this doc');
|
|
215
|
+
console.error('---');
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(`check-docs passed: ${files.length} docs have valid frontmatter`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── check-justfile ──────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function checkJustfile(args) {
|
|
225
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
226
|
+
console.log(`agent-tools check-justfile - Verify all recipes have a summary comment.
|
|
227
|
+
|
|
228
|
+
Usage:
|
|
229
|
+
agent-tools check-justfile [path/to/justfile]
|
|
230
|
+
|
|
231
|
+
just --list uses the # comment above each recipe as its summary.
|
|
232
|
+
This tool ensures no recipe is missing one.`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const justfilePath = resolve(args[0] || 'justfile');
|
|
237
|
+
|
|
238
|
+
let content;
|
|
239
|
+
try {
|
|
240
|
+
content = readFileSync(justfilePath, 'utf8');
|
|
241
|
+
} catch (e) {
|
|
242
|
+
if (e.code === 'ENOENT') {
|
|
243
|
+
console.log(`No justfile found at ${justfilePath}`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
throw e;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const lines = content.split('\n');
|
|
250
|
+
const missing = [];
|
|
251
|
+
let recipeCount = 0;
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < lines.length; i++) {
|
|
254
|
+
const line = lines[i];
|
|
255
|
+
|
|
256
|
+
const recipeMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\s*(?:[^:=]*)?:/);
|
|
257
|
+
if (!recipeMatch) continue;
|
|
258
|
+
|
|
259
|
+
const name = recipeMatch[1];
|
|
260
|
+
if (['set', 'export', 'alias', 'import', 'mod'].includes(name)) continue;
|
|
261
|
+
|
|
262
|
+
recipeCount++;
|
|
263
|
+
|
|
264
|
+
// Look backwards for a comment (skip blank lines)
|
|
265
|
+
let hasComment = false;
|
|
266
|
+
let isPrivate = false;
|
|
267
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
268
|
+
const prev = lines[j].trim();
|
|
269
|
+
if (prev === '') continue;
|
|
270
|
+
if (prev === '[private]') {
|
|
271
|
+
isPrivate = true;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
if (prev.startsWith('#')) {
|
|
275
|
+
hasComment = true;
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!hasComment && !isPrivate) {
|
|
281
|
+
missing.push({ name, line: i + 1 });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (missing.length > 0) {
|
|
286
|
+
console.error(`check-justfile failed: ${missing.length} recipe(s) missing summary comment:\n`);
|
|
287
|
+
for (const { name, line } of missing) {
|
|
288
|
+
console.error(` line ${line}: ${name}`);
|
|
289
|
+
}
|
|
290
|
+
console.error(`\nAdd a comment above each recipe, e.g.:`);
|
|
291
|
+
console.error(` # Build the project.`);
|
|
292
|
+
console.error(` build:`);
|
|
293
|
+
console.error(` cargo build`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log(`check-justfile passed: ${recipeCount} recipes, all have comments`);
|
|
298
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@justinmoon/agent-tools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tooling for AI agent project hygiene: docs indexing, justfile linting.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agent-tools": "./bin/agent-tools.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/justinmoon/agent-tools.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"docs",
|
|
17
|
+
"justfile",
|
|
18
|
+
"lint",
|
|
19
|
+
"ci"
|
|
20
|
+
]
|
|
21
|
+
}
|