@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 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
+ }