@clubmatto/ai-kit 0.0.7 → 0.0.9

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
@@ -83,11 +83,12 @@ ai-kit sync
83
83
 
84
84
  ```bash
85
85
  # Bump version in package.json first
86
- git add package.json && git commit -m "release: bump version to <version>"
86
+ git add ai-kit/package.json
87
+ git commit -m "release: bump version to <version>"
87
88
 
88
- # Create git tag with prefix (triggers automated release)
89
- git tag ai-kit/v<version>
90
- git push origin ai-kit/v<version>
89
+ # Create git tag and push both (tag triggers automated release)
90
+ git tag -a ai-kit/v<version> -m "v<version>"
91
+ git push origin main --follow-tags
91
92
  ```
92
93
 
93
94
  ## License
@@ -9,6 +9,7 @@ const template_1 = require("../template");
9
9
  const output_1 = require("../output");
10
10
  const detect_1 = require("../detection/detect");
11
11
  const language_detectors_1 = require("../detection/language-detectors");
12
+ const plan_1 = require("../plan");
12
13
  const rootDir = (0, path_1.join)(__dirname, "..", "..", "..");
13
14
  const defaultSourceDirs = {
14
15
  rules: (0, path_1.join)(rootDir, "src", "rules"),
@@ -17,41 +18,31 @@ const defaultSourceDirs = {
17
18
  commands: (0, path_1.join)(rootDir, "src", "commands"),
18
19
  };
19
20
  async function sync(cwd, version, options, logger = output_1.log, sourceDirs = defaultSourceDirs) {
20
- const manifest = (0, manifest_1.readManifest)(cwd);
21
21
  logger.logo(version);
22
- if (manifest && manifest.version === version) {
23
- logger.success(`Already at latest version (${version})`);
24
- return;
25
- }
26
22
  logger.welcome();
27
- const counts = await doSync(cwd, version, options, logger, sourceDirs);
28
- logger.summary(counts);
29
- }
30
- function writeItem(aiDir, file) {
31
- const targetDir = (0, path_1.join)(aiDir, file.type);
32
- if (!(0, fs_1.existsSync)(targetDir)) {
33
- (0, fs_1.mkdirSync)(targetDir, { recursive: true });
34
- }
35
- const targetPath = (0, path_1.join)(targetDir, file.name);
36
- const parentDir = (0, path_1.dirname)(targetPath);
37
- if (!(0, fs_1.existsSync)(parentDir)) {
38
- (0, fs_1.mkdirSync)(parentDir, { recursive: true });
39
- }
40
- (0, fs_1.writeFileSync)(targetPath, (0, template_1.processTemplate)(file.content));
41
- }
42
- async function doSync(cwd, version, options, logger, sourceDirs) {
43
23
  const aiDir = (0, path_1.join)(cwd, ".agents");
44
24
  if (!(0, fs_1.existsSync)(aiDir)) {
45
25
  (0, fs_1.mkdirSync)(aiDir, { recursive: true });
46
26
  }
27
+ const manifest = (0, manifest_1.readManifest)(cwd);
28
+ const desired = buildDesiredFiles(sourceDirs, cwd, options);
29
+ const actions = (0, plan_1.diffDesired)(desired, manifest, cwd);
30
+ const changes = executeActions(actions, cwd, logger);
31
+ const newFiles = {};
32
+ for (const [relPath, df] of desired) {
33
+ newFiles[relPath] = { sourceHash: df.identity };
34
+ }
35
+ (0, manifest_1.writeManifest)(cwd, { version, files: newFiles });
36
+ logger.summary(changes);
37
+ }
38
+ function buildDesiredFiles(sourceDirs, cwd, options) {
39
+ const desired = new Map();
47
40
  const contentFiles = (0, reader_1.readContent)(sourceDirs.rules, sourceDirs.skills);
48
41
  const rootFiles = (0, reader_1.readConfigs)(sourceDirs.agents);
49
- // Detect languages and determine project type
50
42
  const detectionResult = (0, detect_1.detectLanguages)(cwd);
51
43
  let languages = detectionResult.languages;
52
44
  let isMonorepo = detectionResult.isMonorepo;
53
45
  const primaryLanguage = detectionResult.primaryLanguage;
54
- // Apply overrides from options
55
46
  if (options.allRules) {
56
47
  languages = language_detectors_1.detectors.map((d) => d.name);
57
48
  isMonorepo = true;
@@ -66,84 +57,127 @@ async function doSync(cwd, version, options, logger, sourceDirs) {
66
57
  languages = options.languages;
67
58
  isMonorepo = languages.length > 1;
68
59
  }
69
- // If no languages detected and no overrides, fall back to all rules (monorepo)
70
60
  if (languages.length === 0) {
71
- languages = language_detectors_1.detectors.map((d) => d.name);
72
61
  isMonorepo = true;
73
62
  }
74
63
  const agentsFile = (0, reader_1.readAgents)(sourceDirs.agents, isMonorepo, primaryLanguage);
75
- // Filter rules based on detected languages
76
64
  const ruleFilesToInclude = options.allRules
77
65
  ? (0, detect_1.getAllRuleFiles)()
78
66
  : (0, detect_1.getRuleFilesForLanguages)(languages);
79
67
  const rules = contentFiles.filter((f) => {
80
68
  if (f.type !== "rules")
81
69
  return false;
82
- // Always include non-language-specific rules (plan-mode.md, unsure.md, etc.)
83
- if (!(0, detect_1.isLanguageSpecificRule)(f.name)) {
70
+ if (!(0, detect_1.isLanguageSpecificRule)(f.name))
84
71
  return true;
85
- }
86
- // For language-specific rules, check if they're in the include list
87
72
  return ruleFilesToInclude.includes(f.name);
88
73
  });
89
- const stats = { rules: 0, skills: 0, commands: 0 };
90
- const installedRootFiles = [];
91
74
  if (!options.skipOpencode) {
92
75
  const commandConfig = (0, reader_1.getCommandConfig)(sourceDirs.commands);
93
- stats.commands = Object.keys(commandConfig).length;
94
- if (Object.keys(commandConfig).length > 0) {
95
- logger.section("commands");
96
- for (const name of Object.keys(commandConfig)) {
97
- logger.success(`${name}.md`);
98
- }
99
- }
100
- if (rootFiles.length > 0) {
101
- logger.section("configs");
102
- for (const file of rootFiles) {
103
- let content = file.content;
104
- if (file.name === "opencode.json" &&
105
- Object.keys(commandConfig).length > 0) {
106
- const config = JSON.parse(content);
107
- config.command = commandConfig;
108
- content = JSON.stringify(config, null, 2) + "\n";
109
- }
110
- const targetPath = (0, path_1.join)(cwd, file.name);
111
- (0, fs_1.writeFileSync)(targetPath, content);
112
- logger.success(`${file.name}`);
113
- installedRootFiles.push(file.name);
76
+ for (const file of rootFiles) {
77
+ let content = file.content;
78
+ if (file.name === "opencode.json" &&
79
+ Object.keys(commandConfig).length > 0) {
80
+ const config = JSON.parse(content);
81
+ config.command = commandConfig;
82
+ content = JSON.stringify(config, null, 2) + "\n";
114
83
  }
84
+ desired.set(file.name, {
85
+ path: file.name,
86
+ identity: (0, manifest_1.hashContent)(content),
87
+ content,
88
+ category: file.name === "opencode.json"
89
+ ? "opencode-json"
90
+ : "static",
91
+ });
115
92
  }
116
93
  }
117
94
  if (agentsFile) {
118
- const targetPath = (0, path_1.join)(cwd, agentsFile.name);
119
- (0, fs_1.writeFileSync)(targetPath, (0, template_1.processTemplate)(agentsFile.content));
120
- logger.success(`${agentsFile.name}`);
95
+ desired.set(agentsFile.name, {
96
+ path: agentsFile.name,
97
+ identity: (0, manifest_1.hashContent)(agentsFile.content),
98
+ content: (0, template_1.processTemplate)(agentsFile.content),
99
+ category: "agents-md",
100
+ });
121
101
  }
122
- if (rules.length > 0) {
123
- logger.section("rules");
124
- for (const file of rules) {
125
- writeItem(aiDir, file);
126
- logger.success(`${file.name}`);
127
- stats.rules++;
128
- }
102
+ for (const file of rules) {
103
+ const relPath = (0, path_1.join)(".agents", file.type, file.name);
104
+ desired.set(relPath, {
105
+ path: relPath,
106
+ identity: (0, manifest_1.hashContent)(file.content),
107
+ content: (0, template_1.processTemplate)(file.content),
108
+ category: "static",
109
+ });
129
110
  }
130
111
  const skills = contentFiles.filter((f) => f.type === "skills");
131
- if (skills.length > 0) {
132
- logger.section("skills");
133
- const skillDirs = [...new Set(skills.map((f) => f.name.split("/")[0]))];
134
- stats.skills = skillDirs.length;
135
- for (const dir of skillDirs) {
136
- const dirFiles = skills.filter((f) => f.name.startsWith(dir + "/"));
137
- logger.success(`${dir} (${dirFiles.length} files)`);
138
- for (const file of dirFiles) {
139
- writeItem(aiDir, file);
112
+ for (const file of skills) {
113
+ const relPath = (0, path_1.join)(".agents", file.type, file.name);
114
+ desired.set(relPath, {
115
+ path: relPath,
116
+ identity: (0, manifest_1.hashContent)(file.content),
117
+ content: (0, template_1.processTemplate)(file.content),
118
+ category: "static",
119
+ });
120
+ }
121
+ return desired;
122
+ }
123
+ function executeActions(actions, cwd, logger) {
124
+ const changes = (0, plan_1.emptySyncChanges)();
125
+ for (const action of actions) {
126
+ const targetPath = (0, path_1.join)(cwd, action.relPath);
127
+ switch (action.action) {
128
+ case "add": {
129
+ const dir = (0, path_1.dirname)(targetPath);
130
+ if (!(0, fs_1.existsSync)(dir))
131
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
132
+ (0, fs_1.writeFileSync)(targetPath, action.content);
133
+ changes.added++;
134
+ logger.success(`+ ${action.relPath}`);
135
+ break;
136
+ }
137
+ case "update": {
138
+ const dir = (0, path_1.dirname)(targetPath);
139
+ if (!(0, fs_1.existsSync)(dir))
140
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
141
+ (0, fs_1.writeFileSync)(targetPath, action.content);
142
+ changes.updated++;
143
+ logger.success(`~ ${action.relPath}`);
144
+ break;
145
+ }
146
+ case "skip":
147
+ changes.skipped++;
148
+ break;
149
+ case "merge": {
150
+ const currentContent = (0, fs_1.readFileSync)(targetPath, "utf-8");
151
+ const merged = (0, plan_1.mergeOpencodeJson)(action.content, currentContent);
152
+ (0, fs_1.writeFileSync)(targetPath, merged);
153
+ changes.merged++;
154
+ logger.success(`M ${action.relPath}`);
155
+ break;
156
+ }
157
+ case "backup": {
158
+ const currentContent = (0, fs_1.readFileSync)(targetPath, "utf-8");
159
+ const backupDir = (0, path_1.join)(cwd, ".agents");
160
+ if (!(0, fs_1.existsSync)(backupDir))
161
+ (0, fs_1.mkdirSync)(backupDir, { recursive: true });
162
+ const backupPath = (0, path_1.join)(backupDir, `${action.relPath}.bak.${Date.now()}`);
163
+ (0, fs_1.writeFileSync)(backupPath, currentContent);
164
+ (0, fs_1.writeFileSync)(targetPath, action.content);
165
+ changes.backedUp++;
166
+ logger.success(`! ${action.relPath}`);
167
+ break;
168
+ }
169
+ case "remove": {
170
+ (0, fs_1.rmSync)(targetPath, { force: true });
171
+ changes.removed++;
172
+ logger.success(`- ${action.relPath}`);
173
+ break;
174
+ }
175
+ case "warn": {
176
+ changes.warned++;
177
+ logger.warn(`${action.relPath} (modified — skipped)`);
178
+ break;
140
179
  }
141
180
  }
142
181
  }
143
- (0, manifest_1.writeManifest)(cwd, {
144
- version,
145
- installedAt: new Date().toISOString(),
146
- rootFiles: installedRootFiles,
147
- });
148
- return stats;
182
+ return changes;
149
183
  }
@@ -6,20 +6,23 @@ exports.getAllRuleFiles = getAllRuleFiles;
6
6
  exports.isLanguageSpecificRule = isLanguageSpecificRule;
7
7
  const language_detectors_1 = require("./language-detectors");
8
8
  const scanner_1 = require("./scanner");
9
+ function hasMatchingFile(files, names) {
10
+ return names.some((name) => files.some((f) => f === name || f.endsWith("/" + name)));
11
+ }
12
+ function hasMatchingExtension(files, extensions) {
13
+ return extensions.some((ext) => files.some((f) => f.endsWith(ext)));
14
+ }
9
15
  function detectLanguages(cwd) {
16
+ const { allFiles, rootFiles } = (0, scanner_1.scanTree)(cwd);
10
17
  const detected = new Set();
11
18
  for (const detector of language_detectors_1.detectors) {
12
- if ((0, scanner_1.hasAnyConfigFile)(cwd, detector.configFiles)) {
19
+ const hasConfigAtRoot = hasMatchingFile(rootFiles, detector.configFiles);
20
+ const hasConfigInTree = hasMatchingFile(allFiles, detector.configFiles);
21
+ const hasSource = hasMatchingExtension(allFiles, detector.extensions);
22
+ if (hasConfigAtRoot || hasConfigInTree || hasSource) {
13
23
  detected.add(detector.name);
14
24
  }
15
25
  }
16
- if (detected.size === 0) {
17
- for (const detector of language_detectors_1.detectors) {
18
- if ((0, scanner_1.hasAnySourceFile)(cwd, detector.extensions, 2)) {
19
- detected.add(detector.name);
20
- }
21
- }
22
- }
23
26
  const languages = Array.from(detected);
24
27
  const isMonorepo = languages.length > 1;
25
28
  const primaryLanguage = languages.length > 0 ? languages[0] : undefined;
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectors = void 0;
4
- //TODO this is a good first implementation
5
- //but we clearly want each detector to come with a detect function
6
4
  exports.detectors = [
7
5
  {
8
6
  name: "typescript",
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasAnyConfigFile = hasAnyConfigFile;
4
- exports.hasAnySourceFile = hasAnySourceFile;
5
- const fs_1 = require("fs");
6
- const path_1 = require("path");
3
+ exports.scanTree = scanTree;
4
+ const fdir_1 = require("fdir");
7
5
  const IGNORE_DIRS = [
8
6
  "node_modules",
9
7
  ".git",
@@ -13,41 +11,13 @@ const IGNORE_DIRS = [
13
11
  ".next",
14
12
  ".nuxt",
15
13
  ];
16
- function hasAnyConfigFile(cwd, configFiles) {
17
- for (const configFile of configFiles) {
18
- if ((0, fs_1.existsSync)((0, path_1.join)(cwd, configFile))) {
19
- return true;
20
- }
21
- }
22
- return false;
23
- }
24
- function hasAnySourceFile(cwd, extensions, maxDepth = 2) {
25
- return scanForExtensions(cwd, extensions, maxDepth, 0);
26
- }
27
- function scanForExtensions(dir, extensions, maxDepth, currentDepth) {
28
- if (currentDepth > maxDepth) {
29
- return false;
30
- }
31
- try {
32
- const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
33
- for (const entry of entries) {
34
- const fullPath = (0, path_1.join)(dir, entry.name);
35
- if (entry.isDirectory()) {
36
- if (!IGNORE_DIRS.includes(entry.name) && !entry.name.startsWith(".")) {
37
- if (scanForExtensions(fullPath, extensions, maxDepth, currentDepth + 1)) {
38
- return true;
39
- }
40
- }
41
- }
42
- else if (entry.isFile()) {
43
- if (extensions.some((ext) => entry.name.endsWith(ext))) {
44
- return true;
45
- }
46
- }
47
- }
48
- }
49
- catch {
50
- // If we can't read the directory, skip it
51
- }
52
- return false;
14
+ function scanTree(cwd, maxDepth = 4) {
15
+ const allFiles = new fdir_1.fdir()
16
+ .withRelativePaths()
17
+ .withMaxDepth(maxDepth)
18
+ .exclude((name) => IGNORE_DIRS.includes(name) || name.startsWith("."))
19
+ .crawl(cwd)
20
+ .sync();
21
+ const rootFiles = allFiles.filter((f) => !f.includes("/"));
22
+ return { allFiles, rootFiles };
53
23
  }
@@ -1,24 +1,38 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hashContent = hashContent;
3
4
  exports.readManifest = readManifest;
4
5
  exports.writeManifest = writeManifest;
5
6
  const fs_1 = require("fs");
6
7
  const path_1 = require("path");
8
+ const crypto_1 = require("crypto");
7
9
  const AI_DIR = ".agents";
8
10
  const MANIFEST_FILE = ".ai-kit";
9
11
  function getManifestPath(cwd) {
10
12
  return (0, path_1.join)(cwd, AI_DIR, MANIFEST_FILE);
11
13
  }
14
+ function hashContent(content) {
15
+ return (0, crypto_1.createHash)("sha256").update(content, "utf-8").digest("hex");
16
+ }
12
17
  function readManifest(cwd) {
13
18
  const path = getManifestPath(cwd);
14
19
  if (!(0, fs_1.existsSync)(path))
15
20
  return null;
16
- return JSON.parse((0, fs_1.readFileSync)(path, "utf-8"));
21
+ try {
22
+ const data = JSON.parse((0, fs_1.readFileSync)(path, "utf-8"));
23
+ if (!data.files) {
24
+ return { version: data.version || "0.0.0", files: {} };
25
+ }
26
+ return data;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
17
31
  }
18
32
  function writeManifest(cwd, manifest) {
19
33
  const dir = (0, path_1.join)(cwd, AI_DIR);
20
34
  if (!(0, fs_1.existsSync)(dir)) {
21
35
  (0, fs_1.mkdirSync)(dir, { recursive: true });
22
36
  }
23
- (0, fs_1.writeFileSync)(getManifestPath(cwd), JSON.stringify(manifest, null, 2));
37
+ (0, fs_1.writeFileSync)(getManifestPath(cwd), JSON.stringify(manifest, null, 2) + "\n");
24
38
  }
@@ -30,17 +30,28 @@ exports.log = {
30
30
  },
31
31
  section: (msg) => console.log(colorize(` → ${msg}`, "cyan")),
32
32
  success: (msg) => console.log(colorize(` ✓ ${msg}`, "green")),
33
+ warn: (msg) => console.log(colorize(` ! ${msg}`, "yellow")),
33
34
  final: (msg) => console.log(colorize(` ✓ ${msg}`, "green")),
34
35
  summary: (counts) => {
35
- console.log(colorize("\n ✓ Done!", "green"));
36
- console.log(colorize(` → `, "dim") +
37
- colorize(counts.commands.toString(), "white") +
38
- colorize(` commands`, "dim") +
39
- colorize(`, `, "dim") +
40
- colorize(counts.rules.toString(), "white") +
41
- colorize(` rules`, "dim") +
42
- colorize(`, `, "dim") +
43
- colorize(counts.skills.toString(), "white") +
44
- colorize(` skills`, "dim"));
36
+ const parts = [];
37
+ if (counts.added > 0)
38
+ parts.push(colorize(`+${counts.added} added`, "green"));
39
+ if (counts.updated > 0)
40
+ parts.push(colorize(`~${counts.updated} updated`, "white"));
41
+ if (counts.merged > 0)
42
+ parts.push(colorize(`M${counts.merged} merged`, "yellow"));
43
+ if (counts.backedUp > 0)
44
+ parts.push(colorize(`!${counts.backedUp} backed up`, "yellow"));
45
+ if (counts.removed > 0)
46
+ parts.push(colorize(`-${counts.removed} removed`, "red"));
47
+ if (counts.warned > 0)
48
+ parts.push(colorize(`!${counts.warned} modified (skipped)`, "yellow"));
49
+ if (parts.length === 0) {
50
+ console.log(colorize("\n ✓ Everything up to date!", "green"));
51
+ }
52
+ else {
53
+ console.log(colorize("\n ✓ Done!", "green"));
54
+ console.log(" " + parts.join(colorize(", ", "dim")));
55
+ }
45
56
  },
46
57
  };
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.emptySyncChanges = emptySyncChanges;
4
+ exports.diffDesired = diffDesired;
5
+ exports.mergeOpencodeJson = mergeOpencodeJson;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const template_1 = require("./template");
9
+ function emptySyncChanges() {
10
+ return {
11
+ added: 0,
12
+ updated: 0,
13
+ merged: 0,
14
+ backedUp: 0,
15
+ removed: 0,
16
+ skipped: 0,
17
+ warned: 0,
18
+ };
19
+ }
20
+ function diffDesired(desired, manifest, cwd) {
21
+ const actions = [];
22
+ for (const [relPath, df] of desired) {
23
+ const lastEntry = manifest?.files[relPath];
24
+ const targetPath = (0, path_1.join)(cwd, relPath);
25
+ let onDiskContent = null;
26
+ try {
27
+ onDiskContent = (0, fs_1.readFileSync)(targetPath, "utf-8");
28
+ }
29
+ catch {
30
+ // file doesn't exist
31
+ }
32
+ if (onDiskContent === null) {
33
+ actions.push({ action: "add", relPath, content: df.content });
34
+ continue;
35
+ }
36
+ const normalizedDisk = (0, template_1.stripDates)(onDiskContent);
37
+ const normalizedDesired = (0, template_1.stripDates)(df.content);
38
+ if (normalizedDisk === normalizedDesired) {
39
+ actions.push({ action: "skip", relPath });
40
+ continue;
41
+ }
42
+ const sourceChanged = !lastEntry || lastEntry.sourceHash !== df.identity;
43
+ if (df.category === "opencode-json") {
44
+ actions.push({ action: "merge", relPath, content: df.content });
45
+ }
46
+ else if (sourceChanged && df.category === "agents-md") {
47
+ actions.push({ action: "backup", relPath, content: df.content });
48
+ }
49
+ else if (sourceChanged) {
50
+ actions.push({ action: "update", relPath, content: df.content });
51
+ }
52
+ else {
53
+ actions.push({ action: "warn", relPath });
54
+ }
55
+ }
56
+ if (manifest) {
57
+ for (const relPath of Object.keys(manifest.files)) {
58
+ if (!desired.has(relPath)) {
59
+ actions.push({ action: "remove", relPath });
60
+ }
61
+ }
62
+ }
63
+ return actions;
64
+ }
65
+ function mergeOpencodeJson(desiredContent, currentContent) {
66
+ const desired = JSON.parse(desiredContent);
67
+ const current = JSON.parse(currentContent);
68
+ const result = { ...desired };
69
+ if (current.mcp && typeof current.mcp === "object") {
70
+ const resultMcp = result.mcp;
71
+ for (const [key, value] of Object.entries(current.mcp)) {
72
+ if (!(key in resultMcp)) {
73
+ resultMcp[key] = value;
74
+ }
75
+ }
76
+ }
77
+ if (current.command && typeof current.command === "object") {
78
+ const resultCmd = result.command;
79
+ for (const [key, value] of Object.entries(current.command)) {
80
+ if (!(key in resultCmd)) {
81
+ resultCmd[key] = value;
82
+ }
83
+ }
84
+ }
85
+ for (const key of Object.keys(current)) {
86
+ if (!(key in result)) {
87
+ result[key] = current[key];
88
+ }
89
+ }
90
+ return JSON.stringify(result, null, 2) + "\n";
91
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.processTemplate = processTemplate;
4
+ exports.stripDates = stripDates;
4
5
  function processTemplate(content) {
5
6
  const now = new Date();
6
7
  const isoDate = now.toISOString().split("T")[0];
@@ -8,3 +9,8 @@ function processTemplate(content) {
8
9
  .replace(/\{\{FOOTER}}/g, `Last updated: ${isoDate}. This file extends the global rules in @AGENTS.md. Always check both files.`)
9
10
  .replace(/\{\{AGENTS_FOOTER}}/g, `This file was last updated: ${isoDate}. Always check the \`.agents/rules/\` directory for the most current language-specific guidelines.`);
10
11
  }
12
+ function stripDates(content) {
13
+ return content
14
+ .replace(/Last updated: \d{4}-\d{2}-\d{2}\. This file extends the global rules in @AGENTS\.md\. Always check both files\./g, "{{FOOTER}}")
15
+ .replace(/This file was last updated: \d{4}-\d{2}-\d{2}\. Always check the `\.agents\/rules\/` directory for the most current language-specific guidelines\./g, "{{AGENTS_FOOTER}}");
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubmatto/ai-kit",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "The AI configuration CLI from Club Matto",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,6 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "commander": "^14.0.3",
33
+ "fdir": "6.5.0",
33
34
  "gradient-string": "^2.0.0"
34
35
  },
35
36
  "devDependencies": {
@@ -8,8 +8,21 @@ Create a commit with the following format:
8
8
 
9
9
  **First line (one-liner):**
10
10
 
11
- - Use conventional commits format: `<type>: <description>`
12
- - Examples: `feat: add init command`, `fix: resolve path issue`, `docs: update README`
11
+ - Use conventional commits format: `<type>(<scope>): <description>`
12
+ - The **scope** is the project subdirectory determine it from the changed files.
13
+
14
+ | Changed files | Scope |
15
+ |---------------------------|--------------|
16
+ | `fakedata/**` | `fakedata` |
17
+ | `ai-kit/**` | `ai-kit` |
18
+ | Mixed (multiple projects) | Pick primary |
19
+
20
+ - If changes span **multiple projects**, pick the one with the most changes, or use
21
+ `type: description` (without scope) when there is no clear primary project.
22
+ - Root-level files (e.g. `opencode.json`, `.github/`, `AGENTS.md`) and changes
23
+ spanning **all** projects get no scope.
24
+ - Examples: `feat(fakedata): add csv output`, `fix(ai-kit): resolve sync crash`,
25
+ `docs: update contributing guide`
13
26
 
14
27
  **Body (bullet list):**
15
28
 
@@ -19,26 +32,41 @@ Create a commit with the following format:
19
32
  **Sign-off:**
20
33
 
21
34
  - End with: `created with the help of <MODEL>`
22
- - Use the current model name (e.g. "MiniMax", "GPT-4", "Claude")
35
+ - Format the model name as lowercase, hyphenated: `<name>-<variant>`
36
+ - Examples: `DeepSeek V4 Flash` → `deepseek-v4-flash`, `Claude Opus 4` → `claude-opus-4`
37
+
38
+ ## Examples
39
+
40
+ **Single project — use scope:**
41
+
42
+ ```
43
+ feat(fakedata): add csv output format
44
+
45
+ - Added --format csv flag to generate command
46
+ - Implemented CSV writer with header detection
47
+ - Added tests for CSV output
48
+
49
+ created with the help of deepseek-v4-flash
50
+ ```
23
51
 
24
- ## Example
52
+ **Cross-project or root — no scope:**
25
53
 
26
54
  ```
27
- feat: add init and update commands
55
+ ci: add shared release workflow for Go projects
28
56
 
29
- - Created init command for first-time setup
30
- - Added manifest tracking in .agents/.ai-kit
31
- - Implemented update command for version sync
32
- - Added --skip-opencode option
57
+ - Created tools/go-release with build, archive, and formula generation
58
+ - Updated fakedata release workflow to use shared tool
59
+ - Added release documentation
33
60
 
34
- created with the help of MiniMax
61
+ created with the help of deepseek-v4-flash
35
62
  ```
36
63
 
37
64
  ## Process
38
65
 
39
66
  1. First, review all changes with `git status` and `git diff`
40
67
  2. If there changes you did not make, ask if you should include them
41
- 3. Write a concise one-liner following conventional commits
42
- 4. List the key changes as bullet points
43
- 5. Add the sign-off line with the current model
44
- 6. Commit with `git commit -m "your message"`
68
+ 3. Determine the scope from the changed file paths
69
+ 4. Write a concise one-liner following `<type>(<scope>): <description>`
70
+ 5. List the key changes as bullet points
71
+ 6. Add the sign-off line with the current model
72
+ 7. Commit with `git commit -m "your message"`